TG Channel Manager
Pipeline: scout → draft → human approves → publisher.
Execution
python3 and curl are available in your environment (declared in requires.bins). Run all commands yourself using exec/bash tool. NEVER ask the user to run commands for you.
Startup
When you load this skill, run the preflight check FIRST:
python3 {baseDir}/scripts/tgcm.py --workspace {workspace} check
After check — act on results, don't ask:
- All
[ok]→ proceed with the user's task silently [fail] Bot token→ ask the user for the token, then save it:tgcm.py config set bot-token <token>. Do NOT ask where the token is or offer choices[warn] SEARXNG_URL→ ask the user for the URL, then save it:tgcm.py config set searxng-url <url>. Proceed without it — scout won't work but other commands will[fail] Channel→ report which channel failed and why, include the fix from the output[warn] No channels→ mention it, but proceed — the user may want to init one
Settings saved via config set persist in tgcm/.config.json and are used by all subsequent commands.
NEVER ask follow-up questions about check results. Report what's wrong and the fix from the output. If the user hasn't specified a task — just report the check status, nothing else.
CLI Reference (FULL list — NO other commands exist)
All commands: python3 {baseDir}/scripts/tgcm.py --workspace {workspace} <cmd>
| Command | What it does |
|---|---|
init <name> | Create a channel |
list | Show all channels |
bind <name> --channel-id ID | Bind channel to Telegram |
info <name> [--chat] [--subscribers] [--permissions] [--admins] [--all] | Channel status |
get-id <@username|ID> | Resolve @username or numeric ID → full channel info (id, type, title) |
check | Preflight: verify bot token, channels, env vars |
config set <key> <value> | Save setting locally (keys: bot-token, searxng-url) |
config get <key> | Read a saved setting |
config list | Show all saved settings |
fetch-posts <name> [--limit N] [--dry-run] | Load channel posts into dedup index (requires public channel with @username) |
connect --channel-id ID [--channel-title T] | Handle #tgcm connect event |
Bot token is auto-resolved: --bot-token arg → $BOT_TOKEN env → openclaw.json (auto-search) → tgcm/.config.json. Just call tgcm.py get-id @username without --bot-token — the script finds the token itself. If auto-detection fails, save it once: tgcm.py config set bot-token <token>.
Channel name validation: ^[a-z0-9][a-z0-9_-]{0,62}$.
Quick Reference
| User says | Do this |
|---|---|
| «узнай/определи channel-id» | tgcm.py get-id @username |
| «подключи канал» | Recipe: Connect a channel (ниже) |
| «какие каналы / список» | tgcm.py list |
| «статус канала X» | tgcm.py info X |
| «что в очереди» | cat tgcm/<name>/content-queue.md |
| «загрузи посты / rebuild index» | tgcm.py fetch-posts <name> |
Recipes
Look up channel ID
python3 {baseDir}/scripts/tgcm.py get-id @username
Token is found automatically. Returns channel id, type, and title.
Connect a channel
- Get ID:
python3 {baseDir}/scripts/tgcm.py get-id @username python3 {baseDir}/scripts/tgcm.py --workspace {workspace} init <name>python3 {baseDir}/scripts/tgcm.py --workspace {workspace} bind <name> --channel-id <id>- Configure
skills.entries["tg-channel-manager"].configin openclaw.json - Add crons (see
{baseDir}/references/cron-setup.md)
Load channel posts (rebuild dedup index)
python3 {baseDir}/scripts/tgcm.py --workspace {workspace} fetch-posts <name>
Fetches posts from the channel's public page (t.me/s/) and adds them to content-index.json.
Options: --limit N (max pages, default 5), --dry-run (preview only).
Requires: channel must be public (have a @username).
View channels / status
tgcm.py listtgcm.py info <name>- Queue:
cat tgcm/<name>/content-queue.md
Do NOT
- Invent commands — the table above is the FULL list
- Publish posts directly — only cron Publisher does this
- Change draft → pending — only the human does this
- Skip dedup-check before drafting
- Ask the user to run commands — python3 and curl are available, use exec/bash yourself
- Ask the user for bot token or env vars — token is auto-resolved,
checkshows what's wrong - Ask the user follow-up questions after
check— report errors and the fix commands, don't offer choices - Ask whether it's a channel or group —
get-idreturns thetypefield, this skill is for channels only
Data Layout
tgcm/
channels.json <- [{"name", "channelId", "status", "createdAt"}, ...]
{channel-name}/
channel.json <- per-channel metadata
content-index.json <- dedup index
content-queue.md <- post queue
A channel is bound when channelId is set and status is "connected".
Configuration
Parameters are read from openclaw.json → skills.entries["tg-channel-manager"]:
Telegram
| Parameter | Type | Description |
|---|---|---|
config.channelId | string | Telegram channel ID for publishing |
config.chatId | string | Channel community chat ID (optional) |
Limits & Schedule
| Parameter | Type | Description |
|---|---|---|
config.maxPostsPerDay | number | Maximum posts per day |
config.maxDraftsPerRun | number | Maximum drafts per scout run |
config.timezone | string | Schedule timezone (IANA format) |
config.language | string | Post language (ru, en, ...) |
config.cronScoutTimes | string[] | Scout schedules (cron format) |
config.cronPublisherTimes | string[] | Publisher schedules (cron format) |
Content
| Parameter | Type | Description |
|---|---|---|
config.rubrics | array | Rubrics: [{id, emoji, name}, ...] |
config.searchQueries | string[] | Search queries for SearXNG |
config.searchInclude | string | What to look for (filter description) |
config.searchExclude | string | What to discard (filter description) |
config.evergreen | string[] | Topics for articles when there are no news |
Post Style
| Parameter | Type | Description |
|---|---|---|
config.postStyle.minChars | number | Minimum characters per post |
config.postStyle.maxChars | number | Maximum characters per post |
config.postStyle.emojiTitle | boolean | Emoji before the title |
config.postStyle.boldTitle | boolean | Bold title |
config.postStyle.signature | string | Post signature |
config.postStyle.newsFooter | string | Extra text for news posts (empty = none) |
config.postStyle.articleFooter | string | Extra text for articles (empty = none) |
Environment
| Parameter | Type | Description |
|---|---|---|
env.SEARXNG_URL | string | SearXNG instance URL |
Path Resolution
| Variable | How to resolve |
|---|---|
{workspace} | Your CWD. Run pwd or use --workspace . |
{baseDir} | {workspace}/skills/tg-channel-manager |
In sandbox mode (workspaceAccess: "none"), the workspace is under ~/.openclaw/sandboxes, not ~/.openclaw/workspace. Always use CWD-relative paths.
Cron setup: see {baseDir}/references/cron-setup.md.
Queue Format
Entry Format in content-queue.md
### <number>
- **Status:** draft | pending
- **Rubric:** <emoji> <name> (from config.rubrics)
- **Topic:** <topic>
- **Source:** <url> (for news)
- **Text:**
<post text>
Statuses:
- draft — awaiting approval
- pending — approved, ready for publishing
After publishing, the entry is removed from content-queue.md.
Deduplication
Before every draft — mandatory check:
python3 {baseDir}/scripts/dedup-check.py --base-dir <workspace> --topic "topic" --links "url1" "url2"
After publishing — add to index:
python3 {baseDir}/scripts/dedup-check.py --base-dir <workspace> --add <msgId> --topic "topic" --links "url"
Rebuild index (via Telegram search):
python3 {baseDir}/scripts/dedup-check.py --base-dir <workspace> --rebuild --channel-id <config.channelId>
Index is stored in <workspace>/content-index.json (or per-channel: tgcm/<name>/content-index.json).