@acedatacloud/skills 2026.713.2 → 2026.713.4
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.
- package/AGENTS.md +2 -1
- package/README.md +4 -2
- package/package.json +2 -1
- package/skills/_shared/mcp-servers.md +1 -0
- package/skills/happyhorse-video/SKILL.md +190 -0
- package/skills/tgstat/SKILL.md +23 -53
- package/skills/tgstat/scripts/tgstat.py +35 -126
package/AGENTS.md
CHANGED
|
@@ -22,6 +22,7 @@ Skills are located in the `skills/` directory (also mirrored to `.agents/skills/
|
|
|
22
22
|
- **veo-video** — Generate videos with Google Veo (native audio)
|
|
23
23
|
- **kling-video** — Generate videos with Kuaishou Kling (motion control)
|
|
24
24
|
- **hailuo-video** — Generate videos with Hailuo / MiniMax
|
|
25
|
+
- **happyhorse-video** — Generate and edit videos with Happy Horse
|
|
25
26
|
- **seedance-video** — Generate dance/motion videos with ByteDance Seedance
|
|
26
27
|
- **wan-video** — Generate videos with Alibaba Wan
|
|
27
28
|
- **maestro-video** — Produce complete videos from a brief with scripting, media, voiceover, editing, captions, and multilingual variants
|
|
@@ -29,7 +30,7 @@ Skills are located in the `skills/` directory (also mirrored to `.agents/skills/
|
|
|
29
30
|
### AI Chat & Tools
|
|
30
31
|
- **ai-chat** — Unified LLM gateway — GPT, Claude, Gemini, Kimi, Grok (50+ models)
|
|
31
32
|
- **google-search** — Search the web, images, news, maps, places, and videos via Google
|
|
32
|
-
- **tgstat** — Discover and analyze public Telegram channels/groups (
|
|
33
|
+
- **tgstat** — Discover and analyze public Telegram channels/groups (BYOC username; no login)
|
|
33
34
|
- **face-transform** — Face analysis, beautification, age/gender transform, swap, cartoon
|
|
34
35
|
- **short-url** — Create and manage short URLs
|
|
35
36
|
- **onepage-pdf** — Convert an HTML page into one tall single-page PDF (local; no API token; needs Python + pymupdf + Chrome/Edge)
|
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ Compatible with **30+ AI coding agents** via the [agentskills.io](https://agents
|
|
|
39
39
|
| [veo-video](skills/veo-video/) | Generate videos with Google Veo (native audio) |
|
|
40
40
|
| [kling-video](skills/kling-video/) | Generate videos with Kuaishou Kling (motion control) |
|
|
41
41
|
| [hailuo-video](skills/hailuo-video/) | Generate videos with Hailuo / MiniMax |
|
|
42
|
+
| [happyhorse-video](skills/happyhorse-video/) | Generate and edit videos with Happy Horse |
|
|
42
43
|
| [seedance-video](skills/seedance-video/) | Generate dance/motion videos with ByteDance Seedance |
|
|
43
44
|
| [maestro-video](skills/maestro-video/) | Produce complete videos from a brief with script, media, voiceover, captions, and editing |
|
|
44
45
|
|
|
@@ -48,7 +49,7 @@ Compatible with **30+ AI coding agents** via the [agentskills.io](https://agents
|
|
|
48
49
|
|-------|-------------|
|
|
49
50
|
| [ai-chat](skills/ai-chat/) | Unified LLM gateway — GPT, Claude, Gemini, Kimi, Grok (50+ models) |
|
|
50
51
|
| [google-search](skills/google-search/) | Search the web, images, news, maps, places, and videos via Google |
|
|
51
|
-
| [tgstat](skills/tgstat/) | Discover and analyze public Telegram channels/groups
|
|
52
|
+
| [tgstat](skills/tgstat/) | Discover and analyze public Telegram channels/groups using a connected username as the default target |
|
|
52
53
|
| [face-transform](skills/face-transform/) | Face analysis, beautification, age/gender transform, swap, cartoon |
|
|
53
54
|
| [short-url](skills/short-url/) | Create and manage short URLs |
|
|
54
55
|
| [onepage-pdf](skills/onepage-pdf/) | Convert an HTML page into one tall single-page PDF — no pagination breaks (local, no token) |
|
|
@@ -76,7 +77,7 @@ These skills drive third-party connectors users wire up at [auth.acedata.cloud/u
|
|
|
76
77
|
| [didi-ride](skills/didi-ride/) | Book DiDi rides, estimate fares, query/cancel orders, plan routes via the DiDi MCP | `didi` (BYOC) |
|
|
77
78
|
| [wecom](skills/wecom/) | WeCom (企业微信) self-built app — contacts, app messages, WeDoc, schedules, meetings | `wecom` (BYOC) |
|
|
78
79
|
| [tencent-docs](skills/tencent-docs/) | Create / read / list / search / manage Tencent Docs — docs, sheets, slides, mind maps, flowcharts | `tencentdocs` (BYOC) |
|
|
79
|
-
| [tgstat](skills/tgstat/) | Public Telegram source discovery, rankings, and audience research | `tgstat` (
|
|
80
|
+
| [tgstat](skills/tgstat/) | Public Telegram source discovery, rankings, and audience research | `tgstat` (BYOC username) |
|
|
80
81
|
|
|
81
82
|
## Prerequisites
|
|
82
83
|
|
|
@@ -211,6 +212,7 @@ Skills provide **knowledge** (when to use, parameters, gotchas). MCP servers pro
|
|
|
211
212
|
| veo-video | [mcp-veo](https://pypi.org/project/mcp-veo/) | `pip install mcp-veo` | `https://veo.mcp.acedata.cloud/mcp` |
|
|
212
213
|
| seedream-image | [mcp-seedream](https://pypi.org/project/mcp-seedream/) | `pip install mcp-seedream` | `https://seedream.mcp.acedata.cloud/mcp` |
|
|
213
214
|
| seedance-video | [mcp-seedance](https://pypi.org/project/mcp-seedance/) | `pip install mcp-seedance` | `https://seedance.mcp.acedata.cloud/mcp` |
|
|
215
|
+
| happyhorse-video | [mcp-happyhorse](https://pypi.org/project/mcp-happyhorse/) | `pip install mcp-happyhorse` | `https://happyhorse.mcp.acedata.cloud/mcp` |
|
|
214
216
|
| maestro-video | [mcp-maestro](https://pypi.org/project/mcp-maestro/) | `pip install mcp-maestro` | `https://maestro.mcp.acedata.cloud/mcp` |
|
|
215
217
|
| nano-banana-image | [mcp-nano-banana](https://pypi.org/project/mcp-nano-banana/) | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
216
218
|
| short-url | [mcp-shorturl](https://pypi.org/project/mcp-shorturl/) | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acedatacloud/skills",
|
|
3
|
-
"version": "2026.713.
|
|
3
|
+
"version": "2026.713.4",
|
|
4
4
|
"description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"luma",
|
|
17
17
|
"sora",
|
|
18
18
|
"veo",
|
|
19
|
+
"happyhorse",
|
|
19
20
|
"ai-music",
|
|
20
21
|
"ai-image",
|
|
21
22
|
"ai-video",
|
|
@@ -15,6 +15,7 @@ Each AceDataCloud service has a corresponding MCP server that provides tool-use
|
|
|
15
15
|
| veo-video | `pip install mcp-veo` | `https://veo.mcp.acedata.cloud/mcp` |
|
|
16
16
|
| seedream-image | `pip install mcp-seedream` | `https://seedream.mcp.acedata.cloud/mcp` |
|
|
17
17
|
| seedance-video | `pip install mcp-seedance` | `https://seedance.mcp.acedata.cloud/mcp` |
|
|
18
|
+
| happyhorse-video | `pip install mcp-happyhorse` | `https://happyhorse.mcp.acedata.cloud/mcp` |
|
|
18
19
|
| maestro-video | `pip install mcp-maestro` | `https://maestro.mcp.acedata.cloud/mcp` |
|
|
19
20
|
| nano-banana-image | `pip install mcp-nano-banana` | `https://nano-banana.mcp.acedata.cloud/mcp` |
|
|
20
21
|
| short-url | `pip install mcp-shorturl` | `https://short-url.mcp.acedata.cloud/mcp` |
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: happyhorse-video
|
|
3
|
+
description: Generate and edit AI videos with Happy Horse via AceDataCloud API. Use when creating videos from text prompts, animating a first-frame image, generating scenes from up to 9 subject or style reference images, or editing an existing video with optional references. Supports 720P/1080P output, 3-15 second generation, aspect-ratio control, audio preservation, seeds, callbacks, and asynchronous task polling.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: acedatacloud
|
|
7
|
+
version: "1.0"
|
|
8
|
+
compatibility: Requires ACEDATACLOUD_API_TOKEN in .env file (see _shared/authentication.md). Optionally pair with mcp-happyhorse for tool-use.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Happy Horse Video Generation and Editing
|
|
12
|
+
|
|
13
|
+
Use Happy Horse through AceDataCloud for text-to-video, first-frame animation, reference-guided
|
|
14
|
+
generation, and video editing.
|
|
15
|
+
|
|
16
|
+
> **Setup:** See [authentication](../_shared/authentication.md) for token setup.
|
|
17
|
+
|
|
18
|
+
## Choose an Action
|
|
19
|
+
|
|
20
|
+
| Goal | `action` | Valid models | Required input |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| Generate from text | `generate` | `happyhorse-1.0-t2v`, `happyhorse-1.1-t2v` | `prompt` |
|
|
23
|
+
| Animate one image | `image_to_video` | `happyhorse-1.0-i2v`, `happyhorse-1.1-i2v` | `image_url` |
|
|
24
|
+
| Generate from references | `reference_to_video` | `happyhorse-1.0-r2v`, `happyhorse-1.1-r2v` | `prompt`, 1-9 `image_urls` |
|
|
25
|
+
| Edit a video | `video_edit` | `happyhorse-1.0-video-edit` | `prompt`, `video_url` |
|
|
26
|
+
|
|
27
|
+
The 1.1 model is the default where available. Video editing currently has only a 1.0 model.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Submit text-to-video asynchronously:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
curl -X POST https://api.acedata.cloud/happyhorse/videos \
|
|
35
|
+
-H "Authorization: Bearer $ACEDATACLOUD_API_TOKEN" \
|
|
36
|
+
-H "Content-Type: application/json" \
|
|
37
|
+
-d '{
|
|
38
|
+
"action": "generate",
|
|
39
|
+
"model": "happyhorse-1.1-t2v",
|
|
40
|
+
"prompt": "A cinematic white horse crossing a snowy ridge at sunrise, wind moving its mane, slow camera push",
|
|
41
|
+
"resolution": "720P",
|
|
42
|
+
"ratio": "16:9",
|
|
43
|
+
"duration": 5,
|
|
44
|
+
"async": true
|
|
45
|
+
}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The response contains a `task_id`. Poll it after about 15 seconds:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
curl -X POST https://api.acedata.cloud/happyhorse/tasks \
|
|
52
|
+
-H "Authorization: Bearer $ACEDATACLOUD_API_TOKEN" \
|
|
53
|
+
-H "Content-Type: application/json" \
|
|
54
|
+
-d '{"id": "<task_id>", "action": "retrieve"}'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Async:** See [async task polling](../_shared/async-tasks.md). Continue until
|
|
58
|
+
> `response.data[].video_url` appears or a terminal error is returned.
|
|
59
|
+
|
|
60
|
+
## Workflows
|
|
61
|
+
|
|
62
|
+
### Text-to-Video
|
|
63
|
+
|
|
64
|
+
Use `generate` when no visual input exists. Describe the subject, action, environment, camera,
|
|
65
|
+
lighting, and style.
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
POST /happyhorse/videos
|
|
69
|
+
{
|
|
70
|
+
"action": "generate",
|
|
71
|
+
"model": "happyhorse-1.1-t2v",
|
|
72
|
+
"prompt": "A white horse gallops across a moonlit beach, side tracking shot, silver reflections, cinematic realism",
|
|
73
|
+
"resolution": "1080P",
|
|
74
|
+
"ratio": "16:9",
|
|
75
|
+
"duration": 8,
|
|
76
|
+
"seed": 42
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### First-Frame Image-to-Video
|
|
81
|
+
|
|
82
|
+
Use `image_to_video` to animate one image. `prompt` is optional but useful for motion and camera
|
|
83
|
+
direction. The output ratio follows the first-frame image, so omit `ratio`.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
POST /happyhorse/videos
|
|
87
|
+
{
|
|
88
|
+
"action": "image_to_video",
|
|
89
|
+
"model": "happyhorse-1.1-i2v",
|
|
90
|
+
"image_url": "https://example.com/horse.jpg",
|
|
91
|
+
"prompt": "The horse slowly lifts its head as the mane moves in the breeze, gentle camera push",
|
|
92
|
+
"resolution": "1080P",
|
|
93
|
+
"duration": 5
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Reference-to-Video
|
|
98
|
+
|
|
99
|
+
Use `reference_to_video` with 1-9 reference images. Mention images as `character1`, `character2`,
|
|
100
|
+
and so on in list order when the prompt needs to distinguish them.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
POST /happyhorse/videos
|
|
104
|
+
{
|
|
105
|
+
"action": "reference_to_video",
|
|
106
|
+
"model": "happyhorse-1.1-r2v",
|
|
107
|
+
"prompt": "character1 walks through a sunrise meadow wearing the leather and gold style from character2",
|
|
108
|
+
"image_urls": [
|
|
109
|
+
"https://example.com/subject.jpg",
|
|
110
|
+
"https://example.com/style.jpg"
|
|
111
|
+
],
|
|
112
|
+
"resolution": "720P",
|
|
113
|
+
"ratio": "16:9",
|
|
114
|
+
"duration": 5
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Video Editing
|
|
119
|
+
|
|
120
|
+
Use `video_edit` with an existing video and editing instructions. Add at most 5 reference images
|
|
121
|
+
for wardrobe, subject, style, or local replacement guidance. Use `audio_setting: "origin"` to
|
|
122
|
+
preserve the source audio.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
POST /happyhorse/videos
|
|
126
|
+
{
|
|
127
|
+
"action": "video_edit",
|
|
128
|
+
"model": "happyhorse-1.0-video-edit",
|
|
129
|
+
"prompt": "Apply the warm leather and gold style from the reference while preserving camera motion",
|
|
130
|
+
"video_url": "https://example.com/source.mp4",
|
|
131
|
+
"image_urls": ["https://example.com/style.jpg"],
|
|
132
|
+
"resolution": "720P",
|
|
133
|
+
"audio_setting": "origin"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Do not send `duration` or `ratio` for video editing. Output duration follows the source video.
|
|
138
|
+
|
|
139
|
+
## Parameters
|
|
140
|
+
|
|
141
|
+
| Parameter | Values | Notes |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `action` | `generate`, `image_to_video`, `reference_to_video`, `video_edit` | Defaults to `generate` |
|
|
144
|
+
| `model` | See action table | The model family must match the action |
|
|
145
|
+
| `prompt` | string | Required except for `image_to_video` |
|
|
146
|
+
| `image_url` | URL | First frame for `image_to_video` |
|
|
147
|
+
| `image_urls` | URL array | 1-9 for reference generation; 0-5 for editing |
|
|
148
|
+
| `video_url` | URL | Required only for editing |
|
|
149
|
+
| `resolution` | `720P`, `1080P` | Uppercase `P` is required; default `1080P` |
|
|
150
|
+
| `ratio` | `16:9`, `9:16`, `1:1`, `4:3`, `3:4` | Text/reference generation only |
|
|
151
|
+
| `duration` | integer 3-15 | Generation only; default 5 seconds |
|
|
152
|
+
| `watermark` | boolean | Default `false` |
|
|
153
|
+
| `audio_setting` | `auto`, `origin` | Video editing only; `origin` preserves source audio |
|
|
154
|
+
| `seed` | integer 0-2147483647 | Optional reproducibility seed |
|
|
155
|
+
| `callback_url` | URL | Webhook for final result; returns immediately |
|
|
156
|
+
| `async` | boolean | Set `true` to return a task ID for polling |
|
|
157
|
+
|
|
158
|
+
## Task Queries
|
|
159
|
+
|
|
160
|
+
Retrieve one task:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
POST /happyhorse/tasks
|
|
164
|
+
{"id": "<task_id>", "action": "retrieve"}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Retrieve several tasks:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
POST /happyhorse/tasks
|
|
171
|
+
{"ids": ["<task_id_1>", "<task_id_2>"], "action": "retrieve_batch"}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A completed single-task response stores the original request in `request` and the final API result
|
|
175
|
+
in `response`. Read the media URL from `response.data[].video_url`.
|
|
176
|
+
|
|
177
|
+
## Gotchas
|
|
178
|
+
|
|
179
|
+
- Match the model family to `action`; for example, an `*-i2v` model is invalid for `generate`
|
|
180
|
+
- Resolution values are `720P` and `1080P`, not lowercase `720p` / `1080p`
|
|
181
|
+
- Generated duration must be an integer from 3 through 15 seconds
|
|
182
|
+
- `image_to_video` requires `image_url`; its prompt is optional and its ratio follows the image
|
|
183
|
+
- `reference_to_video` requires a prompt and 1-9 images
|
|
184
|
+
- `video_edit` requires both prompt and video URL, accepts at most 5 references, and ignores duration/ratio
|
|
185
|
+
- `audio_setting` applies to video editing; use `origin` to preserve source audio
|
|
186
|
+
- A `task_id` is not a completed result; poll until a final video URL or terminal error appears
|
|
187
|
+
- Failed tasks are not billed; video edits use the upstream-reported input plus output duration for billing
|
|
188
|
+
|
|
189
|
+
> **MCP:** `pip install mcp-happyhorse` | Hosted:
|
|
190
|
+
> `https://happyhorse.mcp.acedata.cloud/mcp` | See [all MCP servers](../_shared/mcp-servers.md)
|
package/skills/tgstat/SKILL.md
CHANGED
|
@@ -1,44 +1,35 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tgstat
|
|
3
|
-
description: "Research public Telegram channels and groups with TGStat. Use when discovering communities by topic, comparing audience
|
|
3
|
+
description: "Research public Telegram channels and groups with TGStat. Use when discovering communities by topic, comparing public audience/reach/activity, checking a known @username, or shortlisting ad and outreach sources. The connector supplies TGSTAT_USERNAME as the default profile. Read-only: does not log in, join groups, scrape members, or send messages."
|
|
4
4
|
when_to_use: |
|
|
5
5
|
Use for Telegram source discovery, competitor research, ad-channel
|
|
6
6
|
selection, and public audience analysis. It can find channels/chats by
|
|
7
|
-
keyword, inspect known public usernames, compare
|
|
8
|
-
|
|
9
|
-
reading or sending messages in the user's
|
|
7
|
+
keyword, inspect known public usernames, compare visible metrics, and use
|
|
8
|
+
the connected Telegram username as the default target. Use the separate
|
|
9
|
+
telegram connector for reading or sending messages in the user's account.
|
|
10
10
|
connections: [tgstat]
|
|
11
11
|
allowed_tools: [Bash, web_search, web_fetch]
|
|
12
12
|
license: Apache-2.0
|
|
13
13
|
metadata:
|
|
14
14
|
author: acedatacloud
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.1"
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# TGStat Research
|
|
19
19
|
|
|
20
|
-
Use [scripts/tgstat.py](./scripts/tgstat.py) for public TGStat
|
|
21
|
-
|
|
20
|
+
Use [scripts/tgstat.py](./scripts/tgstat.py) for public TGStat research. It uses
|
|
21
|
+
only the Python standard library. Resolve the script at the start of every Bash
|
|
22
|
+
call because each call runs in a fresh shell:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
TGSTAT="$SKILL_DIR/scripts/tgstat.py"; [ -f "$TGSTAT" ] || TGSTAT=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/tgstat.py' 2>/dev/null | head -1)
|
|
25
26
|
[ -f "$TGSTAT" ] || { echo "tgstat script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
26
|
-
python3 "$TGSTAT"
|
|
27
|
+
python3 "$TGSTAT" profile
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
The connector
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
for discovery and public TGStat ranking/entity pages for verification.
|
|
33
|
-
- **TGStat API Token (optional):** when the connector injects
|
|
34
|
-
`$TGSTAT_TOKEN`, the same commands automatically use the official Stat API.
|
|
35
|
-
|
|
36
|
-
Commands default to `--access-mode auto`. Use `--access-mode public` before the
|
|
37
|
-
subcommand when a configured Token lacks access to a paid API method; use
|
|
38
|
-
`--access-mode api` to require API mode and fail clearly if no Token exists.
|
|
39
|
-
|
|
40
|
-
Never ask the user to paste a token into chat or pass it on the command line.
|
|
41
|
-
If they want API mode, ask them to add the Token through the TGStat connector.
|
|
30
|
+
The connector injects `$TGSTAT_USERNAME`. It is not a login credential; it is
|
|
31
|
+
only the default public channel/group target. Never print the full environment
|
|
32
|
+
or treat the username as proof that the user owns the Telegram account.
|
|
42
33
|
|
|
43
34
|
## Discover Sources by Topic
|
|
44
35
|
|
|
@@ -47,8 +38,7 @@ Run `search` first:
|
|
|
47
38
|
```bash
|
|
48
39
|
TGSTAT="$SKILL_DIR/scripts/tgstat.py"; [ -f "$TGSTAT" ] || TGSTAT=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/tgstat.py' 2>/dev/null | head -1)
|
|
49
40
|
[ -f "$TGSTAT" ] || { echo "tgstat script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
50
|
-
python3 "$TGSTAT" search "Claude API" --type all --language
|
|
51
|
-
python3 "$TGSTAT" --access-mode api search --category technology --type channel
|
|
41
|
+
python3 "$TGSTAT" search "Claude API" --type all --language English --country US
|
|
52
42
|
```
|
|
53
43
|
|
|
54
44
|
In public mode the command returns `web_queries`. Call `web_search` once per
|
|
@@ -65,20 +55,8 @@ TGStat's own keyword-search result endpoint requires sign-in. Do not try to
|
|
|
65
55
|
bypass it, replay private AJAX endpoints, or claim public mode searches the
|
|
66
56
|
full TGStat index.
|
|
67
57
|
|
|
68
|
-
|
|
69
|
-
TGStat
|
|
70
|
-
keyword in public mode instead of guessing another category.
|
|
71
|
-
|
|
72
|
-
With `$TGSTAT_TOKEN`, `search` calls the official `channels/search` endpoint
|
|
73
|
-
instead. That endpoint may require a paid Stat API plan. `--language` must be a
|
|
74
|
-
TGStat language key such as `english` or `russian`; `--country` must be a
|
|
75
|
-
two-letter country code such as `us` or `ru`.
|
|
76
|
-
|
|
77
|
-
If API search reports plan access denied, rerun explicitly in public mode:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
python3 "$TGSTAT" --access-mode public search "Claude API" --type all
|
|
81
|
-
```
|
|
58
|
+
`--language` and `--country` are search terms for the web index, not guaranteed
|
|
59
|
+
TGStat database filters.
|
|
82
60
|
|
|
83
61
|
## Browse Public Rankings
|
|
84
62
|
|
|
@@ -104,12 +82,13 @@ results as web-index discoveries, not as an authoritative TGStat rank.
|
|
|
104
82
|
|
|
105
83
|
## Inspect a Known Channel or Group
|
|
106
84
|
|
|
107
|
-
Accept
|
|
108
|
-
|
|
85
|
+
Accept a public `@username`, bare username, `t.me/<username>` link, or a TGStat
|
|
86
|
+
entity URL. Omit the target to inspect `$TGSTAT_USERNAME` from the connector:
|
|
109
87
|
|
|
110
88
|
```bash
|
|
111
89
|
TGSTAT="$SKILL_DIR/scripts/tgstat.py"; [ -f "$TGSTAT" ] || TGSTAT=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/tgstat.py' 2>/dev/null | head -1)
|
|
112
90
|
[ -f "$TGSTAT" ] || { echo "tgstat script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
91
|
+
python3 "$TGSTAT" info
|
|
113
92
|
python3 "$TGSTAT" info @durov
|
|
114
93
|
python3 "$TGSTAT" stat https://t.me/example_public_chat
|
|
115
94
|
```
|
|
@@ -119,28 +98,20 @@ returns public metadata, and enriches it with ranking metrics when the entity
|
|
|
119
98
|
appears in the current public ranking. Empty metrics mean TGStat did not expose
|
|
120
99
|
them publicly; do not call that full statistics.
|
|
121
100
|
|
|
122
|
-
With `$TGSTAT_TOKEN`:
|
|
123
|
-
|
|
124
|
-
- `info` uses `channels/get` for structured identity/category metadata.
|
|
125
|
-
- `stat` uses `channels/stat` for fuller channel reach/ER or chat activity.
|
|
126
|
-
|
|
127
101
|
For ad selection, rank by relevant reach/activity rather than subscriber count
|
|
128
102
|
alone. High subscribers with weak reach or chat MAU can indicate an inactive or
|
|
129
103
|
inflated audience. Present metrics as evidence, not a guarantee of lead quality.
|
|
130
104
|
|
|
131
|
-
## Check
|
|
105
|
+
## Check the Connected Profile
|
|
132
106
|
|
|
133
107
|
```bash
|
|
134
108
|
TGSTAT="$SKILL_DIR/scripts/tgstat.py"; [ -f "$TGSTAT" ] || TGSTAT=$(find /tmp -maxdepth 8 -path '*/skills/*/scripts/tgstat.py' 2>/dev/null | head -1)
|
|
135
109
|
[ -f "$TGSTAT" ] || { echo "tgstat script not found (SKILL_DIR=$SKILL_DIR)" >&2; exit 1; }
|
|
136
|
-
python3 "$TGSTAT"
|
|
137
|
-
python3 "$TGSTAT" quota
|
|
110
|
+
python3 "$TGSTAT" profile
|
|
138
111
|
```
|
|
139
112
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
public workflow still works and that API search/full stats depend on the user's
|
|
143
|
-
TGStat plan.
|
|
113
|
+
`profile` reports the normalized public username used by target-less `info` and
|
|
114
|
+
`stat`. It does not verify ownership or authenticate to Telegram/TGStat.
|
|
144
115
|
|
|
145
116
|
## Outreach Research Workflow
|
|
146
117
|
|
|
@@ -162,5 +133,4 @@ For prospecting or partnership research:
|
|
|
162
133
|
- Public pages and HTML can change; if parsing fails, use `web_fetch` for a
|
|
163
134
|
single public page and report only values visible in that page.
|
|
164
135
|
- Public web discovery is incomplete and regional TGStat pages may differ.
|
|
165
|
-
-
|
|
166
|
-
containing secrets.
|
|
136
|
+
- Treat `$TGSTAT_USERNAME` as public profile context, not authentication.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""TGStat research CLI with a
|
|
2
|
+
"""Public TGStat research CLI with a connected Telegram username."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -15,7 +15,6 @@ from html.parser import HTMLParser
|
|
|
15
15
|
from typing import Dict, List, Optional, Tuple, Union
|
|
16
16
|
|
|
17
17
|
DEFAULT_PUBLIC_BASE = "https://tgstat.com"
|
|
18
|
-
DEFAULT_API_BASE = "https://api.tgstat.ru"
|
|
19
18
|
USER_AGENT = (
|
|
20
19
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
21
20
|
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36"
|
|
@@ -24,7 +23,6 @@ PUBLIC_USERNAME_RE = re.compile(r"[A-Za-z0-9_]{3,}")
|
|
|
24
23
|
TGSTAT_HOST_RE = re.compile(r"^(?:[a-z]{2,3}\.)?tgstat\.com$", re.IGNORECASE)
|
|
25
24
|
TGSTAT_INPUT_HOST_RE = re.compile(r"^(?:(?:[a-z]{2,3}\.)?tgstat\.com|tgstat\.ru)$", re.IGNORECASE)
|
|
26
25
|
ENTITY_PATH_RE = re.compile(r"/(channel|chat)/(@[A-Za-z0-9_]{3,}|id\d+)/stat/?", re.IGNORECASE)
|
|
27
|
-
API_HOST = urllib.parse.urlparse(DEFAULT_API_BASE).hostname or ""
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
class TGStatError(RuntimeError):
|
|
@@ -66,12 +64,8 @@ def _safe_url(url: str) -> str:
|
|
|
66
64
|
return urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
|
67
65
|
|
|
68
66
|
|
|
69
|
-
def _safe_error_body(body: str
|
|
70
|
-
|
|
71
|
-
token = urllib.parse.parse_qs(urllib.parse.urlsplit(url).query).get("token", [""])[0]
|
|
72
|
-
if token:
|
|
73
|
-
compact = compact.replace(token, "[REDACTED]")
|
|
74
|
-
return compact[:200]
|
|
67
|
+
def _safe_error_body(body: str) -> str:
|
|
68
|
+
return _compact(body)[:200]
|
|
75
69
|
|
|
76
70
|
|
|
77
71
|
def _validate_request_url(url: str, initial_host: Optional[str] = None) -> None:
|
|
@@ -83,9 +77,6 @@ def _validate_request_url(url: str, initial_host: Optional[str] = None) -> None:
|
|
|
83
77
|
if TGSTAT_HOST_RE.fullmatch(allowed_from):
|
|
84
78
|
if not TGSTAT_HOST_RE.fullmatch(host):
|
|
85
79
|
raise TGStatError(f"TGStat redirected to an unexpected host: {host}")
|
|
86
|
-
elif allowed_from == API_HOST:
|
|
87
|
-
if host != API_HOST:
|
|
88
|
-
raise TGStatError(f"TGStat API redirected to an unexpected host: {host}")
|
|
89
80
|
else:
|
|
90
81
|
raise TGStatError(f"unsupported TGStat request host: {allowed_from}")
|
|
91
82
|
|
|
@@ -325,9 +316,9 @@ def _request_with_url(
|
|
|
325
316
|
return response.read().decode("utf-8", "replace"), response.geturl()
|
|
326
317
|
except urllib.error.HTTPError as exc:
|
|
327
318
|
body = exc.read().decode("utf-8", "replace")
|
|
328
|
-
raise TGStatError(f"HTTP {exc.code} from {_safe_url(url)}: {_safe_error_body(body
|
|
319
|
+
raise TGStatError(f"HTTP {exc.code} from {_safe_url(url)}: {_safe_error_body(body)}") from exc
|
|
329
320
|
except urllib.error.URLError as exc:
|
|
330
|
-
raise TGStatError(f"network error for {_safe_url(url)}: {_safe_error_body(str(exc.reason)
|
|
321
|
+
raise TGStatError(f"network error for {_safe_url(url)}: {_safe_error_body(str(exc.reason))}") from exc
|
|
331
322
|
|
|
332
323
|
|
|
333
324
|
def _request(url: str, timeout: int, data: Optional[bytes] = None, headers: Optional[dict] = None) -> str:
|
|
@@ -341,30 +332,7 @@ def _public_base(value: str) -> str:
|
|
|
341
332
|
return f"https://{parsed.hostname}"
|
|
342
333
|
|
|
343
334
|
|
|
344
|
-
def _api_get(args: argparse.Namespace, path: str, params: Optional[dict] = None) -> dict:
|
|
345
|
-
token = os.environ.get("TGSTAT_TOKEN", "")
|
|
346
|
-
if not token:
|
|
347
|
-
raise TGStatError("TGSTAT_TOKEN is required for this API-only operation")
|
|
348
|
-
query = {"token": token, **{key: value for key, value in (params or {}).items() if value not in (None, "")}}
|
|
349
|
-
url = f"{DEFAULT_API_BASE}/{path.lstrip('/')}?{urllib.parse.urlencode(query)}"
|
|
350
|
-
raw = _request(url, args.timeout)
|
|
351
|
-
try:
|
|
352
|
-
payload = json.loads(raw)
|
|
353
|
-
except json.JSONDecodeError as exc:
|
|
354
|
-
raise TGStatError("TGStat API returned a non-JSON response") from exc
|
|
355
|
-
if payload.get("status") != "ok":
|
|
356
|
-
error = str(payload.get("error") or "TGStat API request failed").replace(token, "[REDACTED]")
|
|
357
|
-
raise TGStatError(error)
|
|
358
|
-
return payload
|
|
359
|
-
|
|
360
|
-
|
|
361
335
|
def _normalize_target(target: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
362
|
-
return _normalize_target_for_mode(target, for_api=False)
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _normalize_target_for_mode(
|
|
366
|
-
target: str, *, for_api: bool
|
|
367
|
-
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
368
336
|
target = target.strip()
|
|
369
337
|
if target.startswith("http://") or target.startswith("https://"):
|
|
370
338
|
parsed = urllib.parse.urlparse(target)
|
|
@@ -375,8 +343,6 @@ def _normalize_target_for_mode(
|
|
|
375
343
|
if not entity:
|
|
376
344
|
raise TGStatError("TGStat target must be a channel or chat statistics URL")
|
|
377
345
|
identifier = entity[1]
|
|
378
|
-
if for_api and identifier.lower().startswith("id"):
|
|
379
|
-
identifier = identifier[2:]
|
|
380
346
|
canonical_url = target
|
|
381
347
|
if parsed.hostname.casefold() == "tgstat.ru":
|
|
382
348
|
canonical_url = urllib.parse.urlunsplit(("https", "tgstat.com", parsed.path, "", ""))
|
|
@@ -390,47 +356,40 @@ def _normalize_target_for_mode(
|
|
|
390
356
|
if target.startswith("@") and PUBLIC_USERNAME_RE.fullmatch(target[1:]):
|
|
391
357
|
return target, None, None
|
|
392
358
|
if re.fullmatch(r"\d+", target):
|
|
393
|
-
return
|
|
359
|
+
return f"id{target}", None, None
|
|
394
360
|
if re.fullmatch(r"id\d+", target, re.IGNORECASE):
|
|
395
|
-
|
|
396
|
-
return (digits if for_api else f"id{digits}"), None, None
|
|
361
|
+
return f"id{target[2:]}", None, None
|
|
397
362
|
if PUBLIC_USERNAME_RE.fullmatch(target):
|
|
398
363
|
return f"@{target}", None, None
|
|
399
364
|
raise TGStatError("target must be @username, a t.me link, TGStat entity URL, or id<number>")
|
|
400
365
|
|
|
401
366
|
|
|
402
|
-
def
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if
|
|
408
|
-
raise TGStatError("
|
|
409
|
-
return
|
|
367
|
+
def _connected_username() -> str:
|
|
368
|
+
value = os.environ.get("TGSTAT_USERNAME", "").strip()
|
|
369
|
+
if not value:
|
|
370
|
+
raise TGStatError("TGSTAT_USERNAME is not configured; reconnect TGStat with a public Telegram username")
|
|
371
|
+
identifier, _, _ = _normalize_target(value)
|
|
372
|
+
if not identifier or not identifier.startswith("@"):
|
|
373
|
+
raise TGStatError("TGSTAT_USERNAME must be a public Telegram username such as @durov")
|
|
374
|
+
return identifier
|
|
410
375
|
|
|
411
376
|
|
|
412
|
-
def
|
|
413
|
-
|
|
414
|
-
|
|
377
|
+
def _target_or_profile(target: str) -> str:
|
|
378
|
+
return target.strip() or _connected_username()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def command_profile(args: argparse.Namespace) -> None:
|
|
382
|
+
username = _connected_username()
|
|
415
383
|
_json_out(
|
|
416
384
|
{
|
|
417
|
-
"mode":
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"api_capabilities": ["quota", "structured channel/chat search", "full channel statistics"],
|
|
385
|
+
"mode": "public",
|
|
386
|
+
"username": username,
|
|
387
|
+
"telegram_url": f"https://t.me/{username[1:]}",
|
|
388
|
+
"verified": False,
|
|
422
389
|
}
|
|
423
390
|
)
|
|
424
391
|
|
|
425
392
|
|
|
426
|
-
def command_quota(args: argparse.Namespace) -> None:
|
|
427
|
-
if not _use_api(args):
|
|
428
|
-
_json_out({"mode": "public", "token_configured": False, "quota": None})
|
|
429
|
-
return
|
|
430
|
-
payload = _api_get(args, "usage/stat")
|
|
431
|
-
_json_out({"mode": "api", "quota": payload.get("response")})
|
|
432
|
-
|
|
433
|
-
|
|
434
393
|
def _web_queries(query: str, peer_type: str, language: str, country: str) -> List[str]:
|
|
435
394
|
suffix = " ".join(part for part in (language, country) if part).strip()
|
|
436
395
|
quoted = f'"{query}"'
|
|
@@ -440,54 +399,16 @@ def _web_queries(query: str, peer_type: str, language: str, country: str) -> Lis
|
|
|
440
399
|
return queries
|
|
441
400
|
|
|
442
401
|
|
|
443
|
-
def _use_api(args: argparse.Namespace) -> bool:
|
|
444
|
-
has_token = bool(os.environ.get("TGSTAT_TOKEN"))
|
|
445
|
-
if args.access_mode == "public":
|
|
446
|
-
return False
|
|
447
|
-
if args.access_mode == "api" and not has_token:
|
|
448
|
-
raise TGStatError("TGSTAT_TOKEN is required when --access-mode api is selected")
|
|
449
|
-
return has_token
|
|
450
|
-
|
|
451
|
-
|
|
452
402
|
def command_search(args: argparse.Namespace) -> None:
|
|
453
403
|
query = args.query.strip()
|
|
454
|
-
|
|
455
|
-
if not query and not category:
|
|
456
|
-
raise TGStatError("search requires a query or --category")
|
|
457
|
-
if query and len(query) < 3:
|
|
404
|
+
if len(query) < 3:
|
|
458
405
|
raise TGStatError("query must contain at least 3 characters")
|
|
459
|
-
if _use_api(args):
|
|
460
|
-
language, country = _normalize_api_filters(args.language, args.country)
|
|
461
|
-
payload = _api_get(
|
|
462
|
-
args,
|
|
463
|
-
"channels/search",
|
|
464
|
-
{
|
|
465
|
-
"q": query,
|
|
466
|
-
"category": category,
|
|
467
|
-
"peer_type": args.type,
|
|
468
|
-
"language": language,
|
|
469
|
-
"country": country,
|
|
470
|
-
"search_by_description": 1 if args.description else 0,
|
|
471
|
-
"limit": min(max(args.limit, 1), 100),
|
|
472
|
-
},
|
|
473
|
-
)
|
|
474
|
-
_json_out(
|
|
475
|
-
{
|
|
476
|
-
"mode": "api",
|
|
477
|
-
"query": query or None,
|
|
478
|
-
"category": category or None,
|
|
479
|
-
"response": payload.get("response"),
|
|
480
|
-
}
|
|
481
|
-
)
|
|
482
|
-
return
|
|
483
|
-
discovery_term = query or category
|
|
484
406
|
_json_out(
|
|
485
407
|
{
|
|
486
408
|
"mode": "public",
|
|
487
|
-
"query": query
|
|
488
|
-
"category": category or None,
|
|
409
|
+
"query": query,
|
|
489
410
|
"requires_web_search": True,
|
|
490
|
-
"web_queries": _web_queries(
|
|
411
|
+
"web_queries": _web_queries(query, args.type, args.language, args.country),
|
|
491
412
|
"instructions": (
|
|
492
413
|
"Run web_search for each query, keep only tgstat.com and t.me results, "
|
|
493
414
|
"deduplicate by username, then inspect shortlisted TGStat URLs with the info command."
|
|
@@ -588,41 +509,29 @@ def _public_info(args: argparse.Namespace, target: str, peer_type: str) -> dict:
|
|
|
588
509
|
|
|
589
510
|
|
|
590
511
|
def command_info(args: argparse.Namespace) -> None:
|
|
591
|
-
|
|
592
|
-
identifier, _, _ = _normalize_target_for_mode(args.target, for_api=True)
|
|
593
|
-
payload = _api_get(args, "channels/get", {"channelId": identifier})
|
|
594
|
-
_json_out({"mode": "api", "response": payload.get("response")})
|
|
595
|
-
return
|
|
596
|
-
_json_out({"mode": "public", **_public_info(args, args.target, args.type)})
|
|
512
|
+
_json_out({"mode": "public", **_public_info(args, _target_or_profile(args.target), args.type)})
|
|
597
513
|
|
|
598
514
|
|
|
599
515
|
def command_stat(args: argparse.Namespace) -> None:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
_json_out({"mode": "api", "response": payload.get("response")})
|
|
604
|
-
return
|
|
605
|
-
_json_out({"mode": "public", "limited_metrics": True, **_public_info(args, args.target, args.type)})
|
|
516
|
+
_json_out(
|
|
517
|
+
{"mode": "public", "limited_metrics": True, **_public_info(args, _target_or_profile(args.target), args.type)}
|
|
518
|
+
)
|
|
606
519
|
|
|
607
520
|
|
|
608
521
|
def build_parser() -> argparse.ArgumentParser:
|
|
609
522
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
610
|
-
parser.add_argument("--access-mode", choices=("auto", "public", "api"), default="auto")
|
|
611
523
|
parser.add_argument("--host", default=os.environ.get("TGSTAT_PUBLIC_HOST", DEFAULT_PUBLIC_BASE))
|
|
612
524
|
parser.add_argument("--timeout", type=int, default=30)
|
|
613
525
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
614
526
|
|
|
615
|
-
sub.add_parser("
|
|
616
|
-
sub.add_parser("quota").set_defaults(func=command_quota)
|
|
527
|
+
sub.add_parser("profile").set_defaults(func=command_profile)
|
|
617
528
|
|
|
618
529
|
search = sub.add_parser("search")
|
|
619
|
-
search.add_argument("query"
|
|
620
|
-
search.add_argument("--category", default="")
|
|
530
|
+
search.add_argument("query")
|
|
621
531
|
search.add_argument("--type", choices=("channel", "chat", "all"), default="all")
|
|
622
532
|
search.add_argument("--language", default="")
|
|
623
533
|
search.add_argument("--country", default="")
|
|
624
534
|
search.add_argument("--limit", type=int, default=20)
|
|
625
|
-
search.add_argument("--description", action="store_true")
|
|
626
535
|
search.set_defaults(func=command_search)
|
|
627
536
|
|
|
628
537
|
rankings = sub.add_parser("rankings")
|
|
@@ -633,7 +542,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
633
542
|
|
|
634
543
|
for name, func in (("info", command_info), ("stat", command_stat)):
|
|
635
544
|
command = sub.add_parser(name)
|
|
636
|
-
command.add_argument("target")
|
|
545
|
+
command.add_argument("target", nargs="?", default="")
|
|
637
546
|
command.add_argument("--type", choices=("auto", "channel", "chat"), default="auto")
|
|
638
547
|
command.set_defaults(func=func)
|
|
639
548
|
return parser
|