hermes-tweet 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hermes_tweet-0.1.0/LICENSE +22 -0
- hermes_tweet-0.1.0/MANIFEST.in +4 -0
- hermes_tweet-0.1.0/PKG-INFO +180 -0
- hermes_tweet-0.1.0/README.md +142 -0
- hermes_tweet-0.1.0/hermes_tweet/__init__.py +81 -0
- hermes_tweet-0.1.0/hermes_tweet/catalog.py +164 -0
- hermes_tweet-0.1.0/hermes_tweet/catalog_data.json +2566 -0
- hermes_tweet-0.1.0/hermes_tweet/client.py +87 -0
- hermes_tweet-0.1.0/hermes_tweet/plugin.yaml +17 -0
- hermes_tweet-0.1.0/hermes_tweet/schemas.py +97 -0
- hermes_tweet-0.1.0/hermes_tweet/skills/__init__.py +1 -0
- hermes_tweet-0.1.0/hermes_tweet/skills/hermes-tweet/SKILL.md +49 -0
- hermes_tweet-0.1.0/hermes_tweet/tools.py +103 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/PKG-INFO +180 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/SOURCES.txt +24 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/dependency_links.txt +1 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/entry_points.txt +2 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/requires.txt +13 -0
- hermes_tweet-0.1.0/hermes_tweet.egg-info/top_level.txt +1 -0
- hermes_tweet-0.1.0/plugin.yaml +17 -0
- hermes_tweet-0.1.0/pyproject.toml +118 -0
- hermes_tweet-0.1.0/setup.cfg +4 -0
- hermes_tweet-0.1.0/tests/test_catalog.py +89 -0
- hermes_tweet-0.1.0/tests/test_client.py +158 -0
- hermes_tweet-0.1.0/tests/test_plugin.py +41 -0
- hermes_tweet-0.1.0/tests/test_tools.py +194 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Xquik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hermes-tweet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native Hermes Agent plugin for X automation through Xquik
|
|
5
|
+
Author: Xquik
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://xquik.com
|
|
8
|
+
Project-URL: Documentation, https://docs.xquik.com
|
|
9
|
+
Project-URL: Repository, https://github.com/Xquik-dev/hermes-tweet
|
|
10
|
+
Project-URL: Issues, https://github.com/Xquik-dev/hermes-tweet/issues
|
|
11
|
+
Keywords: hermes-agent,hermes-plugin,twitter,x,xquik,tweet,automation,social-media
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Plugins
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Communications
|
|
20
|
+
Classifier: Topic :: Internet
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: bandit[toml]>=1.9.2; extra == "dev"
|
|
28
|
+
Requires-Dist: basedpyright>=1.36.0; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.3.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pip-audit>=2.9.0; extra == "dev"
|
|
31
|
+
Requires-Dist: PyYAML>=6.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.14.0; extra == "dev"
|
|
35
|
+
Requires-Dist: twine>=6.2.0; extra == "dev"
|
|
36
|
+
Requires-Dist: types-PyYAML>=6.0.12; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# Hermes Tweet
|
|
40
|
+
|
|
41
|
+
[](https://github.com/Xquik-dev/hermes-tweet/actions/workflows/ci.yml)
|
|
42
|
+
[](https://pypi.org/project/hermes-tweet/)
|
|
43
|
+
[](https://pypi.org/project/hermes-tweet/)
|
|
44
|
+
[](https://apify.com/xquik/x-tweet-scraper)
|
|
45
|
+
[](LICENSE)
|
|
46
|
+
|
|
47
|
+
Native [Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin for
|
|
48
|
+
X automation through [Xquik](https://xquik.com).
|
|
49
|
+
|
|
50
|
+
Hermes Tweet brings X search, account reads, tweet posting, replies, likes,
|
|
51
|
+
retweets, follows, DMs, monitors, webhooks, draws, extraction jobs, media, and
|
|
52
|
+
trend reads into Hermes as structured tools.
|
|
53
|
+
|
|
54
|
+
Use it when you need a Hermes Agent Twitter plugin, Hermes X automation, social
|
|
55
|
+
media automation for agents, or a native Hermes toolset for X/Twitter.
|
|
56
|
+
|
|
57
|
+
## Highlights
|
|
58
|
+
|
|
59
|
+
- Native Hermes plugin with `plugin.yaml` and pip entry point.
|
|
60
|
+
- 99 agent-callable Xquik endpoints generated from OpenAPI.
|
|
61
|
+
- 32 MPP-tagged read endpoints in the bundled catalog.
|
|
62
|
+
- Read and action tools are split for least-privilege operation.
|
|
63
|
+
- Action endpoints are disabled by default.
|
|
64
|
+
- Bundled Hermes skill for agent-facing usage guidance.
|
|
65
|
+
- Slash commands for account status and trends.
|
|
66
|
+
- Strict CI with formatting, linting, type checking, tests, coverage, security
|
|
67
|
+
scan, dependency audit, and package build checks.
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
From a local checkout:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e .
|
|
75
|
+
hermes plugins enable hermes-tweet
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For public distribution:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install hermes-tweet
|
|
82
|
+
hermes plugins enable hermes-tweet
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configure
|
|
86
|
+
|
|
87
|
+
Create an API key in the Xquik dashboard, then set:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export XQUIK_API_KEY="xq_..."
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Optional settings:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export XQUIK_BASE_URL="https://xquik.com"
|
|
97
|
+
export HERMES_TWEET_ENABLE_ACTIONS="false"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Action endpoints are disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
|
|
101
|
+
|
|
102
|
+
## Security Model
|
|
103
|
+
|
|
104
|
+
Hermes Tweet never accepts credentials through tool arguments. Auth is read from
|
|
105
|
+
environment variables and injected by the plugin at request time.
|
|
106
|
+
|
|
107
|
+
The plugin blocks dashboard-only admin, billing, credit top-up, support-ticket,
|
|
108
|
+
API-key, and account re-authentication endpoints from the catalog. Private reads
|
|
109
|
+
and write-like endpoints go through `tweet_action`, which is hidden unless
|
|
110
|
+
`HERMES_TWEET_ENABLE_ACTIONS=true`.
|
|
111
|
+
|
|
112
|
+
## Tools
|
|
113
|
+
|
|
114
|
+
| Tool | Purpose |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `tweet_explore` | Search the bundled Xquik endpoint catalog. No API call. |
|
|
117
|
+
| `tweet_read` | Call catalog-listed read-only endpoints. |
|
|
118
|
+
| `tweet_action` | Call write-like or private endpoints. Disabled by default. |
|
|
119
|
+
|
|
120
|
+
Use `tweet_explore` first, then call `tweet_read` or `tweet_action` with a
|
|
121
|
+
concrete `/api/v1/...` path.
|
|
122
|
+
|
|
123
|
+
## Slash Commands
|
|
124
|
+
|
|
125
|
+
| Command | Purpose |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| `/xstatus` | Show Xquik account, subscription, and usage status. |
|
|
128
|
+
| `/xtrends` | Show current X trends. |
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
Generate the bundled catalog from Xquik OpenAPI:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python scripts/build_catalog.py ../xquik/openapi.yaml
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Run checks:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
uv run --python 3.12 --extra dev ruff format --check .
|
|
142
|
+
uv run --python 3.12 --extra dev ruff check .
|
|
143
|
+
uv run --python 3.12 --extra dev basedpyright
|
|
144
|
+
uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100
|
|
145
|
+
uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts
|
|
146
|
+
uv run --python 3.12 --extra dev pip-audit
|
|
147
|
+
uv run --python 3.12 --extra dev python -m build
|
|
148
|
+
uv run --python 3.12 --extra dev twine check dist/*
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For a single local quality gate:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
uv run --python 3.12 --extra dev ruff format --check . && \
|
|
155
|
+
uv run --python 3.12 --extra dev ruff check . && \
|
|
156
|
+
uv run --python 3.12 --extra dev basedpyright && \
|
|
157
|
+
uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100 && \
|
|
158
|
+
uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts && \
|
|
159
|
+
uv run --python 3.12 --extra dev pip-audit && \
|
|
160
|
+
uv run --python 3.12 --extra dev python -m build && \
|
|
161
|
+
uv run --python 3.12 --extra dev twine check dist/*
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Relationship To TweetClaw
|
|
165
|
+
|
|
166
|
+
TweetClaw is the OpenClaw-native npm plugin. Hermes Tweet is the Hermes-native
|
|
167
|
+
Python plugin. Both use the same Xquik API contract.
|
|
168
|
+
|
|
169
|
+
## Public Repo Metadata
|
|
170
|
+
|
|
171
|
+
Recommended GitHub description:
|
|
172
|
+
|
|
173
|
+
> Native Hermes Agent plugin for X/Twitter automation through Xquik.
|
|
174
|
+
|
|
175
|
+
Recommended topics:
|
|
176
|
+
|
|
177
|
+
`hermes-agent`, `hermes-plugin`, `hermes`, `twitter`, `x`, `x-api`,
|
|
178
|
+
`x-automation`, `xquik`, `tweet`, `automation`, `social-media`,
|
|
179
|
+
`social-media-automation`, `ai-agent`, `mcp`, `agent-tools`, `twitter-api`,
|
|
180
|
+
`twitter-automation`, `x-twitter`, `apify`, `python`
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Hermes Tweet
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Xquik-dev/hermes-tweet/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/hermes-tweet/)
|
|
5
|
+
[](https://pypi.org/project/hermes-tweet/)
|
|
6
|
+
[](https://apify.com/xquik/x-tweet-scraper)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Native [Hermes Agent](https://github.com/NousResearch/hermes-agent) plugin for
|
|
10
|
+
X automation through [Xquik](https://xquik.com).
|
|
11
|
+
|
|
12
|
+
Hermes Tweet brings X search, account reads, tweet posting, replies, likes,
|
|
13
|
+
retweets, follows, DMs, monitors, webhooks, draws, extraction jobs, media, and
|
|
14
|
+
trend reads into Hermes as structured tools.
|
|
15
|
+
|
|
16
|
+
Use it when you need a Hermes Agent Twitter plugin, Hermes X automation, social
|
|
17
|
+
media automation for agents, or a native Hermes toolset for X/Twitter.
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- Native Hermes plugin with `plugin.yaml` and pip entry point.
|
|
22
|
+
- 99 agent-callable Xquik endpoints generated from OpenAPI.
|
|
23
|
+
- 32 MPP-tagged read endpoints in the bundled catalog.
|
|
24
|
+
- Read and action tools are split for least-privilege operation.
|
|
25
|
+
- Action endpoints are disabled by default.
|
|
26
|
+
- Bundled Hermes skill for agent-facing usage guidance.
|
|
27
|
+
- Slash commands for account status and trends.
|
|
28
|
+
- Strict CI with formatting, linting, type checking, tests, coverage, security
|
|
29
|
+
scan, dependency audit, and package build checks.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
From a local checkout:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install -e .
|
|
37
|
+
hermes plugins enable hermes-tweet
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For public distribution:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install hermes-tweet
|
|
44
|
+
hermes plugins enable hermes-tweet
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configure
|
|
48
|
+
|
|
49
|
+
Create an API key in the Xquik dashboard, then set:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export XQUIK_API_KEY="xq_..."
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Optional settings:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export XQUIK_BASE_URL="https://xquik.com"
|
|
59
|
+
export HERMES_TWEET_ENABLE_ACTIONS="false"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Action endpoints are disabled unless `HERMES_TWEET_ENABLE_ACTIONS=true`.
|
|
63
|
+
|
|
64
|
+
## Security Model
|
|
65
|
+
|
|
66
|
+
Hermes Tweet never accepts credentials through tool arguments. Auth is read from
|
|
67
|
+
environment variables and injected by the plugin at request time.
|
|
68
|
+
|
|
69
|
+
The plugin blocks dashboard-only admin, billing, credit top-up, support-ticket,
|
|
70
|
+
API-key, and account re-authentication endpoints from the catalog. Private reads
|
|
71
|
+
and write-like endpoints go through `tweet_action`, which is hidden unless
|
|
72
|
+
`HERMES_TWEET_ENABLE_ACTIONS=true`.
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
| Tool | Purpose |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `tweet_explore` | Search the bundled Xquik endpoint catalog. No API call. |
|
|
79
|
+
| `tweet_read` | Call catalog-listed read-only endpoints. |
|
|
80
|
+
| `tweet_action` | Call write-like or private endpoints. Disabled by default. |
|
|
81
|
+
|
|
82
|
+
Use `tweet_explore` first, then call `tweet_read` or `tweet_action` with a
|
|
83
|
+
concrete `/api/v1/...` path.
|
|
84
|
+
|
|
85
|
+
## Slash Commands
|
|
86
|
+
|
|
87
|
+
| Command | Purpose |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `/xstatus` | Show Xquik account, subscription, and usage status. |
|
|
90
|
+
| `/xtrends` | Show current X trends. |
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
Generate the bundled catalog from Xquik OpenAPI:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python scripts/build_catalog.py ../xquik/openapi.yaml
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Run checks:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
uv run --python 3.12 --extra dev ruff format --check .
|
|
104
|
+
uv run --python 3.12 --extra dev ruff check .
|
|
105
|
+
uv run --python 3.12 --extra dev basedpyright
|
|
106
|
+
uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100
|
|
107
|
+
uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts
|
|
108
|
+
uv run --python 3.12 --extra dev pip-audit
|
|
109
|
+
uv run --python 3.12 --extra dev python -m build
|
|
110
|
+
uv run --python 3.12 --extra dev twine check dist/*
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
For a single local quality gate:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
uv run --python 3.12 --extra dev ruff format --check . && \
|
|
117
|
+
uv run --python 3.12 --extra dev ruff check . && \
|
|
118
|
+
uv run --python 3.12 --extra dev basedpyright && \
|
|
119
|
+
uv run --python 3.12 --extra dev pytest --cov=hermes_tweet --cov=tests --cov-report=term-missing --cov-fail-under=100 && \
|
|
120
|
+
uv run --python 3.12 --extra dev bandit -c pyproject.toml -r hermes_tweet scripts && \
|
|
121
|
+
uv run --python 3.12 --extra dev pip-audit && \
|
|
122
|
+
uv run --python 3.12 --extra dev python -m build && \
|
|
123
|
+
uv run --python 3.12 --extra dev twine check dist/*
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Relationship To TweetClaw
|
|
127
|
+
|
|
128
|
+
TweetClaw is the OpenClaw-native npm plugin. Hermes Tweet is the Hermes-native
|
|
129
|
+
Python plugin. Both use the same Xquik API contract.
|
|
130
|
+
|
|
131
|
+
## Public Repo Metadata
|
|
132
|
+
|
|
133
|
+
Recommended GitHub description:
|
|
134
|
+
|
|
135
|
+
> Native Hermes Agent plugin for X/Twitter automation through Xquik.
|
|
136
|
+
|
|
137
|
+
Recommended topics:
|
|
138
|
+
|
|
139
|
+
`hermes-agent`, `hermes-plugin`, `hermes`, `twitter`, `x`, `x-api`,
|
|
140
|
+
`x-automation`, `xquik`, `tweet`, `automation`, `social-media`,
|
|
141
|
+
`social-media-automation`, `ai-agent`, `mcp`, `agent-tools`, `twitter-api`,
|
|
142
|
+
`twitter-automation`, `x-twitter`, `apify`, `python`
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import schemas
|
|
9
|
+
from .tools import (
|
|
10
|
+
action_enabled,
|
|
11
|
+
call_action,
|
|
12
|
+
call_read,
|
|
13
|
+
check_api_available,
|
|
14
|
+
explore,
|
|
15
|
+
xstatus,
|
|
16
|
+
xtrends,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
TOOLSET = "hermes-tweet"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _register_bundled_skills(ctx: Any) -> None:
|
|
25
|
+
skills_dir = Path(__file__).parent / "skills"
|
|
26
|
+
for child in sorted(skills_dir.iterdir()):
|
|
27
|
+
skill_md = child / "SKILL.md"
|
|
28
|
+
if child.is_dir() and skill_md.exists():
|
|
29
|
+
ctx.register_skill(child.name, skill_md)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def register(ctx: Any) -> None:
|
|
33
|
+
ctx.register_tool(
|
|
34
|
+
name="tweet_explore",
|
|
35
|
+
toolset=TOOLSET,
|
|
36
|
+
schema=schemas.TWEET_EXPLORE,
|
|
37
|
+
handler=explore,
|
|
38
|
+
is_async=False,
|
|
39
|
+
description="Search the bundled Xquik endpoint catalog.",
|
|
40
|
+
emoji="🔎",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
ctx.register_tool(
|
|
44
|
+
name="tweet_read",
|
|
45
|
+
toolset=TOOLSET,
|
|
46
|
+
schema=schemas.TWEET_READ,
|
|
47
|
+
handler=call_read,
|
|
48
|
+
check_fn=check_api_available,
|
|
49
|
+
requires_env=["XQUIK_API_KEY"],
|
|
50
|
+
is_async=False,
|
|
51
|
+
description="Call catalog-listed read-only Xquik endpoints.",
|
|
52
|
+
emoji="📖",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ctx.register_tool(
|
|
56
|
+
name="tweet_action",
|
|
57
|
+
toolset=TOOLSET,
|
|
58
|
+
schema=schemas.TWEET_ACTION,
|
|
59
|
+
handler=call_action,
|
|
60
|
+
check_fn=action_enabled,
|
|
61
|
+
requires_env=["XQUIK_API_KEY", "HERMES_TWEET_ENABLE_ACTIONS"],
|
|
62
|
+
is_async=False,
|
|
63
|
+
description="Call write-like or private Xquik endpoints.",
|
|
64
|
+
emoji="✍️",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
ctx.register_command(
|
|
68
|
+
"xstatus",
|
|
69
|
+
handler=xstatus,
|
|
70
|
+
description="Show Xquik account and usage status",
|
|
71
|
+
)
|
|
72
|
+
ctx.register_command("xtrends", handler=xtrends, description="Show current X trends")
|
|
73
|
+
|
|
74
|
+
_register_bundled_skills(ctx)
|
|
75
|
+
logger.info(
|
|
76
|
+
"Hermes Tweet loaded with actions=%s",
|
|
77
|
+
os.getenv("HERMES_TWEET_ENABLE_ACTIONS", "false"),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["register"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
DEFAULT_LIMIT = 25
|
|
9
|
+
MAX_LIMIT = 100
|
|
10
|
+
PACKAGE_NAME = "hermes_tweet"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Endpoint:
|
|
15
|
+
category: str
|
|
16
|
+
free: bool
|
|
17
|
+
method: str
|
|
18
|
+
path: str
|
|
19
|
+
summary: str
|
|
20
|
+
parameters: tuple[dict[str, Any], ...] = ()
|
|
21
|
+
response_shape: str | None = None
|
|
22
|
+
mpp: dict[str, str] | None = None
|
|
23
|
+
action: bool = False
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict[str, Any]:
|
|
26
|
+
data: dict[str, Any] = {
|
|
27
|
+
"category": self.category,
|
|
28
|
+
"free": self.free,
|
|
29
|
+
"method": self.method,
|
|
30
|
+
"path": self.path,
|
|
31
|
+
"summary": self.summary,
|
|
32
|
+
"action": self.action,
|
|
33
|
+
}
|
|
34
|
+
if self.parameters:
|
|
35
|
+
data["parameters"] = list(self.parameters)
|
|
36
|
+
if self.response_shape:
|
|
37
|
+
data["responseShape"] = self.response_shape
|
|
38
|
+
if self.mpp:
|
|
39
|
+
data["mpp"] = self.mpp
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_raw() -> list[dict[str, Any]]:
|
|
44
|
+
text = files(PACKAGE_NAME).joinpath("catalog_data.json").read_text(encoding="utf-8")
|
|
45
|
+
data = cast("object", json.loads(text))
|
|
46
|
+
items = cast("list[object]", data if isinstance(data, list) else [])
|
|
47
|
+
return [cast("dict[str, Any]", item) for item in items if isinstance(item, dict)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _tuple_parameters(value: Any) -> tuple[dict[str, Any], ...]:
|
|
51
|
+
items = cast("list[object]", value if isinstance(value, list) else [])
|
|
52
|
+
return tuple(cast("dict[str, Any]", item) for item in items if isinstance(item, dict))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _endpoint(item: dict[str, Any]) -> Endpoint:
|
|
56
|
+
return Endpoint(
|
|
57
|
+
action=bool(item.get("action")),
|
|
58
|
+
category=str(item.get("category", "unknown")),
|
|
59
|
+
free=bool(item.get("free", False)),
|
|
60
|
+
method=str(item.get("method", "GET")).upper(),
|
|
61
|
+
mpp=item.get("mpp") if isinstance(item.get("mpp"), dict) else None,
|
|
62
|
+
parameters=_tuple_parameters(item.get("parameters")),
|
|
63
|
+
path=str(item.get("path", "")),
|
|
64
|
+
response_shape=str(item["responseShape"]) if item.get("responseShape") else None,
|
|
65
|
+
summary=str(item.get("summary", "")),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
ENDPOINTS: tuple[Endpoint, ...] = tuple(_endpoint(item) for item in _load_raw())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_method(method: str | None) -> str:
|
|
73
|
+
return (method or "GET").upper()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def normalize_limit(value: Any) -> int:
|
|
77
|
+
if not isinstance(value, int):
|
|
78
|
+
return DEFAULT_LIMIT
|
|
79
|
+
return min(max(value, 1), MAX_LIMIT)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _segments(path: str) -> list[str]:
|
|
83
|
+
normalized = path.removesuffix("/")
|
|
84
|
+
return normalized.split("/")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def matches_path(template: str, concrete: str) -> bool:
|
|
88
|
+
if template == concrete:
|
|
89
|
+
return True
|
|
90
|
+
template_segments = _segments(template)
|
|
91
|
+
concrete_segments = _segments(concrete)
|
|
92
|
+
if len(template_segments) != len(concrete_segments):
|
|
93
|
+
return False
|
|
94
|
+
for template_segment, concrete_segment in zip(
|
|
95
|
+
template_segments, concrete_segments, strict=True
|
|
96
|
+
):
|
|
97
|
+
if template_segment.startswith("{") and template_segment.endswith("}"):
|
|
98
|
+
if not concrete_segment:
|
|
99
|
+
return False
|
|
100
|
+
continue
|
|
101
|
+
if template_segment.startswith(":"):
|
|
102
|
+
if not concrete_segment:
|
|
103
|
+
return False
|
|
104
|
+
continue
|
|
105
|
+
if template_segment != concrete_segment:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def find_endpoint(method: str, path: str) -> Endpoint | None:
|
|
111
|
+
normalized = normalize_method(method)
|
|
112
|
+
for endpoint in ENDPOINTS:
|
|
113
|
+
if endpoint.method == normalized and matches_path(endpoint.path, path):
|
|
114
|
+
return endpoint
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _matches_query(endpoint: Endpoint, query: str) -> bool:
|
|
119
|
+
normalized = query.lower()
|
|
120
|
+
haystack = " ".join(
|
|
121
|
+
[
|
|
122
|
+
endpoint.category,
|
|
123
|
+
endpoint.method,
|
|
124
|
+
endpoint.path,
|
|
125
|
+
endpoint.summary,
|
|
126
|
+
endpoint.response_shape or "",
|
|
127
|
+
json.dumps(list(endpoint.parameters), ensure_ascii=False),
|
|
128
|
+
]
|
|
129
|
+
).lower()
|
|
130
|
+
return normalized in haystack
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def explore(args: dict[str, Any]) -> list[dict[str, Any]]:
|
|
134
|
+
method = normalize_method(args.get("method")) if args.get("method") else None
|
|
135
|
+
category = str(args.get("category", "")).strip().lower()
|
|
136
|
+
path = str(args.get("path", "")).strip()
|
|
137
|
+
query = str(args.get("query", "")).strip()
|
|
138
|
+
limit = normalize_limit(args.get("limit"))
|
|
139
|
+
include_actions = bool(args.get("include_actions", False))
|
|
140
|
+
|
|
141
|
+
endpoints = ENDPOINTS
|
|
142
|
+
if method:
|
|
143
|
+
endpoints = tuple(endpoint for endpoint in endpoints if endpoint.method == method)
|
|
144
|
+
if category:
|
|
145
|
+
endpoints = tuple(
|
|
146
|
+
endpoint for endpoint in endpoints if endpoint.category.lower() == category
|
|
147
|
+
)
|
|
148
|
+
if path:
|
|
149
|
+
endpoints = tuple(
|
|
150
|
+
endpoint
|
|
151
|
+
for endpoint in endpoints
|
|
152
|
+
if path in endpoint.path or matches_path(endpoint.path, path)
|
|
153
|
+
)
|
|
154
|
+
if query:
|
|
155
|
+
endpoints = tuple(endpoint for endpoint in endpoints if _matches_query(endpoint, query))
|
|
156
|
+
if "free" in args:
|
|
157
|
+
endpoints = tuple(endpoint for endpoint in endpoints if endpoint.free is bool(args["free"]))
|
|
158
|
+
if "mpp" in args:
|
|
159
|
+
endpoints = tuple(
|
|
160
|
+
endpoint for endpoint in endpoints if (endpoint.mpp is not None) is bool(args["mpp"])
|
|
161
|
+
)
|
|
162
|
+
if not include_actions:
|
|
163
|
+
endpoints = tuple(endpoint for endpoint in endpoints if not endpoint.action)
|
|
164
|
+
return [endpoint.to_dict() for endpoint in endpoints[:limit]]
|