claude-code-telegram-gateway 1.0.0
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/LICENSE +21 -0
- package/README.md +156 -0
- package/SETUP.md +75 -0
- package/bin/claude-tg.js +30 -0
- package/com.claude.telegram-gateway.plist +49 -0
- package/config.example.json +29 -0
- package/gateway.js +1311 -0
- package/install-service.sh +64 -0
- package/package.json +56 -0
- package/resume-hook.js +17 -0
- package/setup.js +108 -0
- package/systemd/claude-gateway.service +23 -0
- package/test/MANUAL-TESTS.md +85 -0
- package/test/check-telegram.js +55 -0
- package/test/gateway.test.js +523 -0
- package/test/inject-probe.sh +49 -0
- package/test/reset-topics.js +42 -0
- package/uninstall-service.sh +17 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install the Claude Code Telegram gateway as a macOS launchd service (auto-start + auto-restart).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
LABEL="com.claude.telegram-gateway"
|
|
7
|
+
PLIST_SRC="$DIR/$LABEL.plist"
|
|
8
|
+
PLIST_DST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
9
|
+
|
|
10
|
+
NODE_BIN="$(command -v node || true)"
|
|
11
|
+
if [[ -z "$NODE_BIN" ]]; then echo "β node not found on PATH. Install Node or fix PATH, then retry." >&2; exit 1; fi
|
|
12
|
+
NODE_DIR="$(dirname "$NODE_BIN")"
|
|
13
|
+
|
|
14
|
+
mkdir -p "$HOME/Library/LaunchAgents"
|
|
15
|
+
|
|
16
|
+
# Fill placeholders. Use | as sed delimiter since paths contain /.
|
|
17
|
+
sed -e "s|__NODE__|$NODE_BIN|g" \
|
|
18
|
+
-e "s|__DIR__|$DIR|g" \
|
|
19
|
+
-e "s|__HOME__|$HOME|g" \
|
|
20
|
+
-e "s|__PATH__|$NODE_DIR:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin|g" \
|
|
21
|
+
"$PLIST_SRC" > "$PLIST_DST"
|
|
22
|
+
|
|
23
|
+
# (Re)load the service. bootout is async, so retry bootstrap a few times to dodge the
|
|
24
|
+
# "Input/output error (5)" race when the old instance hasn't fully torn down yet.
|
|
25
|
+
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
|
26
|
+
booted=0
|
|
27
|
+
for _ in 1 2 3 4 5; do
|
|
28
|
+
sleep 1
|
|
29
|
+
if launchctl bootstrap "gui/$(id -u)" "$PLIST_DST" 2>/dev/null; then booted=1; break; fi
|
|
30
|
+
done
|
|
31
|
+
if [ "$booted" != 1 ]; then
|
|
32
|
+
echo "β οΈ launchctl bootstrap failed. Try: launchctl bootout gui/$(id -u)/$LABEL ; ./install-service.sh" >&2
|
|
33
|
+
fi
|
|
34
|
+
launchctl enable "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
|
35
|
+
launchctl kickstart "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
|
36
|
+
|
|
37
|
+
# Shell integration: auto-resume the branch you were working on from your phone, so opening a
|
|
38
|
+
# terminal drops you straight back in β no `cr` needed (multi-repo aware). `cr` stays as a manual
|
|
39
|
+
# fallback. The block is idempotent and marked so uninstall can strip it.
|
|
40
|
+
RC="$HOME/.zshrc"
|
|
41
|
+
if ! grep -qF 'claude-gateway auto-resume' "$RC" 2>/dev/null; then
|
|
42
|
+
cat >> "$RC" <<HOOK
|
|
43
|
+
|
|
44
|
+
# >>> claude-gateway auto-resume >>>
|
|
45
|
+
# On an interactive shell, resume any branch you just drove from your phone, then clear the marker.
|
|
46
|
+
_claude_gateway_resume() {
|
|
47
|
+
local out; out="\$(node "$DIR/resume-hook.js" 2>/dev/null)" || return
|
|
48
|
+
[ -z "\$out" ] && return
|
|
49
|
+
local repo="\${out%%\$'\t'*}" sid="\${out##*\$'\t'}"
|
|
50
|
+
[ -d "\$repo" ] && cd "\$repo" && command claude --resume "\$sid"
|
|
51
|
+
}
|
|
52
|
+
alias cr='node "$DIR/resume-hook.js" >/dev/null 2>&1; claude -c' # manual: resume most recent here
|
|
53
|
+
[[ -o interactive ]] && _claude_gateway_resume
|
|
54
|
+
# <<< claude-gateway auto-resume <<<
|
|
55
|
+
HOOK
|
|
56
|
+
echo "π Added auto-resume hook to $RC (open a new terminal to use it)."
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "β
Installed and started $LABEL."
|
|
60
|
+
echo " Logs: tail -f \"$DIR/gateway.log\""
|
|
61
|
+
echo " Status: launchctl print gui/$(id -u)/$LABEL | grep -i state"
|
|
62
|
+
echo " Stop: ./uninstall-service.sh"
|
|
63
|
+
echo
|
|
64
|
+
echo "β οΈ For auto-topics: make the bot a group Admin with the 'Manage Topics' permission."
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-telegram-gateway",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Steer and mirror Claude Code sessions from your phone via Telegram β auto-created Forum Topics, live deskβphone mirroring, launchd service.",
|
|
5
|
+
"main": "gateway.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-tg": "bin/claude-tg.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"setup": "node setup.js",
|
|
11
|
+
"start": "node gateway.js",
|
|
12
|
+
"check": "node test/check-telegram.js",
|
|
13
|
+
"install-service": "./install-service.sh",
|
|
14
|
+
"uninstall-service": "./uninstall-service.sh",
|
|
15
|
+
"test": "node --test test/*.test.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude-code",
|
|
19
|
+
"claude",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"telegram",
|
|
22
|
+
"telegram-bot",
|
|
23
|
+
"gateway",
|
|
24
|
+
"macos",
|
|
25
|
+
"launchd"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"gateway.js",
|
|
32
|
+
"setup.js",
|
|
33
|
+
"resume-hook.js",
|
|
34
|
+
"bin/",
|
|
35
|
+
"test/",
|
|
36
|
+
"install-service.sh",
|
|
37
|
+
"uninstall-service.sh",
|
|
38
|
+
"com.claude.telegram-gateway.plist",
|
|
39
|
+
"systemd/claude-gateway.service",
|
|
40
|
+
"config.example.json",
|
|
41
|
+
"README.md",
|
|
42
|
+
"SETUP.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {},
|
|
46
|
+
"author": "hacctarr",
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/hacctarr/claude-code-telegram-gateway.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/hacctarr/claude-code-telegram-gateway#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/hacctarr/claude-code-telegram-gateway/issues"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/resume-hook.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Prints "<repoDir>\t<sessionId>" for the newest phone-driven branch and consumes it, so a shell
|
|
4
|
+
// hook can auto-resume it. Prints nothing when there's nothing pending. Multi-repo aware.
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const MARKER = path.join(process.env.HOME, '.claude-gateway', 'resume.json');
|
|
8
|
+
try {
|
|
9
|
+
const m = JSON.parse(fs.readFileSync(MARKER, 'utf8'));
|
|
10
|
+
const entries = Object.entries(m).sort((a, b) => (b[1].ts || 0) - (a[1].ts || 0));
|
|
11
|
+
if (!entries.length) process.exit(0);
|
|
12
|
+
const [repo, info] = entries[0];
|
|
13
|
+
process.stdout.write(`${repo}\t${info.sessionId}`);
|
|
14
|
+
delete m[repo];
|
|
15
|
+
if (Object.keys(m).length) fs.writeFileSync(MARKER, JSON.stringify(m, null, 2));
|
|
16
|
+
else fs.unlinkSync(MARKER);
|
|
17
|
+
} catch (e) { /* no marker β nothing to resume */ }
|
package/setup.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Interactive setup: validates the bot, auto-detects your claude path, auto-grabs your user id and
|
|
4
|
+
// group chat id from Telegram, and writes config.json. No dependencies. Run: npm run setup
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const CONFIG = path.join(__dirname, 'config.json');
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
const ask = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
|
|
14
|
+
const yes = (s) => /^y/i.test(s);
|
|
15
|
+
|
|
16
|
+
function tg(token, method, payload = {}) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const data = JSON.stringify(payload);
|
|
19
|
+
const req = https.request({
|
|
20
|
+
hostname: 'api.telegram.org', path: `/bot${token}/${method}`, method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
22
|
+
}, (r) => { let b = ''; r.on('data', (c) => (b += c)); r.on('end', () => { try { resolve(JSON.parse(b)); } catch (e) { resolve({ ok: false }); } }); });
|
|
23
|
+
req.on('error', () => resolve({ ok: false })); req.write(data); req.end();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const expand = (p) => p.replace(/^~(?=\/|$)/, process.env.HOME);
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
console.log('\nπ€ Claude Code Telegram Gateway β setup\n');
|
|
30
|
+
|
|
31
|
+
let cfg = {};
|
|
32
|
+
if (fs.existsSync(CONFIG)) {
|
|
33
|
+
if (!yes(await ask('config.json already exists. Update it? [y/N] '))) { console.log('Aborted.'); return rl.close(); }
|
|
34
|
+
try { cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8')); } catch (e) { /* */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1) Bot token (validated via getMe)
|
|
38
|
+
let token = cfg.BOT_TOKEN, me = null;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const entered = await ask(`Bot token from @BotFather${token ? ` [keep ${token.slice(0, 8)}β¦]` : ''}: `);
|
|
41
|
+
token = entered || token;
|
|
42
|
+
if (!token) { console.log(' A token is required.'); continue; }
|
|
43
|
+
process.stdout.write(' validating⦠');
|
|
44
|
+
me = await tg(token, 'getMe');
|
|
45
|
+
if (me.ok) { console.log(`β
@${me.result.username}`); break; }
|
|
46
|
+
console.log('β invalid token β try again.');
|
|
47
|
+
}
|
|
48
|
+
cfg.BOT_TOKEN = token;
|
|
49
|
+
const botId = String(me.result.id);
|
|
50
|
+
|
|
51
|
+
// 2) Claude binary (auto-detected)
|
|
52
|
+
let claudePath = cfg.CLAUDE_PATH;
|
|
53
|
+
if (!claudePath) { try { claudePath = execSync('command -v claude', { shell: '/bin/bash' }).toString().trim(); } catch (e) { /* */ } }
|
|
54
|
+
const cp = await ask(`Path to the claude binary${claudePath ? ` [${claudePath}]` : ''}: `);
|
|
55
|
+
cfg.CLAUDE_PATH = cp || claudePath || 'claude';
|
|
56
|
+
|
|
57
|
+
// 3) Your user id (auto-detected via getUpdates)
|
|
58
|
+
console.log('\nπ€ Your Telegram user id β the ONLY account allowed to control the bot.');
|
|
59
|
+
console.log(' Open a DM with your bot (or post in your group) and send it any message.');
|
|
60
|
+
await ask(' Press Enter once you have sent a message⦠');
|
|
61
|
+
let userId = null;
|
|
62
|
+
const up1 = await tg(token, 'getUpdates', { timeout: 0 });
|
|
63
|
+
if (up1.error_code === 409) console.log(' β οΈ The gateway service seems to be running β stop it (./uninstall-service.sh) and re-run setup for auto-detect.');
|
|
64
|
+
const senders = [...new Set((up1.result || []).map((u) => u.message && u.message.from && u.message.from.id).filter(Boolean))].filter((id) => String(id) !== botId);
|
|
65
|
+
if (senders.length === 1) { userId = String(senders[0]); console.log(` β
detected user id ${userId}`); }
|
|
66
|
+
else if (senders.length > 1) { console.log(' Multiple senders:', senders.join(', ')); userId = await ask(' Which is yours? '); }
|
|
67
|
+
if (!userId) userId = await ask(' Enter your numeric user id (get it from @userinfobot): ');
|
|
68
|
+
cfg.ALLOWED_USER_IDS = userId ? [String(userId)] : (cfg.ALLOWED_USER_IDS || []);
|
|
69
|
+
|
|
70
|
+
// 4) Group β repo mappings (chat id auto-detected)
|
|
71
|
+
console.log('\nπ Map each Telegram supergroup to a local repo directory.');
|
|
72
|
+
cfg.REPO_MAPPINGS = cfg.REPO_MAPPINGS || {};
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (!yes(await ask(' Add a group β repo mapping? [Y/n] ') || 'y')) break;
|
|
75
|
+
console.log(' In that group\'s General topic, post any message.');
|
|
76
|
+
await ask(' Press Enter once posted⦠');
|
|
77
|
+
const up = await tg(token, 'getUpdates', { timeout: 0 });
|
|
78
|
+
const chats = [...new Map((up.result || [])
|
|
79
|
+
.map((u) => u.message && u.message.chat)
|
|
80
|
+
.filter((c) => c && (c.type === 'supergroup' || c.type === 'group'))
|
|
81
|
+
.map((c) => [String(c.id), c])).values()];
|
|
82
|
+
let chatId;
|
|
83
|
+
if (chats.length) {
|
|
84
|
+
chats.forEach((c, i) => console.log(` [${i + 1}] ${c.id} ${c.title || ''}`));
|
|
85
|
+
const pick = await ask(` Pick [1-${chats.length}] or type a chat id: `);
|
|
86
|
+
chatId = (/^-?\d+$/.test(pick) && pick.length > 3) ? pick : String((chats[(parseInt(pick, 10) || 1) - 1] || chats[0]).id);
|
|
87
|
+
} else {
|
|
88
|
+
chatId = await ask(' Couldn\'t detect a group β enter its chat id (-100β¦): ');
|
|
89
|
+
}
|
|
90
|
+
let dir = expand(await ask(' Local repo directory (absolute path): '));
|
|
91
|
+
if (dir && !fs.existsSync(dir) && !yes(await ask(` ${dir} doesn't exist β use anyway? [y/N] `))) continue;
|
|
92
|
+
if (chatId && dir) { cfg.REPO_MAPPINGS[chatId] = dir; console.log(` β
${chatId} β ${dir}`); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.writeFileSync(CONFIG, JSON.stringify(cfg, null, 2) + '\n');
|
|
96
|
+
console.log(`\nβ
Wrote ${CONFIG}`);
|
|
97
|
+
console.log(' (config.json is gitignored β your token stays local.)\n');
|
|
98
|
+
|
|
99
|
+
if (yes(await ask('Run the Telegram permission check now? [Y/n] ') || 'y')) {
|
|
100
|
+
try { execSync(`node ${path.join(__dirname, 'test', 'check-telegram.js')}`, { stdio: 'inherit' }); } catch (e) { /* */ }
|
|
101
|
+
}
|
|
102
|
+
if (yes(await ask('\nInstall as a background service (launchd, auto-start + restart)? [y/N] '))) {
|
|
103
|
+
try { execSync(path.join(__dirname, 'install-service.sh'), { stdio: 'inherit' }); } catch (e) { /* */ }
|
|
104
|
+
} else {
|
|
105
|
+
console.log('\nStart it anytime: npm start (foreground) Β· ./install-service.sh (service)');
|
|
106
|
+
}
|
|
107
|
+
rl.close();
|
|
108
|
+
})().catch((e) => { console.error('setup error:', e.message); rl.close(); process.exit(1); });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Linux (systemd --user) unit for the Claude Code Telegram gateway.
|
|
2
|
+
# Install:
|
|
3
|
+
# mkdir -p ~/.config/systemd/user
|
|
4
|
+
# sed "s#__DIR__#$(pwd)#; s#__NODE__#$(command -v node)#" systemd/claude-gateway.service \
|
|
5
|
+
# > ~/.config/systemd/user/claude-gateway.service
|
|
6
|
+
# systemctl --user daemon-reload
|
|
7
|
+
# systemctl --user enable --now claude-gateway
|
|
8
|
+
# loginctl enable-linger "$USER" # keep it running after you log out
|
|
9
|
+
# Logs: journalctl --user -u claude-gateway -f
|
|
10
|
+
[Unit]
|
|
11
|
+
Description=Claude Code Telegram gateway
|
|
12
|
+
After=network-online.target
|
|
13
|
+
Wants=network-online.target
|
|
14
|
+
|
|
15
|
+
[Service]
|
|
16
|
+
Type=simple
|
|
17
|
+
WorkingDirectory=__DIR__
|
|
18
|
+
ExecStart=__NODE__ __DIR__/gateway.js
|
|
19
|
+
Restart=on-failure
|
|
20
|
+
RestartSec=10
|
|
21
|
+
|
|
22
|
+
[Install]
|
|
23
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Manual verification runbook
|
|
2
|
+
|
|
3
|
+
Unit tests (`npm test`) cover the pure logic. These steps cover what they can't: the real Telegram
|
|
4
|
+
API and the interaction between headless injection and the live desk TUI. Do them once before
|
|
5
|
+
trusting the gateway in daily use.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Test A β Injection vs. an open TUI (the critical one, gap #1)
|
|
10
|
+
|
|
11
|
+
**Why:** phoneβdesk injection runs `claude -p --resume <id>` when the transcript looks idle. If the
|
|
12
|
+
native TUI is holding that session, we need to know whether that injection is safe (clean append),
|
|
13
|
+
locks/errors, or forks the transcript.
|
|
14
|
+
|
|
15
|
+
1. **Terminal 1 (the "desk"):**
|
|
16
|
+
```bash
|
|
17
|
+
cd <your mapped repo, e.g. ~/Documents>
|
|
18
|
+
claude
|
|
19
|
+
```
|
|
20
|
+
Send one message, e.g. `Remember the codeword: BANANA.` Wait for the reply, then **leave the TUI
|
|
21
|
+
open and idle** (don't quit).
|
|
22
|
+
|
|
23
|
+
2. **Terminal 2 (the "phone", simulated):**
|
|
24
|
+
```bash
|
|
25
|
+
cd ~/telegram_gateway
|
|
26
|
+
./test/inject-probe.sh
|
|
27
|
+
```
|
|
28
|
+
It auto-picks the most recent session (your Terminal-1 one) and injects a headless turn.
|
|
29
|
+
|
|
30
|
+
3. **Read the probe output:**
|
|
31
|
+
- `exit_ok: true` and `result: "INJECTED_OK"` β headless resume succeeded even with the TUI open.
|
|
32
|
+
- `same_session_id` equals the session id β no new session was minted.
|
|
33
|
+
- `appended to the SAME file: yes` and `New sibling session files: 0` β **clean append, no fork.** β
|
|
34
|
+
- Any `non-JSON output (possible lock/conflict)`, a new sibling file, or a changed session id β
|
|
35
|
+
**injection conflicts with the open TUI.** β In that case we should tighten the idle-gate to
|
|
36
|
+
also refuse injection whenever the session is the *most recently active* one (assume TUI owns it),
|
|
37
|
+
or require the desk session be closed. Tell me the result and I'll adjust.
|
|
38
|
+
|
|
39
|
+
4. **Back in Terminal 1:** send another message (e.g. `What was the codeword?`). Confirm the TUI still
|
|
40
|
+
responds coherently and didn't break. Note whether it "sees" the injected turn (it generally
|
|
41
|
+
won't until you `cr`/reload β that's expected).
|
|
42
|
+
|
|
43
|
+
5. **Cleanup:** quit the TUI. Run `cr` (or `claude -c`) in the repo and confirm the session loads with
|
|
44
|
+
both the desk turns and the injected `INJECTED_OK` turn present.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Test B β Telegram permissions & topic lifecycle (gap #2)
|
|
49
|
+
|
|
50
|
+
**Why:** auto-topics need the bot to be an admin with **Manage Topics**. This verifies it for real and
|
|
51
|
+
leaves no junk behind (it deletes its own self-test topic).
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cd ~/telegram_gateway
|
|
55
|
+
node test/check-telegram.js
|
|
56
|
+
```
|
|
57
|
+
Expect `β
All checks passed`. If it reports "not an admin" or "can_manage_topics: false", fix the
|
|
58
|
+
bot's rights in the group (Admin β Manage Topics) and re-run.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Test C β End-to-end mirror + auto-initiate
|
|
63
|
+
|
|
64
|
+
1. Start the gateway (`npm start`, or `./install-service.sh` then `tail -f gateway.log`).
|
|
65
|
+
2. In the desk TUI, open/continue a session in the mapped repo and send a message.
|
|
66
|
+
3. Within ~30 min of activity (default `ACTIVE_WINDOW_MIN`) a **new topic** should appear in the
|
|
67
|
+
supergroup with an opener, and within ~2s (`POLL_MS`) your exchange should mirror in as
|
|
68
|
+
`π₯οΈ desk:` / assistant text / `π§ tool` lines.
|
|
69
|
+
4. From the phone, reply in that topic while the desk session is **idle** β the turn runs and streams
|
|
70
|
+
back once (no duplicate from the mirror).
|
|
71
|
+
5. From the phone, `/new draft a haiku` β a brand-new topic + independent session appears.
|
|
72
|
+
6. **Prune:** temporarily set `PRUNE_AFTER_DAYS` very low (e.g. via a quick config edit + restart) and
|
|
73
|
+
confirm an idle session's topic gets closed; revive the session and confirm it reopens.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick reference
|
|
78
|
+
|
|
79
|
+
| Check | Command |
|
|
80
|
+
|---|---|
|
|
81
|
+
| Unit tests | `npm test` |
|
|
82
|
+
| Telegram perms | `node test/check-telegram.js` |
|
|
83
|
+
| Injection safety | `./test/inject-probe.sh [session-uuid]` |
|
|
84
|
+
| Live logs (service) | `tail -f gateway.log` |
|
|
85
|
+
| Service state | `launchctl print gui/$(id -u)/com.claude.telegram-gateway \| grep -i state` |
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Live check that the bot can do what the gateway needs: identify itself, create a forum topic,
|
|
4
|
+
// post into it, and delete it. Verifies the "admin + Manage Topics" requirement without guessing.
|
|
5
|
+
// Run: node test/check-telegram.js
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config.json'), 'utf8'));
|
|
11
|
+
const { BOT_TOKEN, REPO_MAPPINGS } = cfg;
|
|
12
|
+
|
|
13
|
+
function tg(method, payload) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const data = JSON.stringify(payload || {});
|
|
16
|
+
const req = https.request({
|
|
17
|
+
hostname: 'api.telegram.org', port: 443, path: `/bot${BOT_TOKEN}/${method}`, method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
19
|
+
}, (res) => { let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { try { resolve(JSON.parse(b)); } catch (e) { reject(e); } }); });
|
|
20
|
+
req.on('error', reject); req.write(data); req.end();
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
(async () => {
|
|
25
|
+
const me = await tg('getMe');
|
|
26
|
+
if (!me.ok) { console.error('β getMe failed β check BOT_TOKEN.', me); process.exit(1); }
|
|
27
|
+
console.log(`β
Bot: @${me.result.username} (id ${me.result.id})`);
|
|
28
|
+
|
|
29
|
+
let allGood = true;
|
|
30
|
+
for (const chatId of Object.keys(REPO_MAPPINGS)) {
|
|
31
|
+
process.stdout.write(`\nβ chat ${chatId} (${REPO_MAPPINGS[chatId]})\n`);
|
|
32
|
+
|
|
33
|
+
const chat = await tg('getChat', { chat_id: chatId });
|
|
34
|
+
if (!chat.ok) { console.error(` β getChat failed: ${chat.description}`); allGood = false; continue; }
|
|
35
|
+
console.log(` chat type: ${chat.result.type}, forum: ${chat.result.is_forum === true}`);
|
|
36
|
+
if (!chat.result.is_forum) { console.error(' β Topics/Forum not enabled on this group.'); allGood = false; }
|
|
37
|
+
|
|
38
|
+
const admin = await tg('getChatMember', { chat_id: chatId, user_id: me.result.id });
|
|
39
|
+
const status = admin.ok ? admin.result.status : 'unknown';
|
|
40
|
+
const canManage = admin.ok && (admin.result.can_manage_topics === true || status === 'creator');
|
|
41
|
+
console.log(` bot status: ${status}, can_manage_topics: ${admin.ok ? admin.result.can_manage_topics : '?'}`);
|
|
42
|
+
if (status !== 'administrator' && status !== 'creator') { console.error(' β Bot is not an admin.'); allGood = false; continue; }
|
|
43
|
+
|
|
44
|
+
const created = await tg('createForumTopic', { chat_id: chatId, name: 'π§ gateway self-test' });
|
|
45
|
+
if (!created.ok) { console.error(` β createForumTopic failed: ${created.description}`); allGood = false; continue; }
|
|
46
|
+
const threadId = created.result.message_thread_id;
|
|
47
|
+
console.log(` β
created topic ${threadId}`);
|
|
48
|
+
await tg('sendMessage', { chat_id: chatId, message_thread_id: threadId, text: 'Self-test OK β deleting this topic.' });
|
|
49
|
+
const del = await tg('deleteForumTopic', { chat_id: chatId, message_thread_id: threadId });
|
|
50
|
+
console.log(del.ok ? ' β
posted + deleted self-test topic (cleanup done)' : ` β οΈ could not delete self-test topic: ${del.description}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`\n${allGood ? 'β
All checks passed β auto-topics + mirroring will work.' : 'β Some checks failed β fix the above before relying on auto-topics.'}`);
|
|
54
|
+
process.exit(allGood ? 0 : 1);
|
|
55
|
+
})().catch((e) => { console.error('check-telegram error:', e); process.exit(1); });
|