diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ff21a00..19869e8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -16,7 +16,28 @@ "source": "./plugins/core", "description": "Commands, agents, skills, and context for AI-assisted development workflows", "version": "9.22.0", - "tags": ["commands", "agents", "skills", "workflows", "essential"] + "tags": [ + "commands", + "agents", + "skills", + "workflows", + "essential" + ] + }, + { + "name": "hermes-tweet", + "source": "./plugins/hermes-tweet", + "description": "Hermes Agent X/Twitter plugin for research, profile and post reads, and gated write actions through Xquik.", + "version": "0.1.6", + "tags": [ + "hermes-agent", + "hermes-plugin", + "xquik", + "twitter", + "x", + "social-media", + "automation" + ] } ] } diff --git a/README.md b/README.md index fd63db3..00e3bce 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,15 @@ If you prefer to install manually, run these in Claude Code: ``` /plugin install ai-coding-config +/plugin install hermes-tweet ``` ## Todo Persistence Across Compaction +### Hermes Tweet Plugin + +Hermes Tweet adds X/Twitter research, profile reads, post reads, and gated action tools for Hermes Agent users. Exploratory catalog access works without a key; read tools require `XQUIK_API_KEY`, and action tools also require `HERMES_TWEET_ENABLE_ACTIONS=true`. + **The problem**: Claude Code's context compaction summarizes conversation history to stay within token limits. When this happens, your todo list vanishes - you lose track of what you were working on. diff --git a/plugins/hermes-tweet/.claude-plugin/plugin.json b/plugins/hermes-tweet/.claude-plugin/plugin.json new file mode 100644 index 0000000..366fe44 --- /dev/null +++ b/plugins/hermes-tweet/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "hermes-tweet", + "version": "0.1.6", + "description": "Native Hermes Agent X/Twitter plugin for Xquik automation with read-first workflows and approval-gated actions.", + "author": { + "name": "Xquik", + "url": "https://github.com/Xquik-dev" + }, + "license": "MIT", + "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme", + "repository": "https://github.com/Xquik-dev/hermes-tweet", + "keywords": [ + "hermes-agent", + "hermes-plugin", + "xquik", + "twitter", + "x", + "social-media", + "automation" + ] +} diff --git a/plugins/hermes-tweet/.codex-plugin/plugin.json b/plugins/hermes-tweet/.codex-plugin/plugin.json new file mode 100644 index 0000000..3c28322 --- /dev/null +++ b/plugins/hermes-tweet/.codex-plugin/plugin.json @@ -0,0 +1,45 @@ +{ + "name": "hermes-tweet", + "version": "0.1.6", + "description": "Native Hermes Agent X/Twitter plugin for Xquik automation with read-first workflows and approval-gated actions.", + "author": { + "name": "Xquik", + "url": "https://github.com/Xquik-dev" + }, + "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme", + "repository": "https://github.com/Xquik-dev/hermes-tweet", + "license": "MIT", + "keywords": [ + "hermes-agent", + "hermes-plugin", + "xquik", + "twitter", + "x", + "social-media", + "automation", + "codex-plugin" + ], + "skills": "./skills/", + "interface": { + "displayName": "Hermes Tweet", + "shortDescription": "Use Hermes Agent for X/Twitter research and gated actions.", + "longDescription": "Hermes Tweet gives Codex a source-native install surface for the Hermes Agent X/Twitter plugin. Use it to discover tweet and user read tools, summarize public X context, and keep account-changing actions behind explicit Hermes Tweet approval gates.", + "developerName": "Xquik", + "category": "Productivity", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://github.com/Xquik-dev/hermes-tweet#readme", + "privacyPolicyURL": "https://github.com/Xquik-dev/hermes-tweet/security/policy", + "termsOfServiceURL": "https://github.com/Xquik-dev/hermes-tweet/blob/master/LICENSE", + "defaultPrompt": [ + "Use Hermes Tweet to research this X/Twitter topic.", + "Use Hermes Tweet to read public replies and summarize the thread.", + "Use Hermes Tweet to prepare an approval-gated X/Twitter action." + ], + "brandColor": "#111827", + "composerIcon": "./assets/icon.svg" + } +} diff --git a/plugins/hermes-tweet/LICENSE b/plugins/hermes-tweet/LICENSE new file mode 100644 index 0000000..ba5298f --- /dev/null +++ b/plugins/hermes-tweet/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 Xquik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/plugins/hermes-tweet/README.md b/plugins/hermes-tweet/README.md new file mode 100644 index 0000000..bdc27fd --- /dev/null +++ b/plugins/hermes-tweet/README.md @@ -0,0 +1,306 @@ +# Hermes Tweet + +[![CI](https://github.com/Xquik-dev/hermes-tweet/actions/workflows/ci.yml/badge.svg)](https://github.com/Xquik-dev/hermes-tweet/actions/workflows/ci.yml) +[![Docs](https://img.shields.io/badge/docs-README-blue.svg)](https://github.com/Xquik-dev/hermes-tweet#readme) +NHS Agentic Readiness Score +[![Ask DeepWiki](https://deepwiki.com/badge.svg?url=https%3A%2F%2Fgithub.com%2FXquik-dev%2Fhermes-tweet)](https://deepwiki.com/Xquik-dev/hermes-tweet) +[![PyPI](https://img.shields.io/pypi/v/hermes-tweet.svg)](https://pypi.org/project/hermes-tweet/) +[![piwheels](https://img.shields.io/piwheels/v/hermes-tweet.svg)](https://piwheels.org/project/hermes-tweet/) +[![Python](https://img.shields.io/pypi/pyversions/hermes-tweet.svg)](https://pypi.org/project/hermes-tweet/) +[![PyPI Status](https://img.shields.io/pypi/status/hermes-tweet.svg)](https://pypi.org/project/hermes-tweet/) +[![Wheel](https://img.shields.io/pypi/wheel/hermes-tweet.svg)](https://pypi.org/project/hermes-tweet/#files) +[![Downloads](https://img.shields.io/pypi/dm/hermes-tweet.svg)](https://pypi.org/project/hermes-tweet/) +[![Release](https://img.shields.io/github/v/release/Xquik-dev/hermes-tweet?sort=semver)](https://github.com/Xquik-dev/hermes-tweet/releases) +[![Apify Actor](https://apify.com/actor-badge?actor=xquik/x-tweet-scraper)](https://apify.com/xquik/x-tweet-scraper) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +Native [Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin for +X automation through [Xquik](https://xquik.com). + +Hermes Tweet brings X search, account reads, tweet posting, replies, likes, +retweets, follows, DMs, monitors, webhooks, draws, extraction jobs, media, and +trend reads into Hermes as structured tools. + +Use it when you need a Hermes Agent Twitter plugin, Hermes X automation, social +media automation for agents, or a native Hermes toolset for X/Twitter. + +## Highlights + +- Published Python package with a native Hermes plugin entry point. +- Installable from PyPI as `hermes-tweet`. +- 99 agent-callable Xquik endpoints generated from OpenAPI. +- 31 MPP-tagged read endpoints in the bundled catalog. +- Read and action tools are split for least-privilege operation. +- Action endpoints are disabled by default. +- Bundled Hermes skill for agent-facing usage guidance. +- Slash commands for account status and trends. +- Current guidance for Hermes Agent v0.16.0 Desktop, remote gateway, and + dashboard credential workflows. +- Strict CI with formatting, linting, type checking, tests, coverage, security + scan, dependency audit, and package build checks. + +## Install + +Recommended Hermes plugin install: + +```bash +hermes plugins install Xquik-dev/hermes-tweet --enable +``` + +Hermes Agent treats third-party plugins as opt-in. Without `--enable`, the +installer can discover Hermes Tweet, but it may leave the plugin in the +`not enabled` state until you run `hermes plugins enable hermes-tweet` or toggle +it in the interactive `hermes plugins` UI. Use `hermes plugins list` when a +fresh install does not show the `hermes-tweet` toolset. + +Hermes will prompt for `XQUIK_API_KEY` during an interactive install and save it +to `~/.hermes/.env`. In non-interactive installs the prompt is skipped; set the +key through the environment or `~/.hermes/.env` before running `tweet_read`. +If you edit `~/.hermes/.env` while Hermes is already running, use `/reload` in +an interactive CLI session, or restart gateway and cron sessions before calling +`tweet_read`. +When `XQUIK_API_KEY` is not configured, Hermes should expose only the no-network +`tweet_explore` tool from this plugin. That is expected safe gating, not an +install failure. + +Install the published Python package from PyPI into the Hermes Python +environment: + +```bash +uv pip install --python ~/.hermes/hermes-agent/venv/bin/python hermes-tweet +hermes plugins enable hermes-tweet +``` + +If your Hermes venv includes `pip`, this path is also valid: + +```bash +~/.hermes/hermes-agent/venv/bin/python -m pip install hermes-tweet +hermes plugins enable hermes-tweet +``` + +From a local checkout: + +```bash +hermes plugins install file:///absolute/path/to/hermes-tweet --force --enable +``` + +If you are testing from a project-local `.hermes/plugins/` directory instead of +installing the plugin into `~/.hermes/plugins/` or through the PyPI entry point, +start Hermes with `HERMES_ENABLE_PROJECT_PLUGINS=true` only for trusted +repositories. + +Hermes Agent v0.16.0 adds a native desktop app and remote gateway profiles. +For a remote gateway profile, install and enable Hermes Tweet on the remote +Hermes host because that is where plugin code executes and where +`XQUIK_API_KEY` must be available. The desktop app is the chat surface; it +should not receive or store the key unless it is also running the Hermes +runtime locally. + +## Python Package + +| Field | Value | +| --- | --- | +| PyPI | [`hermes-tweet`](https://pypi.org/project/hermes-tweet/) | +| Official guide | [`github.com/Xquik-dev/hermes-tweet#readme`](https://github.com/Xquik-dev/hermes-tweet#readme) | +| Context7 | [`context7.com/xquik-dev/hermes-tweet`](https://context7.com/xquik-dev/hermes-tweet) | +| piwheels | [`hermes-tweet`](https://piwheels.org/project/hermes-tweet/) | +| Latest release | [`v0.1.6`](https://github.com/Xquik-dev/hermes-tweet/releases/tag/v0.1.6) | +| Supported Python | `>=3.11` | +| Package format | Wheel and source distribution | +| Hermes entry point | `hermes-tweet = hermes_tweet` | +| Entry point group | `hermes_agent.plugins` | +| Included assets | `plugin.yaml`, `catalog_data.json`, bundled Hermes skill, Codex/Claude manifests | +| Claude plugin manifest | [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json) | +| Codex plugin manifest | [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json) | +| Registry skill path | [`skills/hermes-tweet/SKILL.md`](skills/hermes-tweet/SKILL.md) | +| Context7 guide | [`docs/CONTEXT7.md`](docs/CONTEXT7.md) | +| Hermes surface guide | [`docs/HERMES_SURFACES.md`](docs/HERMES_SURFACES.md) | +| Integration patterns | [`docs/INTEGRATION_PATTERNS.md`](docs/INTEGRATION_PATTERNS.md) | +| Submission readiness | [`docs/SUBMISSION_READINESS.md`](docs/SUBMISSION_READINESS.md) | +| Merge enablement | [`docs/MERGE_ENABLEMENT.md`](docs/MERGE_ENABLEMENT.md) | + +Hermes Tweet also ships source-native `.codex-plugin` metadata, a root +security policy, a local composer icon, and a HOL Plugin Scanner workflow for +Codex plugin catalogs that require local validation evidence before listing. + +## Configure + +Create an API key in the Xquik dashboard, then set: + +```bash +export XQUIK_API_KEY="xq_..." +``` + +Optional settings: + +```bash +export XQUIK_BASE_URL="https://xquik.com" +export HERMES_TWEET_ENABLE_ACTIONS="false" +``` + +Action endpoints are disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`. +If you configure keys through `~/.hermes/.env` during an active Hermes session, +use `/reload` in the interactive CLI, or restart gateway and cron sessions so +they pick up the new values. + +## Security Model + +Hermes Tweet never accepts credentials through tool arguments. Auth is read from +environment variables and injected by the plugin at request time. + +The plugin blocks dashboard-only admin, billing, credit top-up, support-ticket, +API-key, and account re-authentication endpoints from the catalog. Private reads +and write-like endpoints go through `tweet_action`, which is hidden unless +`HERMES_TWEET_ENABLE_ACTIONS=true`. + +## Tools + +| Tool | Purpose | +| --- | --- | +| `tweet_explore` | Search the bundled Xquik endpoint catalog. No API call. | +| `tweet_read` | Call catalog-listed read-only endpoints. | +| `tweet_action` | Call write-like or private endpoints. Disabled by default. | + +Use `tweet_explore` first, then call `tweet_read` or `tweet_action` with a +concrete `/api/v1/...` path. +Copied endpoint URLs are accepted, but Hermes Tweet matches only catalog-listed +paths. + +## Hermes Agent Workflows + +Hermes Tweet is best used as the X context layer for Hermes Agent workflows that +need current public signal, authenticated account context, or approval-gated +account actions: + +| Workflow | Recommended Path | +| --- | --- | +| Social listening | Use `tweet_explore` to find search, user, trend, monitor, or radar routes, then use `tweet_read` for public reads. | +| Launch monitoring | Keep `tweet_action` disabled, schedule Hermes cron sessions around read-only trend, mention, and account checks. | +| Support triage | Read public mentions and user timelines, summarize issues in Hermes, then hand off account-changing responses for explicit approval. | +| Creator or brand research | Combine X search, user profile, follower, media, and trend reads before drafting content or campaign briefs. | +| Giveaway and community audits | Use read routes for tweet, reply, follower, list, draw, and export evidence before any action route. | +| Controlled publishing | Enable `HERMES_TWEET_ENABLE_ACTIONS=true` only in sessions that require posting, DMs, follows, webhooks, monitors, or media changes. | +| Desktop operator sessions | Use Hermes Desktop for interactive review, then keep action calls explicit and approval-gated. | +| Remote gateway teams | Install Hermes Tweet and set `XQUIK_API_KEY` on the remote gateway host, then connect Desktop profiles to that host. | +| Dashboard-administered agents | Use the dashboard for gateway and credential operations, but keep Hermes Tweet secrets in the runtime environment. | + +For marketing or user education, position Hermes Tweet as a native Hermes Agent +plugin, not a generic API wrapper: it ships a PyPI entry point, a `plugin.yaml` +manifest with interactive secret prompts, slash commands for quick diagnostics, +and a bundled skill registered through Hermes' plugin skill system. + +## Hermes Runtime Fit + +Hermes Tweet registers a dedicated `hermes-tweet` plugin toolset. Hermes can +show and manage those tools through its normal `hermes tools` and platform +toolset flows, so teams can keep X automation available only where it belongs. +Current Hermes Agent releases discover third-party plugins but do not execute +them until they are enabled in `plugins.enabled`, through `hermes plugins enable`, +or by installing with `--enable`. This is expected safety behavior for user and +PyPI entry-point plugins. + +Hermes Agent v0.16.0 expands the surfaces where the same toolset can appear: +the native Desktop app, remote gateway profiles, the web dashboard, the TUI, +and the CLI can all route work to the enabled runtime. Hermes Tweet does not +need a different plugin entry point for those surfaces. It needs the same +enabled plugin, the same runtime environment, and the same read/action split. + +The v0.16.0 Desktop command palette surfaces skills and quick-command slash +commands. Treat `/xstatus` and `/xtrends` as interactive runtime commands for +active CLI, TUI, Desktop, or gateway sessions; keep `hermes -z` for tool-call +smoke tests. + +The v0.16.0 dashboard added broader administration and credential-management +surfaces. Those are useful for gateway operations, but Hermes Tweet still reads +`XQUIK_API_KEY` and `HERMES_TWEET_ENABLE_ACTIONS` from the runtime environment. +Do not paste API keys into prompts, issue bodies, PR comments, or tool inputs. + +For non-interactive smoke tests and CI-style diagnostics, use +`hermes tools list`; bare `hermes tools` opens the interactive tool UI and +requires a TTY. In current Hermes Agent releases, `hermes tools list` reports plugin toolsets, +not every individual plugin tool name. + +Use the read-only path for social listening, trend research, account checks, +giveaway audits, and draft planning. Keep `HERMES_TWEET_ENABLE_ACTIONS=false` +for unattended cron or gateway sessions unless the workflow has an explicit +approval step for posting, DMs, follows, monitor changes, webhook changes, or +other account actions. + +Runtime smoke test: + +```bash +hermes -z "Use tweet_explore, then read /api/v1/account. Do not call tweet_action." --toolsets hermes-tweet +``` + +Expected results: + +- `tweet_explore` discovers catalog endpoints without using the API key. +- Without `XQUIK_API_KEY`, a non-mutating Hermes probe exposes `tweet_explore` + only. +- After `XQUIK_API_KEY` is configured and the CLI is reloaded, or the gateway + or cron process is restarted, `tweet_read` can read `/api/v1/account`. +- `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`. +- `/xstatus` and `/xtrends` appear in the Hermes plugin command registry. + +Hermes one-shot `hermes -z "/xstatus"` can run as a model prompt, not as the +interactive slash-command dispatcher. Verify slash commands in an active CLI, +TUI, Desktop, or gateway session, or through the plugin registry tests, and use +`hermes -z` for tool-call probes. + +If `hermes plugins install` runs without a TTY, Hermes cannot safely prompt for +secrets and will skip API-key storage. This is expected; set `XQUIK_API_KEY` +in the process environment or `~/.hermes/.env`. + +## Slash Commands + +| Command | Purpose | +| --- | --- | +| `/xstatus` | Show Xquik account, subscription, and usage status. | +| `/xtrends` | Show current X trends. | + +## Development + +Generate the bundled catalog from Xquik OpenAPI: + +```bash +python scripts/build_catalog.py ../xquik/openapi.yaml +``` + +Run checks: + +```bash +uv run --python 3.12 --extra dev ruff format --check . +uv run --python 3.12 --extra dev ruff check . +uv run --python 3.12 --extra dev basedpyright +uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100 +uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts +uv run --python 3.12 --extra dev pip-audit +uv run --python 3.12 --extra dev python -m build +uv run --python 3.12 --extra dev twine check dist/* +``` + +For a single local quality gate: + +```bash +uv run --python 3.12 --extra dev ruff format --check . && \ +uv run --python 3.12 --extra dev ruff check . && \ +uv run --python 3.12 --extra dev basedpyright && \ +uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100 && \ +uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts && \ +uv run --python 3.12 --extra dev pip-audit && \ +uv run --python 3.12 --extra dev python -m build && \ +uv run --python 3.12 --extra dev twine check dist/* +``` + +## Public Repo Metadata + +Recommended GitHub description: + +> Native Hermes Agent plugin for X/Twitter automation through Xquik. + +Recommended topics: + +`hermes-agent`, `hermes-plugin`, `hermes`, `twitter`, `x-api`, +`x-automation`, `xquik`, `tweet`, `automation`, `social-media`, +`social-media-automation`, `ai-agent`, `mcp`, `agent-tools`, `twitter-api`, +`twitter-automation`, `x-twitter`, `social-media-api`, `agent-skill`, `python` diff --git a/plugins/hermes-tweet/__init__.py b/plugins/hermes-tweet/__init__.py new file mode 100644 index 0000000..147cb1a --- /dev/null +++ b/plugins/hermes-tweet/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +if __package__: + from .hermes_tweet import register +else: + from hermes_tweet import register + +__all__ = ["register"] diff --git a/plugins/hermes-tweet/after-install.md b/plugins/hermes-tweet/after-install.md new file mode 100644 index 0000000..dacb4ad --- /dev/null +++ b/plugins/hermes-tweet/after-install.md @@ -0,0 +1,58 @@ +# Hermes Tweet Installed + +Hermes Tweet is enabled as the `hermes-tweet` toolset. + +If this plugin was installed without `--enable`, Hermes may show it as +`not enabled` until you run: + +```bash +hermes plugins enable hermes-tweet +hermes plugins list +``` + +Set your Xquik API key before using read tools: + +```bash +export XQUIK_API_KEY="xq_..." +``` + +For persistent Hermes sessions, add it to `~/.hermes/.env`: + +```bash +XQUIK_API_KEY=xq_... +``` + +If Hermes is already running after you edit `~/.hermes/.env`, use `/reload` in +an interactive CLI session, or restart gateway and cron sessions before calling +`tweet_read`. +When `XQUIK_API_KEY` is missing, Hermes should expose only `tweet_explore` from +this plugin. Set the key, then reload the CLI or restart the gateway or cron +process before expecting `tweet_read`. + +Keep actions disabled unless you are intentionally allowing account-changing +operations: + +```bash +export HERMES_TWEET_ENABLE_ACTIONS=false +``` + +Quick smoke test: + +```bash +hermes -z "Use tweet_explore, then read /api/v1/account. Do not call tweet_action." --toolsets hermes-tweet +``` + +Use catalog-listed `/api/v1/...` paths from `tweet_explore`. Copied endpoint +URLs are accepted only when they resolve to catalog-listed paths. + +Expected behavior: + +- `tweet_explore` loads without an API call. +- `tweet_read` works when `XQUIK_API_KEY` is set. +- `/xstatus` and `/xtrends` are registered slash commands. +- `tweet_action` stays hidden or returns a disabled error unless + `HERMES_TWEET_ENABLE_ACTIONS=true`. + +For Hermes v0.12.0, do not use `hermes -z "/xstatus"` as a slash-command smoke +test. One-shot `-z` treats that text as a model prompt. Verify slash commands in +an active CLI or gateway session, or through the plugin registry tests. diff --git a/plugins/hermes-tweet/assets/icon.svg b/plugins/hermes-tweet/assets/icon.svg new file mode 100644 index 0000000..88cdf55 --- /dev/null +++ b/plugins/hermes-tweet/assets/icon.svg @@ -0,0 +1,7 @@ + + Hermes Tweet + Hermes Tweet plugin icon + + + + diff --git a/plugins/hermes-tweet/hermes_tweet/__init__.py b/plugins/hermes-tweet/hermes_tweet/__init__.py new file mode 100644 index 0000000..1aaa2bc --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/__init__.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Any + +from . import schemas +from .tools import ( + action_enabled, + call_action, + call_read, + check_api_available, + explore, + xstatus, + xtrends, +) + +logger = logging.getLogger(__name__) + +TOOLSET = "hermes-tweet" + + +def _register_bundled_skills(ctx: Any) -> None: + skills_dir = Path(__file__).parent / "skills" + for child in sorted(skills_dir.iterdir()): + skill_md = child / "SKILL.md" + if child.is_dir() and skill_md.exists(): + ctx.register_skill(child.name, skill_md) + + +def register(ctx: Any) -> None: + ctx.register_tool( + name="tweet_explore", + toolset=TOOLSET, + schema=schemas.TWEET_EXPLORE, + handler=explore, + is_async=False, + description="Search the bundled Xquik endpoint catalog.", + emoji="🔎", + ) + + ctx.register_tool( + name="tweet_read", + toolset=TOOLSET, + schema=schemas.TWEET_READ, + handler=call_read, + check_fn=check_api_available, + requires_env=["XQUIK_API_KEY"], + is_async=False, + description="Call catalog-listed read-only Xquik endpoints.", + emoji="📖", + ) + + ctx.register_tool( + name="tweet_action", + toolset=TOOLSET, + schema=schemas.TWEET_ACTION, + handler=call_action, + check_fn=action_enabled, + requires_env=["XQUIK_API_KEY", "HERMES_TWEET_ENABLE_ACTIONS"], + is_async=False, + description="Call write-like or private Xquik endpoints.", + emoji="✍️", + ) + + ctx.register_command( + "xstatus", + handler=xstatus, + description="Show Xquik account and usage status", + ) + ctx.register_command("xtrends", handler=xtrends, description="Show current X trends") + + _register_bundled_skills(ctx) + logger.info( + "Hermes Tweet loaded with actions=%s", + os.getenv("HERMES_TWEET_ENABLE_ACTIONS", "false"), + ) + + +__all__ = ["register"] diff --git a/plugins/hermes-tweet/hermes_tweet/catalog.py b/plugins/hermes-tweet/hermes_tweet/catalog.py new file mode 100644 index 0000000..a1efcc5 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/catalog.py @@ -0,0 +1,210 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from importlib.resources import files +from typing import Any, cast +from urllib.parse import urlsplit + +DEFAULT_LIMIT = 25 +MAX_LIMIT = 100 + + +@dataclass(frozen=True) +class Endpoint: + category: str + free: bool + method: str + path: str + summary: str + parameters: tuple[dict[str, Any], ...] = () + response_shape: str | None = None + mpp: dict[str, str] | None = None + action: bool = False + + def to_dict(self) -> dict[str, Any]: + data: dict[str, Any] = { + "category": self.category, + "free": self.free, + "method": self.method, + "path": self.path, + "summary": self.summary, + "action": self.action, + } + if self.parameters: + data["parameters"] = list(self.parameters) + if self.response_shape: + data["responseShape"] = self.response_shape + if self.mpp: + data["mpp"] = self.mpp + return data + + +def _load_raw() -> list[dict[str, Any]]: + text = ( + files(__package__ or "hermes_tweet") + .joinpath("catalog_data.json") + .read_text(encoding="utf-8") + ) + data = cast("object", json.loads(text)) + items = cast("list[object]", data if isinstance(data, list) else []) + return [cast("dict[str, Any]", item) for item in items if isinstance(item, dict)] + + +def _tuple_parameters(value: Any) -> tuple[dict[str, Any], ...]: + items = cast("list[object]", value if isinstance(value, list) else []) + return tuple(cast("dict[str, Any]", item) for item in items if isinstance(item, dict)) + + +def _endpoint(item: dict[str, Any]) -> Endpoint: + return Endpoint( + action=bool(item.get("action")), + category=str(item.get("category", "unknown")), + free=bool(item.get("free", False)), + method=str(item.get("method", "GET")).upper(), + mpp=item.get("mpp") if isinstance(item.get("mpp"), dict) else None, + parameters=_tuple_parameters(item.get("parameters")), + path=str(item.get("path", "")), + response_shape=str(item["responseShape"]) if item.get("responseShape") else None, + summary=str(item.get("summary", "")), + ) + + +ENDPOINTS: tuple[Endpoint, ...] = tuple(_endpoint(item) for item in _load_raw()) + + +def normalize_method(method: Any, *, default: str = "GET") -> str: + if not isinstance(method, str): + return default.upper() + normalized = method.strip().upper() + return normalized or default.upper() + + +def normalize_limit(value: Any) -> int: + if isinstance(value, bool): + return DEFAULT_LIMIT + if isinstance(value, str): + value = value.strip() + if not value.isdecimal(): + return DEFAULT_LIMIT + return normalize_limit(int(value)) + if not isinstance(value, int): + return DEFAULT_LIMIT + return min(max(value, 1), MAX_LIMIT) + + +def _optional_bool(value: Any) -> bool | None: + if isinstance(value, bool): + return value + if isinstance(value, str): + normalized = value.strip().lower() + if normalized == "true": + return True + if normalized == "false": + return False + return None + + +def _optional_text(value: Any) -> str: + if not isinstance(value, str): + return "" + return value.strip() + + +def _optional_method(value: Any) -> str | None: + normalized = _optional_text(value) + if not normalized: + return None + return normalize_method(normalized) + + +def _segments(path: str) -> list[str]: + normalized = path.removesuffix("/") + return normalized.split("/") + + +def normalize_path(path: str) -> str: + return urlsplit(path.strip()).path + + +def matches_path(template: str, concrete: str) -> bool: + normalized_template = normalize_path(template) + normalized_concrete = normalize_path(concrete) + if normalized_template == normalized_concrete: + return True + template_segments = _segments(normalized_template) + concrete_segments = _segments(normalized_concrete) + if len(template_segments) != len(concrete_segments): + return False + for template_segment, concrete_segment in zip( + template_segments, concrete_segments, strict=True + ): + if template_segment.startswith("{") and template_segment.endswith("}"): + if not concrete_segment: + return False + continue + if template_segment.startswith(":"): + if not concrete_segment: + return False + continue + if template_segment != concrete_segment: + return False + return True + + +def find_endpoint(method: str, path: str) -> Endpoint | None: + normalized = normalize_method(method) + normalized_path = normalize_path(path) + for endpoint in ENDPOINTS: + if endpoint.method == normalized and matches_path(endpoint.path, normalized_path): + return endpoint + return None + + +def _matches_query(endpoint: Endpoint, query: str) -> bool: + normalized = query.lower() + haystack = " ".join( + [ + endpoint.category, + endpoint.method, + endpoint.path, + endpoint.summary, + endpoint.response_shape or "", + json.dumps(list(endpoint.parameters), ensure_ascii=False), + ] + ).lower() + return normalized in haystack + + +def explore(args: dict[str, Any]) -> list[dict[str, Any]]: + method = _optional_method(args.get("method")) + category = _optional_text(args.get("category")).lower() + path = _optional_text(args.get("path")) + query = _optional_text(args.get("query")) + limit = normalize_limit(args.get("limit")) + include_actions = _optional_bool(args.get("include_actions")) is True + + endpoints = ENDPOINTS + if method: + endpoints = tuple(endpoint for endpoint in endpoints if endpoint.method == method) + if category: + endpoints = tuple( + endpoint for endpoint in endpoints if endpoint.category.lower() == category + ) + if path: + endpoints = tuple( + endpoint + for endpoint in endpoints + if path in endpoint.path or matches_path(endpoint.path, path) + ) + if query: + endpoints = tuple(endpoint for endpoint in endpoints if _matches_query(endpoint, query)) + free = _optional_bool(args.get("free")) + if free is not None: + endpoints = tuple(endpoint for endpoint in endpoints if endpoint.free is free) + mpp = _optional_bool(args.get("mpp")) + if mpp is not None: + endpoints = tuple(endpoint for endpoint in endpoints if (endpoint.mpp is not None) is mpp) + if not include_actions: + endpoints = tuple(endpoint for endpoint in endpoints if not endpoint.action) + return [endpoint.to_dict() for endpoint in endpoints[:limit]] diff --git a/plugins/hermes-tweet/hermes_tweet/catalog_data.json b/plugins/hermes-tweet/hermes_tweet/catalog_data.json new file mode 100644 index 0000000..1bdff5a --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/catalog_data.json @@ -0,0 +1,3849 @@ +[ + { + "action": false, + "category": "account", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/account", + "responseShape": "Account info", + "summary": "Get account info" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Workflow step, topic, goal, and optional style parameters." + } + ], + "path": "/api/v1/compose", + "responseShape": "Composition result", + "summary": "Compose, refine, or score a tweet" + }, + { + "action": false, + "category": "subscribe", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/credits", + "responseShape": "Credits balance and usage", + "summary": "Get credits balance" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits." + }, + { + "name": "afterCursor", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for pagination" + } + ], + "path": "/api/v1/drafts", + "responseShape": "Draft list", + "summary": "List saved drafts" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Draft text with optional topic and optimization goal." + } + ], + "path": "/api/v1/drafts", + "responseShape": "Draft created", + "summary": "Save a tweet draft" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/drafts/{id}", + "responseShape": "", + "summary": "Delete a draft" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/drafts/{id}", + "responseShape": "Draft details", + "summary": "Get draft by ID" + }, + { + "action": false, + "category": "draws", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for keyset pagination from prior response next_cursor" + } + ], + "path": "/api/v1/draws", + "responseShape": "Draw list", + "summary": "List draws" + }, + { + "action": true, + "category": "draws", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Tweet URL, winner count, and optional eligibility filters (retweet, follow, keywords, hashtags, account age)." + } + ], + "path": "/api/v1/draws", + "responseShape": "Draw completed", + "summary": "Run giveaway draw" + }, + { + "action": false, + "category": "draws", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Draw public ID returned by create and list draw responses." + } + ], + "path": "/api/v1/draws/{id}", + "responseShape": "Draw with winners", + "summary": "Get draw details" + }, + { + "action": false, + "category": "draws", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Draw public ID returned by create and list draw responses." + }, + { + "name": "format", + "in": "query", + "required": true, + "type": "string", + "description": "Export output format" + }, + { + "name": "type", + "in": "query", + "required": false, + "type": "string", + "description": "Export winners or all entries" + } + ], + "path": "/api/v1/draws/{id}/export", + "responseShape": "Exported draw file", + "summary": "Export draw data" + }, + { + "action": true, + "category": "events", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for keyset pagination from prior response next_cursor" + }, + { + "name": "monitorId", + "in": "query", + "required": false, + "type": "string", + "description": "Filter events by monitor ID" + }, + { + "name": "eventType", + "in": "query", + "required": false, + "type": "unknown", + "description": "Filter events by type" + } + ], + "path": "/api/v1/events", + "responseShape": "Event list", + "summary": "List events" + }, + { + "action": true, + "category": "events", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/events/{id}", + "responseShape": "Event details", + "summary": "Get event" + }, + { + "action": false, + "category": "extractions", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Maximum number of items to return (1-100, default 50). For paid per-result endpoints, the returned count may be lower when remaining credits cannot cover the requested page. If zero paid results are affordable, the endpoint returns 402 insufficient_credits." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for keyset pagination from prior response next_cursor" + }, + { + "name": "toolType", + "in": "query", + "required": false, + "type": "unknown", + "description": "Filter by extraction tool type" + }, + { + "name": "status", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by job status" + } + ], + "path": "/api/v1/extractions", + "responseShape": "Extraction job list", + "summary": "List extraction jobs" + }, + { + "action": true, + "category": "extractions", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Tool type and target identifier (tweet, user, community, list, or search query)." + } + ], + "path": "/api/v1/extractions", + "responseShape": "Extraction started", + "summary": "Run extraction" + }, + { + "action": true, + "category": "extractions", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Same parameters as a real extraction; returns estimated credit cost without running the job." + } + ], + "path": "/api/v1/extractions/estimate", + "responseShape": "Extraction estimate", + "summary": "Estimate extraction cost" + }, + { + "action": false, + "category": "extractions", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Extraction public ID (UUID)" + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Maximum number of results to return (1-1000, default 100)" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for keyset pagination from prior response next_cursor" + } + ], + "path": "/api/v1/extractions/{id}", + "responseShape": "Extraction job with results", + "summary": "Get extraction results" + }, + { + "action": false, + "category": "extractions", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Extraction public ID" + }, + { + "name": "format", + "in": "query", + "required": true, + "type": "string", + "description": "Export file format" + } + ], + "path": "/api/v1/extractions/{id}/export", + "responseShape": "Exported file", + "summary": "Export extraction results" + }, + { + "action": false, + "category": "monitors", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/monitors", + "responseShape": "Monitor list", + "summary": "List monitors" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Target X username and event types to monitor." + } + ], + "path": "/api/v1/monitors", + "responseShape": "Monitor created", + "summary": "Create monitor" + }, + { + "action": false, + "category": "monitors", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/monitors/keywords", + "responseShape": "Keyword monitor list", + "summary": "List keyword monitors" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Search query and event types to monitor." + } + ], + "path": "/api/v1/monitors/keywords", + "responseShape": "Keyword monitor created", + "summary": "Create keyword monitor" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/monitors/keywords/{id}", + "responseShape": "", + "summary": "Delete keyword monitor" + }, + { + "action": false, + "category": "monitors", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/monitors/keywords/{id}", + "responseShape": "Keyword monitor details", + "summary": "Get keyword monitor" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Updated event types or active status." + } + ], + "path": "/api/v1/monitors/keywords/{id}", + "responseShape": "Keyword monitor updated", + "summary": "Update keyword monitor" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/monitors/{id}", + "responseShape": "", + "summary": "Delete monitor" + }, + { + "action": false, + "category": "monitors", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/monitors/{id}", + "responseShape": "Monitor details", + "summary": "Get monitor" + }, + { + "action": true, + "category": "monitors", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Event type list or active state for the account monitor." + } + ], + "path": "/api/v1/monitors/{id}", + "responseShape": "Monitor updated", + "summary": "Update monitor" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "after", + "in": "query", + "required": false, + "type": "string", + "description": "Cursor for pagination (from prior response nextCursor)." + }, + { + "name": "category", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by category." + }, + { + "name": "hours", + "in": "query", + "required": false, + "type": "integer", + "description": "Lookback window in hours (1-72, default 6)." + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Number of items to return (1-100, default 50)." + }, + { + "name": "region", + "in": "query", + "required": false, + "type": "string", + "description": "Region filter. Use `global` or a region code such as `US`, `GB`, `TR`, or `ES`." + }, + { + "name": "source", + "in": "query", + "required": false, + "type": "string", + "description": "Source filter. One of: github, google_trends, hacker_news, polymarket, reddit, trustmrr, wikipedia" + } + ], + "path": "/api/v1/radar", + "responseShape": "Radar items", + "summary": "Get trending topics from curated sources" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/styles", + "responseShape": "Style profile list", + "summary": "List cached style profiles" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "X username whose recent tweets define the style." + } + ], + "path": "/api/v1/styles", + "responseShape": "Fresh cached style profile returned", + "summary": "Analyze writing style from recent tweets" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "username1", + "in": "query", + "required": true, + "type": "string", + "description": "First username to compare" + }, + { + "name": "username2", + "in": "query", + "required": true, + "type": "string", + "description": "Second username to compare" + } + ], + "path": "/api/v1/styles/compare", + "responseShape": "Style comparison", + "summary": "Compare two style profiles" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Style profile ID or X username" + } + ], + "path": "/api/v1/styles/{id}", + "responseShape": "", + "summary": "Delete a style profile" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Style profile ID or X username" + } + ], + "path": "/api/v1/styles/{id}", + "responseShape": "Style profile", + "summary": "Get cached style profile" + }, + { + "action": true, + "category": "composition", + "free": true, + "method": "PUT", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Style profile ID or X username" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Label and sample tweets that define the style profile." + } + ], + "path": "/api/v1/styles/{id}", + "responseShape": "Style profile saved", + "summary": "Save style profile with custom tweets" + }, + { + "action": false, + "category": "composition", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Style profile ID or X username" + } + ], + "path": "/api/v1/styles/{id}/performance", + "responseShape": "Performance metrics", + "summary": "Get engagement metrics for style tweets" + }, + { + "action": false, + "category": "trends", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00045/call" + }, + "parameters": [ + { + "name": "woeid", + "in": "query", + "required": false, + "type": "integer", + "description": "Region Yahoo WOEID code (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey)" + }, + { + "name": "count", + "in": "query", + "required": false, + "type": "integer", + "description": "Number of trending topics returned (1-50, default 30)" + } + ], + "path": "/api/v1/trends", + "responseShape": "Trending topics", + "summary": "Get trending hashtags and topics by region (alias)" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/webhooks", + "responseShape": "Webhook list", + "summary": "List webhooks" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "HTTPS callback URL and event types to subscribe to." + } + ], + "path": "/api/v1/webhooks", + "responseShape": "Webhook created", + "summary": "Create webhook" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/webhooks/{id}", + "responseShape": "", + "summary": "Deactivate webhook" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Updated URL, event types, or active status." + } + ], + "path": "/api/v1/webhooks/{id}", + "responseShape": "Webhook updated", + "summary": "Update webhook" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/webhooks/{id}/deliveries", + "responseShape": "Delivery list", + "summary": "List webhook deliveries" + }, + { + "action": true, + "category": "webhooks", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + } + ], + "path": "/api/v1/webhooks/{id}/test", + "responseShape": "Test result", + "summary": "Test webhook endpoint" + }, + { + "action": true, + "category": "x-accounts", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/x/accounts", + "responseShape": "X account list", + "summary": "List connected X accounts" + }, + { + "action": false, + "category": "articles", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00105/call" + }, + "parameters": [ + { + "name": "tweetId", + "in": "path", + "required": true, + "type": "string", + "description": "Numeric tweet ID of the article, 15-20 digits. If you have a tweet URL, use the final status ID." + } + ], + "path": "/api/v1/x/articles/{tweetId}", + "responseShape": "Article with author", + "summary": "Get full X Article content with cover image and metadata" + }, + { + "action": true, + "category": "tweets", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "folderId", + "in": "query", + "required": false, + "type": "string", + "description": "Optional bookmark folder ID" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for bookmarks" + } + ], + "path": "/api/v1/x/bookmarks", + "responseShape": "List of bookmarked tweets", + "summary": "Get bookmarked tweets" + }, + { + "action": true, + "category": "tweets", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [], + "path": "/api/v1/x/bookmarks/folders", + "responseShape": "List of bookmark folders", + "summary": "Get bookmark folders" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account, community name, and optional description." + } + ], + "path": "/api/v1/x/communities", + "responseShape": "Community created", + "summary": "Create community" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "type": "string", + "description": "Search query" + }, + { + "name": "queryType", + "in": "query", + "required": false, + "type": "string", + "description": "Sort order (Latest or Top)" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for community search" + } + ], + "path": "/api/v1/x/communities/search", + "responseShape": "Community search results", + "summary": "Search for communities by keyword" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "type": "string", + "description": "Search query for cross-community tweets" + }, + { + "name": "queryType", + "in": "query", + "required": false, + "type": "string", + "description": "Sort order for cross-community results (Latest or Top)" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for cross-community results" + } + ], + "path": "/api/v1/x/communities/tweets", + "responseShape": "Paginated list of tweets from all communities", + "summary": "List tweets across all communities" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account and community name for deletion confirmation." + } + ], + "path": "/api/v1/x/communities/{id}", + "responseShape": "", + "summary": "Delete community" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Community ID" + } + ], + "path": "/api/v1/x/communities/{id}/info", + "responseShape": "Community details", + "summary": "Get community name, description and member count" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account to leave the community." + } + ], + "path": "/api/v1/x/communities/{id}/join", + "responseShape": "", + "summary": "Leave community" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Resource ID returned by the matching create or list endpoint." + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account to join the community." + } + ], + "path": "/api/v1/x/communities/{id}/join", + "responseShape": "", + "summary": "Join community" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Community ID for member lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "description": "Items per page (20-200, default 20). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits." + } + ], + "path": "/api/v1/x/communities/{id}/members", + "responseShape": "List of community members", + "summary": "List members of a community" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Community ID for moderator lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for community moderators" + } + ], + "path": "/api/v1/x/communities/{id}/moderators", + "responseShape": "List of community moderators", + "summary": "List moderators of a community" + }, + { + "action": false, + "category": "communities", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Community ID for tweet lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for community tweets" + } + ], + "path": "/api/v1/x/communities/{id}/tweets", + "responseShape": "List of community tweets", + "summary": "List tweets posted in a community" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "type": "string", + "description": "Recipient user ID" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account, message text, and an optional media attachment." + } + ], + "path": "/api/v1/x/dm/{userId}", + "responseShape": "DM sent", + "summary": "Send direct message" + }, + { + "action": true, + "category": "users", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "type": "string", + "description": "Target user ID" + }, + { + "name": "account", + "in": "query", + "required": true, + "type": "string", + "description": "X handle (without the `@` prefix) of the connected X account used to read the conversation. The account must be a participant in the conversation." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for DM history" + }, + { + "name": "maxId", + "in": "query", + "required": false, + "type": "string", + "description": "Legacy pagination cursor (backward compat)" + } + ], + "path": "/api/v1/x/dm/{userId}/history", + "responseShape": "List of DM messages", + "summary": "Get DM conversation history" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00105/call" + }, + "parameters": [ + { + "name": "source", + "in": "query", + "required": true, + "type": "string", + "description": "Username to check (without @)" + }, + { + "name": "target", + "in": "query", + "required": true, + "type": "string", + "description": "Target username (without @)" + } + ], + "path": "/api/v1/x/followers/check", + "responseShape": "Follow check result", + "summary": "Check if one user follows another" + }, + { + "action": false, + "category": "lists", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "List ID" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for list followers" + } + ], + "path": "/api/v1/x/lists/{id}/followers", + "responseShape": "List of followers", + "summary": "List followers of an X List" + }, + { + "action": false, + "category": "lists", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "List ID for member lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for list members" + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "description": "Members per page (20-200, default 20)" + } + ], + "path": "/api/v1/x/lists/{id}/members", + "responseShape": "List of members", + "summary": "List members of an X List" + }, + { + "action": false, + "category": "lists", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "List ID for tweet lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for list tweets" + }, + { + "name": "sinceTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - filter after" + }, + { + "name": "untilTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - filter before" + }, + { + "name": "includeReplies", + "in": "query", + "required": false, + "type": "boolean", + "description": "Include replies (default false)" + } + ], + "path": "/api/v1/x/lists/{id}/tweets", + "responseShape": "List tweets", + "summary": "List tweets from an X List" + }, + { + "action": true, + "category": "media", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Upload media with multipart form data, or provide a JSON URL for server-side download." + } + ], + "path": "/api/v1/x/media", + "responseShape": "Media uploaded", + "summary": "Upload media" + }, + { + "action": true, + "category": "media", + "free": false, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Single tweet URL/ID, accepted aliases, or array of up to 50 tweet URLs/IDs for bulk download. When `tweetIds` contains at least one string value, bulk mode is used." + } + ], + "path": "/api/v1/x/media/download", + "responseShape": "Media download result. Single: tweetId + galleryUrl + cacheHit. Bulk: galleryUrl + totalTweets + totalMedia.", + "summary": "Download images and videos from tweets" + }, + { + "action": true, + "category": "users", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "type": "string", + "description": "Notification type filter. Unrecognized values fall back to All." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for notifications" + } + ], + "path": "/api/v1/x/notifications", + "responseShape": "List of notifications", + "summary": "Get notifications" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account and profile fields to update (name, bio, location, website)." + } + ], + "path": "/api/v1/x/profile", + "responseShape": "", + "summary": "Update X profile" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account and avatar image file or HTTPS image URL (max 700 KB)." + } + ], + "path": "/api/v1/x/profile/avatar", + "responseShape": "", + "summary": "Update profile avatar" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "PATCH", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account and banner image file or HTTPS image URL (max 2 MB)." + } + ], + "path": "/api/v1/x/profile/banner", + "responseShape": "", + "summary": "Update profile banner" + }, + { + "action": true, + "category": "tweets", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "seenTweetIds", + "in": "query", + "required": false, + "type": "string", + "description": "Comma-separated tweet IDs to exclude from results. Empty entries are ignored." + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for timeline" + } + ], + "path": "/api/v1/x/timeline", + "responseShape": "List of timeline tweets", + "summary": "Get home timeline" + }, + { + "action": false, + "category": "trends", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00045/call" + }, + "parameters": [ + { + "name": "woeid", + "in": "query", + "required": false, + "type": "integer", + "description": "Region WOEID (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey)" + }, + { + "name": "count", + "in": "query", + "required": false, + "type": "integer", + "description": "Number of trending topics to return (1-50, default 30)" + } + ], + "path": "/api/v1/x/trends", + "responseShape": "List of trending topics", + "summary": "Get trending hashtags and topics from X by region" + }, + { + "action": false, + "category": "tweets", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "ids", + "in": "query", + "required": true, + "type": "string", + "description": "Comma-separated tweet IDs (max 100)" + } + ], + "path": "/api/v1/x/tweets", + "responseShape": "List of tweets", + "summary": "Get multiple tweets by IDs" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account and tweet content. Requires text, media, or both." + } + ], + "path": "/api/v1/x/tweets", + "responseShape": "Tweet created", + "summary": "Create tweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "type": "string", + "description": "Search query (keywords," + }, + { + "name": "queryType", + "in": "query", + "required": false, + "type": "string", + "description": "Sort order - Latest (chronological) or Top (engagement-ranked)" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor from previous response" + }, + { + "name": "sinceTime", + "in": "query", + "required": false, + "type": "string", + "description": "ISO 8601 timestamp - only return tweets after this time" + }, + { + "name": "untilTime", + "in": "query", + "required": false, + "type": "string", + "description": "ISO 8601 timestamp - only return tweets before this time" + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Max tweets to return (server paginates internally). Omit for single page (~20). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits." + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + }, + { + "name": "listId", + "in": "query", + "required": false, + "type": "string", + "description": "Search within a list ID." + }, + { + "name": "place", + "in": "query", + "required": false, + "type": "string", + "description": "Search within a place ID." + }, + { + "name": "placeCountry", + "in": "query", + "required": false, + "type": "string", + "description": "Search within a country code." + }, + { + "name": "pointRadius", + "in": "query", + "required": false, + "type": "string", + "description": "Geo point radius, e.g. -73.99 40.73 25mi." + }, + { + "name": "boundingBox", + "in": "query", + "required": false, + "type": "string", + "description": "Geo bounding box, e.g. -74.1 40.6 -73.9 40.8." + }, + { + "name": "advancedQuery", + "in": "query", + "required": false, + "type": "string", + "description": "Raw advanced search query appended as-is." + } + ], + "path": "/api/v1/x/tweets/search", + "responseShape": "Search results", + "summary": "Search tweets by query, Tweet ID, X status URL, or account date window" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to delete" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account that owns the tweet." + } + ], + "path": "/api/v1/x/tweets/{id}", + "responseShape": "", + "summary": "Delete tweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID" + } + ], + "path": "/api/v1/x/tweets/{id}", + "responseShape": "Tweet with author", + "summary": "Get tweet with full text, author, metrics and media" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to get favoriters" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for favoriters" + } + ], + "path": "/api/v1/x/tweets/{id}/favoriters", + "responseShape": "List of users who liked the tweet", + "summary": "List users who liked a tweet" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to unlike" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account that liked the tweet." + } + ], + "path": "/api/v1/x/tweets/{id}/like", + "responseShape": "", + "summary": "Unlike tweet" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to like" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account to perform the like." + } + ], + "path": "/api/v1/x/tweets/{id}/like", + "responseShape": "", + "summary": "Like tweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to get quotes" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for quote tweets" + }, + { + "name": "sinceTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return quotes posted after this time" + }, + { + "name": "untilTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return quotes posted before this time" + }, + { + "name": "includeReplies", + "in": "query", + "required": false, + "type": "boolean", + "description": "Include reply quotes (default false)" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/tweets/{id}/quotes", + "responseShape": "List of quote tweets", + "summary": "List quote tweets of a tweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to get replies" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for tweet replies" + }, + { + "name": "sinceTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return replies posted after this time" + }, + { + "name": "untilTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return replies posted before this time" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/tweets/{id}/replies", + "responseShape": "List of replies", + "summary": "List replies to a tweet" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to unretweet" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account that retweeted the tweet." + } + ], + "path": "/api/v1/x/tweets/{id}/retweet", + "responseShape": "", + "summary": "Unretweet" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to retweet" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account to perform the retweet." + } + ], + "path": "/api/v1/x/tweets/{id}/retweet", + "responseShape": "", + "summary": "Retweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to get retweeters" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for retweeters" + } + ], + "path": "/api/v1/x/tweets/{id}/retweeters", + "responseShape": "List of retweeters", + "summary": "List users who retweeted a tweet" + }, + { + "action": false, + "category": "tweets", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Tweet ID to get thread context" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for thread tweets" + } + ], + "path": "/api/v1/x/tweets/{id}/thread", + "responseShape": "Thread tweets", + "summary": "Get full conversation thread for a tweet" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "ids", + "in": "query", + "required": true, + "type": "string", + "description": "Comma-separated user IDs (max 100)" + } + ], + "path": "/api/v1/x/users/batch", + "responseShape": "List of users", + "summary": "Look up multiple users by IDs in one call" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "type": "string", + "description": "User search query" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for user search" + } + ], + "path": "/api/v1/x/users/search", + "responseShape": "User search results", + "summary": "Search users by name or username" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "charge", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "X username (without @) or user ID" + } + ], + "path": "/api/v1/x/users/{id}", + "responseShape": "User profile", + "summary": "Get user profile with follower counts and verification" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "DELETE", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID to unfollow" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account that follows the target user." + } + ], + "path": "/api/v1/x/users/{id}/follow", + "responseShape": "", + "summary": "Unfollow user" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID to follow" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account to perform the follow." + } + ], + "path": "/api/v1/x/users/{id}/follow", + "responseShape": "", + "summary": "Follow user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID or username" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for followers list" + }, + { + "name": "after", + "in": "query", + "required": false, + "type": "string", + "description": "Legacy cursor alias for following lists. Prefer cursor." + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "description": "Items per page (20-200, default 200). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits." + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Legacy integer page size alias for following lists. Prefer pageSize." + } + ], + "path": "/api/v1/x/users/{id}/followers", + "responseShape": "List of user followers", + "summary": "List followers of a user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID for followers-you-know lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for followers-you-know" + } + ], + "path": "/api/v1/x/users/{id}/followers-you-know", + "responseShape": "List of mutual followers", + "summary": "List mutual followers between you and a user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID or username for following lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for following list" + }, + { + "name": "after", + "in": "query", + "required": false, + "type": "string", + "description": "Legacy cursor alias. Prefer cursor." + }, + { + "name": "pageSize", + "in": "query", + "required": false, + "type": "integer", + "description": "Results per page (20-200, default 200). This is an upper bound for paid authenticated calls: remaining credits can reduce the returned page size, and zero affordable results returns 402 insufficient_credits." + }, + { + "name": "limit", + "in": "query", + "required": false, + "type": "integer", + "description": "Legacy page size alias. Prefer pageSize." + } + ], + "path": "/api/v1/x/users/{id}/following", + "responseShape": "List of following", + "summary": "List accounts a user follows" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for liked tweets" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/users/{id}/likes", + "responseShape": "List of liked tweets", + "summary": "List tweets liked by a user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID for media lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for media tweets" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/users/{id}/media", + "responseShape": "List of media tweets", + "summary": "List media tweets posted by a user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID or username for mentions lookup" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for mentions" + }, + { + "name": "sinceTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return mentions after this time" + }, + { + "name": "untilTime", + "in": "query", + "required": false, + "type": "string", + "description": "Unix timestamp - return mentions before this time" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/users/{id}/mentions", + "responseShape": "List of mentions", + "summary": "List tweets mentioning a user" + }, + { + "action": true, + "category": "x-write", + "free": true, + "method": "POST", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID to remove from your followers" + }, + { + "name": "body", + "in": "body", + "required": true, + "type": "object", + "description": "Account whose follower list should be updated." + } + ], + "path": "/api/v1/x/users/{id}/remove-follower", + "responseShape": "", + "summary": "Remove follower" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "X user ID or username" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for user tweets" + }, + { + "name": "includeReplies", + "in": "query", + "required": false, + "type": "boolean", + "description": "Include reply tweets" + }, + { + "name": "includeParentTweet", + "in": "query", + "required": false, + "type": "boolean", + "description": "Include parent tweet for replies" + }, + { + "name": "fromUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by author username." + }, + { + "name": "toUser", + "in": "query", + "required": false, + "type": "string", + "description": "Filter replies sent to a username." + }, + { + "name": "mentioning", + "in": "query", + "required": false, + "type": "string", + "description": "Filter tweets mentioning a username." + }, + { + "name": "language", + "in": "query", + "required": false, + "type": "string", + "description": "Language code filter, e.g. en or tr." + }, + { + "name": "sinceDate", + "in": "query", + "required": false, + "type": "string", + "description": "Start date in YYYY-MM-DD format." + }, + { + "name": "untilDate", + "in": "query", + "required": false, + "type": "string", + "description": "End date in YYYY-MM-DD format." + }, + { + "name": "mediaType", + "in": "query", + "required": false, + "type": "string", + "description": "Filter by media type." + }, + { + "name": "minFaves", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum likes threshold." + }, + { + "name": "minRetweets", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum retweets threshold." + }, + { + "name": "minReplies", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum replies threshold." + }, + { + "name": "minQuotes", + "in": "query", + "required": false, + "type": "integer", + "description": "Minimum quote count threshold." + }, + { + "name": "verifiedOnly", + "in": "query", + "required": false, + "type": "boolean", + "description": "Only return tweets from verified authors." + }, + { + "name": "replies", + "in": "query", + "required": false, + "type": "string", + "description": "Reply mode." + }, + { + "name": "retweets", + "in": "query", + "required": false, + "type": "string", + "description": "Retweet mode." + }, + { + "name": "quotes", + "in": "query", + "required": false, + "type": "string", + "description": "Quote mode." + }, + { + "name": "exactPhrase", + "in": "query", + "required": false, + "type": "string", + "description": "Exact phrase to match." + }, + { + "name": "excludeWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases to exclude. Separate with spaces, commas, or lines." + }, + { + "name": "anyWords", + "in": "query", + "required": false, + "type": "string", + "description": "Words or quoted phrases where any one can match. Separate with spaces, commas, or lines." + }, + { + "name": "hashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Hashtags separated by spaces, commas, or lines." + }, + { + "name": "cashtags", + "in": "query", + "required": false, + "type": "string", + "description": "Cashtags separated by spaces, commas, or lines." + }, + { + "name": "url", + "in": "query", + "required": false, + "type": "string", + "description": "URL substring or domain filter." + }, + { + "name": "conversationId", + "in": "query", + "required": false, + "type": "string", + "description": "Conversation ID filter." + }, + { + "name": "inReplyToTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only replies to this tweet ID." + }, + { + "name": "quotesOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only quotes of this tweet ID." + }, + { + "name": "retweetsOfTweetId", + "in": "query", + "required": false, + "type": "string", + "description": "Only retweets of this tweet ID." + } + ], + "path": "/api/v1/x/users/{id}/tweets", + "responseShape": "User tweets list", + "summary": "List recent tweets posted by a user" + }, + { + "action": false, + "category": "users", + "free": false, + "method": "GET", + "mpp": { + "intent": "session", + "price": "$0.00015/call" + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "User ID or username for verified followers" + }, + { + "name": "cursor", + "in": "query", + "required": false, + "type": "string", + "description": "Pagination cursor for verified followers" + } + ], + "path": "/api/v1/x/users/{id}/verified-followers", + "responseShape": "List of verified followers", + "summary": "List verified followers of a user" + }, + { + "action": false, + "category": "x-write", + "free": true, + "method": "GET", + "mpp": null, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string", + "description": "Write action ID returned by a pending write response." + } + ], + "path": "/api/v1/x/write-actions/{id}", + "responseShape": "Write action status", + "summary": "Get write action status" + } +] diff --git a/plugins/hermes-tweet/hermes_tweet/client.py b/plugins/hermes-tweet/hermes_tweet/client.py new file mode 100644 index 0000000..cc89e59 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/client.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import json +import os +from math import isfinite +from typing import Any, cast +from urllib.parse import urljoin + +import httpx + +API_V1_PREFIX = "/api/v1/" +DEFAULT_BASE_URL = "https://xquik.com" +TIMEOUT_SECONDS = 30.0 + + +def _env_text(name: str, default: str = "") -> str: + value = os.getenv(name) + if value is None: + return default + normalized = value.strip() + return normalized or default + + +def _request_text(value: Any) -> str: + if not isinstance(value, str): + return "" + return value.strip() + + +def normalize_query_params(value: Any) -> dict[str, str] | None: + if not isinstance(value, dict): + return None + output: dict[str, str] = {} + for key, item in cast("dict[object, object]", value).items(): + if not isinstance(key, str): + continue + normalized_key = key.strip() + if not normalized_key: + continue + if isinstance(item, bool): + output[normalized_key] = str(item).lower() + elif isinstance(item, (str, int)) or (isinstance(item, float) and isfinite(item)): + output[normalized_key] = str(item) + return output or None + + +def base_url() -> str: + return _env_text("XQUIK_BASE_URL", DEFAULT_BASE_URL).rstrip("/") + "/" + + +def api_key() -> str: + return _env_text("XQUIK_API_KEY") + + +def check_api_available() -> bool: + return bool(api_key()) + + +def action_enabled() -> bool: + return check_api_available() and _env_text("HERMES_TWEET_ENABLE_ACTIONS").lower() == "true" + + +def build_headers(key: str, *, has_body: bool) -> dict[str, str]: + headers: dict[str, str] = {} + if key.startswith("xq_"): + headers["x-api-key"] = key + elif key: + headers["authorization"] = f"Bearer {key}" + if has_body: + headers["content-type"] = "application/json" + return headers + + +def request( + method: Any, + path: Any, + query: Any = None, + body: Any | None = None, +) -> Any: + normalized_method = _request_text(method).upper() or "GET" + normalized_path = _request_text(path) + params = normalize_query_params(query) + if not normalized_path.startswith(API_V1_PREFIX): + return {"success": False, "error": f"Path must start with {API_V1_PREFIX}"} + if "?" in normalized_path or "#" in normalized_path: + return { + "success": False, + "error": "Pass query parameters through the query object, not in the path.", + } + + key = api_key() + if not key: + return {"success": False, "error": "XQUIK_API_KEY is not configured."} + + url = urljoin(base_url(), normalized_path.lstrip("/")) + try: + with httpx.Client(timeout=TIMEOUT_SECONDS) as client: + response = client.request( + method=normalized_method, + url=url, + params=params, + json=body, + headers=build_headers(key, has_body=body is not None), + ) + try: + payload = response.json() + except ValueError: + payload = {"text": response.text} + if not response.is_success: + return { + "success": False, + "error": "API request failed.", + "status_code": response.status_code, + "response": payload, + } + return payload + except httpx.HTTPError as exc: + return {"success": False, "error": str(exc)} + + +def dumps(data: Any) -> str: + return json.dumps(data, ensure_ascii=False, separators=(",", ":")) diff --git a/plugins/hermes-tweet/hermes_tweet/plugin.yaml b/plugins/hermes-tweet/hermes_tweet/plugin.yaml new file mode 100644 index 0000000..8a0af54 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/plugin.yaml @@ -0,0 +1,12 @@ +name: hermes-tweet +version: 0.1.6 +description: "Native Hermes Agent plugin for X/Twitter automation through Xquik" +author: Xquik +optional_env: + - XQUIK_API_KEY + - XQUIK_BASE_URL + - HERMES_TWEET_ENABLE_ACTIONS +provides_tools: + - tweet_explore + - tweet_read + - tweet_action diff --git a/plugins/hermes-tweet/hermes_tweet/schemas.py b/plugins/hermes-tweet/hermes_tweet/schemas.py new file mode 100644 index 0000000..c746cc3 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/schemas.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +_METHOD_ENUM = ["GET", "POST", "PATCH", "PUT", "DELETE"] +_API_PATH_PATTERN = r"^(?:/api/v1/|https?://[^/]+/api/v1/)" +_API_PATH_DESCRIPTION = ( + "Concrete /api/v1/... endpoint path or copied API URL whose path starts with /api/v1/." +) + +TWEET_EXPLORE = { + "name": "tweet_explore", + "description": ( + "Search the bundled Xquik endpoint catalog. Use this before calling " + "tweet_read or tweet_action. This tool does not make network calls." + ), + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "pattern": "\\S", + "description": ( + "Keyword search across endpoint paths, summaries, parameters, " + "and response shapes." + ), + }, + "category": { + "type": "string", + "minLength": 1, + "pattern": "\\S", + "description": "Endpoint category filter.", + }, + "method": { + "type": "string", + "enum": _METHOD_ENUM, + "description": "HTTP method filter.", + }, + "path": { + "type": "string", + "minLength": 1, + "pattern": "\\S", + "description": "Exact or partial /api/v1 path filter.", + }, + "free": {"type": "boolean", "description": "Filter free or paid endpoints."}, + "mpp": {"type": "boolean", "description": "Filter MPP eligible endpoints."}, + "include_actions": { + "type": "boolean", + "description": "Include write-like and private endpoints in catalog results.", + "default": False, + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25, + "description": "Maximum endpoint descriptors to return.", + }, + }, + "additionalProperties": False, + }, +} + +TWEET_READ = { + "name": "tweet_read", + "description": ( + "Invoke one catalog-listed read-only Xquik endpoint. Use concrete /api/v1 paths " + "from tweet_explore. This tool rejects write-like and private endpoints." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 8, + "pattern": _API_PATH_PATTERN, + "description": _API_PATH_DESCRIPTION, + }, + "query": { + "type": "object", + "description": "Query parameters as string, number, or boolean values.", + "propertyNames": {"minLength": 1, "pattern": "\\S"}, + "additionalProperties": {"type": ["string", "number", "boolean"]}, + }, + }, + "required": ["path"], + "additionalProperties": False, + }, +} + +TWEET_ACTION = { + "name": "tweet_action", + "description": ( + "Invoke one catalog-listed Xquik action endpoint, including writes and private reads. " + "Disabled unless HERMES_TWEET_ENABLE_ACTIONS=true. Show the endpoint and payload " + "to the user first." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 8, + "pattern": _API_PATH_PATTERN, + "description": _API_PATH_DESCRIPTION, + }, + "method": {"type": "string", "enum": _METHOD_ENUM, "default": "POST"}, + "query": { + "type": "object", + "description": "Query parameters as string, number, or boolean values.", + "propertyNames": {"minLength": 1, "pattern": "\\S"}, + "additionalProperties": {"type": ["string", "number", "boolean"]}, + }, + "body": { + "description": "JSON request body.", + "type": ["object", "array", "string", "number", "boolean", "null"], + }, + "reason": { + "type": "string", + "minLength": 1, + "pattern": "\\S", + "description": "Brief user-visible reason for the action.", + }, + }, + "required": ["path", "reason"], + "additionalProperties": False, + }, +} diff --git a/plugins/hermes-tweet/hermes_tweet/skills/__init__.py b/plugins/hermes-tweet/hermes_tweet/skills/__init__.py new file mode 100644 index 0000000..8b2c69c --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/skills/__init__.py @@ -0,0 +1 @@ +"""Bundled Hermes Tweet skills.""" diff --git a/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md new file mode 100644 index 0000000..817eb61 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/SKILL.md @@ -0,0 +1,249 @@ +--- +name: hermes-tweet +version: 0.1.6 +author: Xquik +description: Use Xquik from Hermes Agent for X search, posting, replies, likes, retweets, follows, DMs, monitors, extraction jobs, draws, media, and trends. +tags: + - hermes-agent + - xquik + - twitter + - x + - social-media + - automation +metadata: + version: 0.1.6 + author: Xquik + tags: + - hermes-agent + - xquik + - twitter + - x + - social-media + - automation +capabilities: + shell: + required: false + justification: Optional Hermes CLI checks are used only for installation and registry diagnostics. + network: + required: true + justification: Hermes Tweet tools call Xquik API routes for X/Twitter reads and approved actions. + files: + required: false + justification: Normal use does not require local file reads or writes. + environment: + required: true + variables: + - XQUIK_API_KEY + - HERMES_TWEET_ENABLE_ACTIONS + - HERMES_ENABLE_PROJECT_PLUGINS + justification: Runtime configuration controls authenticated reads, gated actions, and trusted project-local plugin loading. + mcp: + required: false + justification: No MCP server access is required. + tools: + - tweet_explore + - tweet_read + - tweet_action +--- + +# Hermes Tweet + +Use Hermes Tweet when the user wants to automate or inspect X through Xquik. + +## When to Use + +Use this skill for Hermes Agent sessions that need X/Twitter data or controlled +X actions through the Hermes Tweet plugin. + +Use this skill especially for social listening, launch monitoring, support +triage, creator research, brand research, giveaway audits, community audits, +and controlled publishing workflows. + +Use `tweet_explore` first when the user asks for a capability, endpoint, route, +or Xquik API surface. Use `tweet_read` only after a read-only endpoint is known. +Use `tweet_action` only after the user requests a write, private read, monitor, +webhook, extraction job, giveaway draw, or media operation that requires action +permissions. + +## Permissions and Capabilities + +- Use `tweet_explore`, `tweet_read`, and `tweet_action` only through the enabled + Hermes Tweet toolset. +- Network access is limited to catalog-listed Xquik API routes reached by those + tools. Do not create direct HTTP fallbacks. +- Shell access is not part of normal operation. Use Hermes CLI commands only for + the install and registry checks listed in Testing. +- Local file access is not part of normal operation. Do not write reports, + credentials, logs, screenshots, or cached API payloads unless the user asks + for an explicit export workflow. +- Environment access is limited to configuration presence checks for + `XQUIK_API_KEY`, `HERMES_TWEET_ENABLE_ACTIONS`, and + `HERMES_ENABLE_PROJECT_PLUGINS`. Never request or echo their values. +- MCP access is not required. + +## Workflow + +1. Use `tweet_explore` to find the endpoint. +2. Use `tweet_read` for public read-only endpoints. +3. Use `tweet_action` only for writes or private reads after stating the exact endpoint and payload. + +## Decision Rules + +- IF the task is endpoint discovery, THEN call `tweet_explore` with a short + query. +- IF the endpoint method is `GET` and the catalog does not mark it as an + action, THEN call `tweet_read`. +- IF the endpoint method is not `GET`, or the route touches private account + state, THEN call `tweet_action` only when actions are enabled and the user has + approved the operation. +- IF `tweet_action` is unavailable or disabled, THEN explain that action tools + are intentionally gated by `HERMES_TWEET_ENABLE_ACTIONS=true`. +- IF `XQUIK_API_KEY` is missing, THEN ask the user to set it in the Hermes + runtime environment without requesting the key value in chat. +- IF Hermes lists the plugin as `not enabled`, THEN tell the user to run + `hermes plugins enable hermes-tweet` or reinstall with `--enable`. +- IF the plugin is installed as a project-local `.hermes/plugins/` copy, THEN + remind the user that Hermes requires `HERMES_ENABLE_PROJECT_PLUGINS=true` for + trusted repositories. +- IF the task is unattended, scheduled, gateway-driven, or cron-driven, THEN + prefer `tweet_read` and keep `tweet_action` disabled unless the workflow has a + clear approval step. +- IF the user is in Hermes Desktop with a remote gateway profile, THEN remind + them that Hermes Tweet must be installed, enabled, and configured on the + remote Hermes host where plugin tools execute. +- IF the user uses the Hermes dashboard for gateway administration or + credentials, THEN keep Hermes Tweet secrets in the runtime environment and do + not ask for key values in chat. + +## Safety + +- Never ask for or reveal API keys, signing keys, passwords, cookies, or TOTP secrets. +- Never pass credentials in tool arguments. +- Use only catalog-listed `/api/v1/...` endpoints. +- Copied endpoint URLs are accepted only when they resolve to catalog-listed paths. +- Do not use account connection, re-authentication, API key, billing, credit top-up, or support-ticket endpoints. +- For posting, deleting, following, DMs, profile changes, monitors, webhooks, extraction jobs, and draws, summarize the action before calling `tweet_action`. + +## Known Risks and Mitigations + +- Risk: A broad X/Twitter request may map to a write-capable route. + Mitigation: Start with `tweet_explore`, prefer `tweet_read`, and require a + user-approved endpoint plus payload before `tweet_action`. +- Risk: Secrets may be pasted into chat or examples. + Mitigation: Ask only for environment configuration, never for key values, and + never put credentials in tool arguments. +- Risk: Endpoint guessing may bypass catalog review. + Mitigation: Accept only catalog-listed `/api/v1/...` paths and reject direct + HTTP fallbacks. +- Risk: Automated X/Twitter actions can affect real accounts. + Mitigation: Keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize + side effects before any account-changing call. + +## Skill Output + +- Output type: endpoint selection, API-result summaries, action previews, and + troubleshooting guidance. +- Output format: concise Markdown for humans and JSON-like tool payloads for + Hermes Tweet calls. +- Side effects: `tweet_explore` has no external side effects, `tweet_read` + performs authenticated reads, and `tweet_action` may change account or + workflow state only after explicit approval. + +## Pitfalls + +- Do not guess endpoint paths. Always use the catalog returned by `tweet_explore`. +- Do not treat a slash command prompt as proof that Hermes registered the + command. Verify slash commands through an active Hermes session or plugin + registry test. +- Do not use bare `hermes tools` for scripted diagnostics. Run + `hermes tools list` instead. +- Do not assume installation means execution. Current Hermes Agent versions + discover third-party plugins before they are enabled. +- Do not assume the Desktop app stores plugin secrets for a remote gateway. + Configure `XQUIK_API_KEY` where the Hermes runtime executes. +- Do not retry writes through alternate routes after a policy, auth, or account + state error. +- Do not include secrets in examples, logs, prompts, issue bodies, or tool input. + +## Hermes Agent v0.16.0 Surfaces + +Hermes Agent v0.16.0 added a native Desktop app, remote gateway profiles, a +larger web dashboard, and a command palette that can surface skills and quick +commands. Hermes Tweet uses the same plugin entry point on all of those +surfaces: + +- Install and enable `hermes-tweet` on the Hermes runtime host. +- Put `XQUIK_API_KEY` in the runtime environment or `~/.hermes/.env`. +- Keep `HERMES_TWEET_ENABLE_ACTIONS=false` unless the session intentionally + allows account-changing actions. +- Use Desktop, TUI, CLI, or gateway sessions for interactive slash commands such + as `/xstatus` and `/xtrends`. + +## Examples + +Search tweets: + +```json +{"query":"tweet search","method":"GET"} +``` + +Then call: + +```json +{"path":"/api/v1/x/tweets/search","query":{"q":"AI agents","limit":25}} +``` + +Post a tweet: + +```json +{"query":"post tweet","include_actions":true} +``` + +Then call `tweet_action` with: + +```json +{"path":"/api/v1/x/tweets","method":"POST","body":{"account":"@example","text":"Hello from Hermes Tweet"},"reason":"Post the user-approved tweet."} +``` + +## Testing + +After installing or upgrading the plugin in Hermes Agent: + +1. Run `hermes plugins enable hermes-tweet` unless the install used `--enable`. +2. Run `hermes plugins list` and confirm the plugin is `enabled`. +3. Run `hermes tools list` and confirm the `hermes-tweet` toolset is enabled. +4. Confirm `tweet_explore` is available without `XQUIK_API_KEY`. +5. Confirm `tweet_read` appears only when `XQUIK_API_KEY` is configured. +6. Confirm `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`. + +Useful CLI checks: + +```bash +hermes plugins enable hermes-tweet +hermes tools list +``` + +## Release Trust Gate + +Before presenting this skill as NVIDIA-verified or ready for broad enterprise +deployment: + +1. Run SkillSpector against the complete skill directory and resolve critical or + high findings. +2. Complete `skill-card.md` with owner, license, use case, deployment + geography, risks, references, output shape, and release version. +3. Include Tier-3 eval data and `BENCHMARK.md` for the reviewed release. +4. Sign the exact reviewed skill directory and publish `skill.oms.sig`. +5. Verify the published directory with the expected certificate chain. + +Do not claim NVIDIA verification when those release artifacts are absent. + +## Version History + +- Unreleased: Add NVIDIA-style capability declarations, risk controls, output + shape, and release trust gate. +- Unreleased: Refresh current Hermes Agent opt-in plugin lifecycle guidance and + workflow positioning. +- 0.1.6: Refresh catalog wording from current Xquik OpenAPI. +- 0.1.5: Add registry-compatible nested metadata and clearer Hermes runtime guidance. +- 0.1.4: Add public registry frontmatter for skill directory discovery. diff --git a/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md new file mode 100644 index 0000000..9ef9774 --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/skills/hermes-tweet/skill-card.md @@ -0,0 +1,102 @@ +# Hermes Tweet Skill Card + +Status: public self-assessment. Not NVIDIA-verified. + +Do not present Hermes Tweet as NVIDIA-verified unless the release also includes +a clean SkillSpector scan report, Tier-3 eval data, `BENCHMARK.md`, +`skill.oms.sig`, and signature verification instructions for the exact reviewed +skill directory. + +## Owner + +- Publisher: Xquik +- Repository: https://github.com/Xquik-dev/hermes-tweet +- License: MIT +- Version: 0.1.6 +- Primary skill file: `SKILL.md` + +## Use Case + +Hermes Tweet helps Hermes Agent users find X/Twitter endpoints, perform +authenticated X/Twitter reads, and run explicitly approved X/Twitter workflow +actions through the bundled Hermes Tweet tools. + +Use it for: + +- Searching tweets, reading tweet details, replies, and user profiles. +- Preparing action previews for posts, replies, follows, direct messages, + monitors, webhooks, extraction jobs, media workflows, and giveaway draws. +- Keeping X/Twitter automation inside catalog-listed Xquik API routes. + +Do not use it for account connection, re-authentication, billing, credit top-up, +support tickets, or direct HTTP fallback routes. + +## Inputs and Configuration + +- Required configuration: `XQUIK_API_KEY` must be configured in the runtime + environment. Never request, echo, log, or store the value. +- Action gate: `HERMES_TWEET_ENABLE_ACTIONS=true` is required before + write-capable tool calls. +- Project plugin gate: `HERMES_ENABLE_PROJECT_PLUGINS=true` is required for + trusted local Hermes project plugin loading. +- User input: natural language requests, endpoint choices, and explicit action + payload approval. + +## Capabilities + +- Tools: `tweet_explore`, `tweet_read`, `tweet_action`. +- Network: required only through catalog-listed Xquik API routes reached by + those tools. +- Shell: not required for normal operation. Use Hermes CLI commands only for + installation and registry diagnostics. +- Files: not required for normal operation. Do not write reports, credentials, + logs, screenshots, or cached payloads unless the user asks for an explicit + export workflow. +- MCP: not required. + +## Outputs + +- Endpoint recommendations from `tweet_explore`. +- Concise summaries of authenticated read results from `tweet_read`. +- Action previews, JSON-like payloads, and post-call summaries for + user-approved `tweet_action` calls. +- Troubleshooting guidance for missing configuration or disabled action gates. + +## Side Effects + +- `tweet_explore` has no external side effects. +- `tweet_read` performs authenticated reads. +- `tweet_action` may change account or workflow state only after explicit user + approval and only when the action gate is enabled. + +## Known Risks and Mitigations + +- Risk: a broad X/Twitter request may map to a write-capable route. + Mitigation: start with `tweet_explore`, prefer `tweet_read`, and require a + user-approved endpoint plus payload before `tweet_action`. +- Risk: secrets may appear in chat or examples. + Mitigation: ask only for environment configuration, never key values, and + never put credentials in tool arguments. +- Risk: endpoint guessing may bypass catalog review. + Mitigation: accept only catalog-listed `/api/v1/...` paths and reject direct + HTTP fallbacks. +- Risk: automated X/Twitter actions can affect real accounts. + Mitigation: keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize + side effects before any account-changing call. + +## Release Trust Gate + +Before broad enterprise release or any NVIDIA-verified claim: + +1. Run SkillSpector against the complete skill directory. +2. Resolve critical or high findings. +3. Add Tier-3 eval data and `BENCHMARK.md` for the reviewed release. +4. Sign the exact reviewed skill directory and publish `skill.oms.sig`. +5. Verify the published directory with the expected certificate chain. + +## References + +- `SKILL.md` +- `README.md` +- `after-install.md` +- `SECURITY.md` diff --git a/plugins/hermes-tweet/hermes_tweet/tools.py b/plugins/hermes-tweet/hermes_tweet/tools.py new file mode 100644 index 0000000..77c3cfe --- /dev/null +++ b/plugins/hermes-tweet/hermes_tweet/tools.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from typing import Any, cast + +from .catalog import explore as explore_catalog +from .catalog import find_endpoint, matches_path, normalize_method, normalize_path +from .client import action_enabled, check_api_available, dumps, normalize_query_params, request + +ARGS_ERROR = "Tool arguments must be a JSON object." +ACTION_REASON_ERROR = "Action reason is required." +PATH_QUERY_ERROR = "Pass query parameters through the query object, not in path." +BLOCKED_ACTION_ERROR = ( + "Endpoint is blocked: account-connection challenges are not callable through Hermes Tweet." +) +BLOCKED_ACTION_ENDPOINTS: tuple[tuple[str, str], ...] = ( + ("POST", "/api/v1/x/account-connection-challenges/{id}/submit"), +) + + +def _args(value: Any) -> dict[str, Any] | None: + if not isinstance(value, dict): + return None + return cast("dict[str, Any]", value) + + +def _args_error() -> str: + return dumps({"success": False, "error": ARGS_ERROR}) + + +def _text(value: Any) -> str: + if not isinstance(value, str): + return "" + return value.strip() + + +def _path_error(path: str) -> str: + if "?" not in path and "#" not in path: + return "" + return PATH_QUERY_ERROR + + +def _is_blocked_action(method: str, path: str) -> bool: + return any( + blocked_method == method and matches_path(blocked_path, path) + for blocked_method, blocked_path in BLOCKED_ACTION_ENDPOINTS + ) + + +def explore(args: Any, **_: Any) -> str: + try: + tool_args = _args(args) + if tool_args is None: + return _args_error() + return dumps({"success": True, "endpoints": explore_catalog(tool_args)}) + except Exception as exc: + return dumps({"success": False, "error": str(exc)}) + + +def call_read(args: Any, **_: Any) -> str: + try: + tool_args = _args(args) + if tool_args is None: + return _args_error() + path = _text(tool_args.get("path")) + path_error = _path_error(path) + if path_error: + return dumps({"success": False, "error": path_error}) + catalog_path = normalize_path(path) + endpoint = find_endpoint("GET", catalog_path) + if endpoint is None: + return dumps( + { + "success": False, + "error": f"Endpoint is not in the Hermes Tweet catalog: GET {path}", + } + ) + if endpoint.action: + return dumps( + { + "success": False, + "error": "Use tweet_action for private or write-like endpoints.", + } + ) + return dumps( + request("GET", catalog_path, query=normalize_query_params(tool_args.get("query"))) + ) + except Exception as exc: + return dumps({"success": False, "error": str(exc)}) + + +def call_action(args: Any, **_: Any) -> str: + try: + tool_args = _args(args) + if tool_args is None: + return _args_error() + endpoint_error = "" + if not action_enabled(): + endpoint_error = ( + "tweet_action is disabled. Set HERMES_TWEET_ENABLE_ACTIONS=true to enable it." + ) + elif not _text(tool_args.get("reason")): + endpoint_error = ACTION_REASON_ERROR + method = normalize_method(tool_args.get("method"), default="POST") + path = _text(tool_args.get("path")) + path_error = _path_error(path) + catalog_path = normalize_path(path) + endpoint = find_endpoint(method, catalog_path) + if not endpoint_error and path_error: + endpoint_error = path_error + elif _is_blocked_action(method, catalog_path): + endpoint_error = BLOCKED_ACTION_ERROR + elif endpoint is None: + endpoint_error = f"Endpoint is not in the Hermes Tweet catalog: {method} {path}" + if endpoint_error: + return dumps( + { + "success": False, + "error": endpoint_error, + } + ) + return dumps( + request( + method, + catalog_path, + query=normalize_query_params(tool_args.get("query")), + body=tool_args.get("body"), + ) + ) + except Exception as exc: + return dumps({"success": False, "error": str(exc)}) + + +def xstatus(raw_args: Any = "") -> str: + _ = raw_args + return call_read({"path": "/api/v1/account"}) + + +def xtrends(raw_args: Any = "") -> str: + category = _text(raw_args) + query = {"category": category} if category else None + return call_read({"path": "/api/v1/x/trends", "query": query}) + + +__all__ = [ + "action_enabled", + "call_action", + "call_read", + "check_api_available", + "explore", + "xstatus", + "xtrends", +] diff --git a/plugins/hermes-tweet/plugin.yaml b/plugins/hermes-tweet/plugin.yaml new file mode 100644 index 0000000..8a0af54 --- /dev/null +++ b/plugins/hermes-tweet/plugin.yaml @@ -0,0 +1,12 @@ +name: hermes-tweet +version: 0.1.6 +description: "Native Hermes Agent plugin for X/Twitter automation through Xquik" +author: Xquik +optional_env: + - XQUIK_API_KEY + - XQUIK_BASE_URL + - HERMES_TWEET_ENABLE_ACTIONS +provides_tools: + - tweet_explore + - tweet_read + - tweet_action diff --git a/plugins/hermes-tweet/pyproject.toml b/plugins/hermes-tweet/pyproject.toml new file mode 100644 index 0000000..963fe0d --- /dev/null +++ b/plugins/hermes-tweet/pyproject.toml @@ -0,0 +1,132 @@ +[build-system] +requires = ["setuptools>=82.0.1", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "hermes-tweet" +version = "0.1.6" +description = "Native Hermes Agent plugin for X/Twitter automation through Xquik" +readme = "README.md" +requires-python = ">=3.11" +license = "MIT" +authors = [{ name = "Xquik" }] +dependencies = [ + "httpx>=0.28.1,<0.29", +] +keywords = [ + "agent-skill", + "agent-tools", + "ai-agent", + "automation", + "hermes-agent", + "hermes-plugin", + "mcp", + "social-media", + "tweet", + "twitter", + "twitter-api", + "twitter-automation", + "x", + "x-api", + "x-automation", + "x-twitter", + "xquik", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Plugins", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Communications", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.optional-dependencies] +dev = [ + "bandit[toml]>=1.9.4", + "basedpyright>=1.39.9", + "build>=1.5.0", + "pip>=26.1.2", + "pip-audit>=2.10.1", + "PyYAML>=6.0.3", + "pytest>=9.1.1", + "pytest-cov>=7.1.0", + "ruff>=0.15.20", + "twine>=6.2.0", + "types-PyYAML>=6.0.12.20260518", +] + +[project.urls] +Homepage = "https://github.com/Xquik-dev/hermes-tweet#readme" +Documentation = "https://github.com/Xquik-dev/hermes-tweet#readme" +Repository = "https://github.com/Xquik-dev/hermes-tweet" +Issues = "https://github.com/Xquik-dev/hermes-tweet/issues" +ClawHub = "https://clawhub.ai/skills/hermes-tweet" +DeepWiki = "https://deepwiki.com/Xquik-dev/hermes-tweet" +piwheels = "https://piwheels.org/project/hermes-tweet/" +"Xquik Platform" = "https://xquik.com" + +[project.entry-points."hermes_agent.plugins"] +hermes-tweet = "hermes_tweet" + +[tool.setuptools] +packages = ["hermes_tweet", "hermes_tweet.skills"] +include-package-data = true + +[tool.setuptools.package-data] +hermes_tweet = [ + "plugin.yaml", + "catalog_data.json", + "skills/hermes-tweet/SKILL.md", +] + +[tool.ruff] +line-length = 100 +src = ["hermes_tweet", "scripts", "tests"] +target-version = "py311" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN401", + "BLE001", + "COM812", + "D100", + "D101", + "D102", + "D103", + "D104", + "D203", + "D213", + "ISC001", + "N999", + "TRY300", +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/*.py" = ["D105", "D107", "EM101", "PLR2004", "S101", "TRY003"] +"scripts/**/*.py" = ["T201"] + +[tool.ruff.lint.isort] +known-first-party = ["hermes_tweet"] + +[tool.basedpyright] +include = ["hermes_tweet", "scripts", "tests"] +pythonVersion = "3.11" +typeCheckingMode = "strict" + +[tool.coverage.run] +source = ["hermes_tweet", "tests"] + +[tool.coverage.report] +fail_under = 100 +show_missing = true +skip_covered = true + +[tool.bandit] +exclude_dirs = ["tests"] +skips = [] diff --git a/plugins/hermes-tweet/skill.json b/plugins/hermes-tweet/skill.json new file mode 100644 index 0000000..2c98f5a --- /dev/null +++ b/plugins/hermes-tweet/skill.json @@ -0,0 +1,39 @@ +{ + "name": "hermes-tweet", + "version": "0.1.6", + "description": "Hermes Agent X/Twitter plugin for Xquik automation", + "author": "Xquik", + "tags": [ + "hermes-agent", + "hermes-plugin", + "xquik", + "twitter", + "x", + "social-media", + "automation" + ], + "dependencies": [], + "conflicts": [], + "install": { + "openclaw": "hermes plugins install Xquik-dev/hermes-tweet --enable" + }, + "keywords": [ + "agent-skill", + "hermes-agent", + "hermes-plugin", + "xquik", + "twitter", + "twitter-api", + "twitter-automation", + "x", + "x-api", + "x-automation", + "x-twitter", + "social-media", + "automation", + "agent-tools", + "mcp" + ], + "homepage": "https://github.com/Xquik-dev/hermes-tweet#readme", + "repository": "https://github.com/Xquik-dev/hermes-tweet" +} diff --git a/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md b/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md new file mode 100644 index 0000000..817eb61 --- /dev/null +++ b/plugins/hermes-tweet/skills/hermes-tweet/SKILL.md @@ -0,0 +1,249 @@ +--- +name: hermes-tweet +version: 0.1.6 +author: Xquik +description: Use Xquik from Hermes Agent for X search, posting, replies, likes, retweets, follows, DMs, monitors, extraction jobs, draws, media, and trends. +tags: + - hermes-agent + - xquik + - twitter + - x + - social-media + - automation +metadata: + version: 0.1.6 + author: Xquik + tags: + - hermes-agent + - xquik + - twitter + - x + - social-media + - automation +capabilities: + shell: + required: false + justification: Optional Hermes CLI checks are used only for installation and registry diagnostics. + network: + required: true + justification: Hermes Tweet tools call Xquik API routes for X/Twitter reads and approved actions. + files: + required: false + justification: Normal use does not require local file reads or writes. + environment: + required: true + variables: + - XQUIK_API_KEY + - HERMES_TWEET_ENABLE_ACTIONS + - HERMES_ENABLE_PROJECT_PLUGINS + justification: Runtime configuration controls authenticated reads, gated actions, and trusted project-local plugin loading. + mcp: + required: false + justification: No MCP server access is required. + tools: + - tweet_explore + - tweet_read + - tweet_action +--- + +# Hermes Tweet + +Use Hermes Tweet when the user wants to automate or inspect X through Xquik. + +## When to Use + +Use this skill for Hermes Agent sessions that need X/Twitter data or controlled +X actions through the Hermes Tweet plugin. + +Use this skill especially for social listening, launch monitoring, support +triage, creator research, brand research, giveaway audits, community audits, +and controlled publishing workflows. + +Use `tweet_explore` first when the user asks for a capability, endpoint, route, +or Xquik API surface. Use `tweet_read` only after a read-only endpoint is known. +Use `tweet_action` only after the user requests a write, private read, monitor, +webhook, extraction job, giveaway draw, or media operation that requires action +permissions. + +## Permissions and Capabilities + +- Use `tweet_explore`, `tweet_read`, and `tweet_action` only through the enabled + Hermes Tweet toolset. +- Network access is limited to catalog-listed Xquik API routes reached by those + tools. Do not create direct HTTP fallbacks. +- Shell access is not part of normal operation. Use Hermes CLI commands only for + the install and registry checks listed in Testing. +- Local file access is not part of normal operation. Do not write reports, + credentials, logs, screenshots, or cached API payloads unless the user asks + for an explicit export workflow. +- Environment access is limited to configuration presence checks for + `XQUIK_API_KEY`, `HERMES_TWEET_ENABLE_ACTIONS`, and + `HERMES_ENABLE_PROJECT_PLUGINS`. Never request or echo their values. +- MCP access is not required. + +## Workflow + +1. Use `tweet_explore` to find the endpoint. +2. Use `tweet_read` for public read-only endpoints. +3. Use `tweet_action` only for writes or private reads after stating the exact endpoint and payload. + +## Decision Rules + +- IF the task is endpoint discovery, THEN call `tweet_explore` with a short + query. +- IF the endpoint method is `GET` and the catalog does not mark it as an + action, THEN call `tweet_read`. +- IF the endpoint method is not `GET`, or the route touches private account + state, THEN call `tweet_action` only when actions are enabled and the user has + approved the operation. +- IF `tweet_action` is unavailable or disabled, THEN explain that action tools + are intentionally gated by `HERMES_TWEET_ENABLE_ACTIONS=true`. +- IF `XQUIK_API_KEY` is missing, THEN ask the user to set it in the Hermes + runtime environment without requesting the key value in chat. +- IF Hermes lists the plugin as `not enabled`, THEN tell the user to run + `hermes plugins enable hermes-tweet` or reinstall with `--enable`. +- IF the plugin is installed as a project-local `.hermes/plugins/` copy, THEN + remind the user that Hermes requires `HERMES_ENABLE_PROJECT_PLUGINS=true` for + trusted repositories. +- IF the task is unattended, scheduled, gateway-driven, or cron-driven, THEN + prefer `tweet_read` and keep `tweet_action` disabled unless the workflow has a + clear approval step. +- IF the user is in Hermes Desktop with a remote gateway profile, THEN remind + them that Hermes Tweet must be installed, enabled, and configured on the + remote Hermes host where plugin tools execute. +- IF the user uses the Hermes dashboard for gateway administration or + credentials, THEN keep Hermes Tweet secrets in the runtime environment and do + not ask for key values in chat. + +## Safety + +- Never ask for or reveal API keys, signing keys, passwords, cookies, or TOTP secrets. +- Never pass credentials in tool arguments. +- Use only catalog-listed `/api/v1/...` endpoints. +- Copied endpoint URLs are accepted only when they resolve to catalog-listed paths. +- Do not use account connection, re-authentication, API key, billing, credit top-up, or support-ticket endpoints. +- For posting, deleting, following, DMs, profile changes, monitors, webhooks, extraction jobs, and draws, summarize the action before calling `tweet_action`. + +## Known Risks and Mitigations + +- Risk: A broad X/Twitter request may map to a write-capable route. + Mitigation: Start with `tweet_explore`, prefer `tweet_read`, and require a + user-approved endpoint plus payload before `tweet_action`. +- Risk: Secrets may be pasted into chat or examples. + Mitigation: Ask only for environment configuration, never for key values, and + never put credentials in tool arguments. +- Risk: Endpoint guessing may bypass catalog review. + Mitigation: Accept only catalog-listed `/api/v1/...` paths and reject direct + HTTP fallbacks. +- Risk: Automated X/Twitter actions can affect real accounts. + Mitigation: Keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize + side effects before any account-changing call. + +## Skill Output + +- Output type: endpoint selection, API-result summaries, action previews, and + troubleshooting guidance. +- Output format: concise Markdown for humans and JSON-like tool payloads for + Hermes Tweet calls. +- Side effects: `tweet_explore` has no external side effects, `tweet_read` + performs authenticated reads, and `tweet_action` may change account or + workflow state only after explicit approval. + +## Pitfalls + +- Do not guess endpoint paths. Always use the catalog returned by `tweet_explore`. +- Do not treat a slash command prompt as proof that Hermes registered the + command. Verify slash commands through an active Hermes session or plugin + registry test. +- Do not use bare `hermes tools` for scripted diagnostics. Run + `hermes tools list` instead. +- Do not assume installation means execution. Current Hermes Agent versions + discover third-party plugins before they are enabled. +- Do not assume the Desktop app stores plugin secrets for a remote gateway. + Configure `XQUIK_API_KEY` where the Hermes runtime executes. +- Do not retry writes through alternate routes after a policy, auth, or account + state error. +- Do not include secrets in examples, logs, prompts, issue bodies, or tool input. + +## Hermes Agent v0.16.0 Surfaces + +Hermes Agent v0.16.0 added a native Desktop app, remote gateway profiles, a +larger web dashboard, and a command palette that can surface skills and quick +commands. Hermes Tweet uses the same plugin entry point on all of those +surfaces: + +- Install and enable `hermes-tweet` on the Hermes runtime host. +- Put `XQUIK_API_KEY` in the runtime environment or `~/.hermes/.env`. +- Keep `HERMES_TWEET_ENABLE_ACTIONS=false` unless the session intentionally + allows account-changing actions. +- Use Desktop, TUI, CLI, or gateway sessions for interactive slash commands such + as `/xstatus` and `/xtrends`. + +## Examples + +Search tweets: + +```json +{"query":"tweet search","method":"GET"} +``` + +Then call: + +```json +{"path":"/api/v1/x/tweets/search","query":{"q":"AI agents","limit":25}} +``` + +Post a tweet: + +```json +{"query":"post tweet","include_actions":true} +``` + +Then call `tweet_action` with: + +```json +{"path":"/api/v1/x/tweets","method":"POST","body":{"account":"@example","text":"Hello from Hermes Tweet"},"reason":"Post the user-approved tweet."} +``` + +## Testing + +After installing or upgrading the plugin in Hermes Agent: + +1. Run `hermes plugins enable hermes-tweet` unless the install used `--enable`. +2. Run `hermes plugins list` and confirm the plugin is `enabled`. +3. Run `hermes tools list` and confirm the `hermes-tweet` toolset is enabled. +4. Confirm `tweet_explore` is available without `XQUIK_API_KEY`. +5. Confirm `tweet_read` appears only when `XQUIK_API_KEY` is configured. +6. Confirm `tweet_action` stays hidden or disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`. + +Useful CLI checks: + +```bash +hermes plugins enable hermes-tweet +hermes tools list +``` + +## Release Trust Gate + +Before presenting this skill as NVIDIA-verified or ready for broad enterprise +deployment: + +1. Run SkillSpector against the complete skill directory and resolve critical or + high findings. +2. Complete `skill-card.md` with owner, license, use case, deployment + geography, risks, references, output shape, and release version. +3. Include Tier-3 eval data and `BENCHMARK.md` for the reviewed release. +4. Sign the exact reviewed skill directory and publish `skill.oms.sig`. +5. Verify the published directory with the expected certificate chain. + +Do not claim NVIDIA verification when those release artifacts are absent. + +## Version History + +- Unreleased: Add NVIDIA-style capability declarations, risk controls, output + shape, and release trust gate. +- Unreleased: Refresh current Hermes Agent opt-in plugin lifecycle guidance and + workflow positioning. +- 0.1.6: Refresh catalog wording from current Xquik OpenAPI. +- 0.1.5: Add registry-compatible nested metadata and clearer Hermes runtime guidance. +- 0.1.4: Add public registry frontmatter for skill directory discovery. diff --git a/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md b/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md new file mode 100644 index 0000000..9ef9774 --- /dev/null +++ b/plugins/hermes-tweet/skills/hermes-tweet/skill-card.md @@ -0,0 +1,102 @@ +# Hermes Tweet Skill Card + +Status: public self-assessment. Not NVIDIA-verified. + +Do not present Hermes Tweet as NVIDIA-verified unless the release also includes +a clean SkillSpector scan report, Tier-3 eval data, `BENCHMARK.md`, +`skill.oms.sig`, and signature verification instructions for the exact reviewed +skill directory. + +## Owner + +- Publisher: Xquik +- Repository: https://github.com/Xquik-dev/hermes-tweet +- License: MIT +- Version: 0.1.6 +- Primary skill file: `SKILL.md` + +## Use Case + +Hermes Tweet helps Hermes Agent users find X/Twitter endpoints, perform +authenticated X/Twitter reads, and run explicitly approved X/Twitter workflow +actions through the bundled Hermes Tweet tools. + +Use it for: + +- Searching tweets, reading tweet details, replies, and user profiles. +- Preparing action previews for posts, replies, follows, direct messages, + monitors, webhooks, extraction jobs, media workflows, and giveaway draws. +- Keeping X/Twitter automation inside catalog-listed Xquik API routes. + +Do not use it for account connection, re-authentication, billing, credit top-up, +support tickets, or direct HTTP fallback routes. + +## Inputs and Configuration + +- Required configuration: `XQUIK_API_KEY` must be configured in the runtime + environment. Never request, echo, log, or store the value. +- Action gate: `HERMES_TWEET_ENABLE_ACTIONS=true` is required before + write-capable tool calls. +- Project plugin gate: `HERMES_ENABLE_PROJECT_PLUGINS=true` is required for + trusted local Hermes project plugin loading. +- User input: natural language requests, endpoint choices, and explicit action + payload approval. + +## Capabilities + +- Tools: `tweet_explore`, `tweet_read`, `tweet_action`. +- Network: required only through catalog-listed Xquik API routes reached by + those tools. +- Shell: not required for normal operation. Use Hermes CLI commands only for + installation and registry diagnostics. +- Files: not required for normal operation. Do not write reports, credentials, + logs, screenshots, or cached payloads unless the user asks for an explicit + export workflow. +- MCP: not required. + +## Outputs + +- Endpoint recommendations from `tweet_explore`. +- Concise summaries of authenticated read results from `tweet_read`. +- Action previews, JSON-like payloads, and post-call summaries for + user-approved `tweet_action` calls. +- Troubleshooting guidance for missing configuration or disabled action gates. + +## Side Effects + +- `tweet_explore` has no external side effects. +- `tweet_read` performs authenticated reads. +- `tweet_action` may change account or workflow state only after explicit user + approval and only when the action gate is enabled. + +## Known Risks and Mitigations + +- Risk: a broad X/Twitter request may map to a write-capable route. + Mitigation: start with `tweet_explore`, prefer `tweet_read`, and require a + user-approved endpoint plus payload before `tweet_action`. +- Risk: secrets may appear in chat or examples. + Mitigation: ask only for environment configuration, never key values, and + never put credentials in tool arguments. +- Risk: endpoint guessing may bypass catalog review. + Mitigation: accept only catalog-listed `/api/v1/...` paths and reject direct + HTTP fallbacks. +- Risk: automated X/Twitter actions can affect real accounts. + Mitigation: keep `HERMES_TWEET_ENABLE_ACTIONS=false` by default and summarize + side effects before any account-changing call. + +## Release Trust Gate + +Before broad enterprise release or any NVIDIA-verified claim: + +1. Run SkillSpector against the complete skill directory. +2. Resolve critical or high findings. +3. Add Tier-3 eval data and `BENCHMARK.md` for the reviewed release. +4. Sign the exact reviewed skill directory and publish `skill.oms.sig`. +5. Verify the published directory with the expected certificate chain. + +## References + +- `SKILL.md` +- `README.md` +- `after-install.md` +- `SECURITY.md`