claude-telegram-mirror 0.2.22 → 0.2.24
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/README.md +11 -8
- package/SECURITY.md +13 -12
- package/package.json +5 -5
- package/postinstall.cjs +65 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ This installs a native Rust binary (`ctm`) via platform-specific optional packag
|
|
|
22
22
|
- **CLI to Telegram**: Mirror Claude's responses, tool usage, and notifications
|
|
23
23
|
- **Telegram to CLI**: Send prompts from Telegram directly to Claude Code
|
|
24
24
|
- **Tool Summarizer**: Human-readable summaries for 30+ command patterns ("Running tests" instead of "Running: Bash")
|
|
25
|
-
- **AskUserQuestion (
|
|
25
|
+
- **AskUserQuestion (both surfaces)**: Claude's interactive multiple-choice question renders natively in the CLI *and* as inline buttons on Telegram — answer from either side. Telegram answers drive the live CLI widget via `tmux send-keys`, with `capture-pane` readiness pacing on multi-select so the confirming Enter only fires once Claude's review screen is on-screen (ADR-015)
|
|
26
26
|
- **Photo & Document Upload**: Send images/files from Telegram, path injected into Claude
|
|
27
27
|
- **Stop/Interrupt**: Type `stop` to send Escape, `kill` to send Ctrl-C
|
|
28
28
|
- **Session Threading**: Each Claude session gets its own Forum Topic
|
|
@@ -153,10 +153,10 @@ Approval buttons only appear in normal mode, not with `--dangerously-skip-permis
|
|
|
153
153
|
|
|
154
154
|
**Flow:**
|
|
155
155
|
1. Claude Code hooks invoke `ctm hook`, which reads the event from stdin
|
|
156
|
-
2. PreToolUse: sends an approval request
|
|
156
|
+
2. PreToolUse: for tool approvals, sends an approval request via socket and blocks for the Telegram response. **AskUserQuestion is *not* intercepted here** — the hook returns fast so Claude renders its native widget in the CLI; the daemon mirrors the question to Telegram from the standard `tool_start` event (ADR-015)
|
|
157
157
|
3. Other hooks: sends JSON to daemon via socket and exits immediately
|
|
158
158
|
4. Daemon forwards messages to Telegram Forum Topic with summarized tool actions
|
|
159
|
-
5. Telegram
|
|
159
|
+
5. Telegram replies are injected into the live CLI via `tmux send-keys` — including AskUserQuestion option/multi-select answers, which drive the native widget directly. **Multi-select uses `tmux capture-pane` readiness pacing** so each keystroke and the confirming Enter only fire once the expected screen has rendered — no blind sleeps (ADR-015). The question shows in **both** the CLI (native) and Telegram, answerable from either
|
|
160
160
|
6. Stop/kill commands send Escape or Ctrl-C to interrupt Claude
|
|
161
161
|
|
|
162
162
|
## Multi-System Architecture
|
|
@@ -301,7 +301,7 @@ ctm install-hooks --project
|
|
|
301
301
|
- **Topic routing**: Each daemon only processes topics it created (multi-bot safe)
|
|
302
302
|
- **Rate limiting**: Governor-based with exponential backoff retry queue
|
|
303
303
|
- **Token scrubbing**: All log output filtered through regex to strip bot tokens
|
|
304
|
-
- **Test suite**:
|
|
304
|
+
- **Test suite**: 470+ Rust tests (unit + 11 integration test files)
|
|
305
305
|
|
|
306
306
|
## Troubleshooting
|
|
307
307
|
|
|
@@ -367,7 +367,7 @@ cargo test
|
|
|
367
367
|
./target/release/ctm start
|
|
368
368
|
```
|
|
369
369
|
|
|
370
|
-
### Project Structure (
|
|
370
|
+
### Project Structure (33 source files)
|
|
371
371
|
|
|
372
372
|
```
|
|
373
373
|
rust-crates/ctm/src/
|
|
@@ -382,16 +382,19 @@ rust-crates/ctm/src/
|
|
|
382
382
|
injector.rs # tmux input injection
|
|
383
383
|
formatting.rs # Message formatting, chunking, ANSI stripping
|
|
384
384
|
summarize.rs # Tool action summarizer (30+ patterns)
|
|
385
|
+
liveness.rs # tmux pane liveness checks for topic reconciliation
|
|
386
|
+
prune.rs # prune-topics subcommand (bulk stale-topic cleanup)
|
|
385
387
|
colors.rs # ANSI color helpers for terminal output
|
|
386
388
|
doctor.rs # Diagnostic checks with --fix
|
|
387
389
|
installer.rs # Hook installer
|
|
388
390
|
setup.rs # Interactive setup wizard
|
|
389
|
-
bot/ # Telegram API client (client.rs, queue.rs, types.rs)
|
|
391
|
+
bot/ # Telegram API client (mod.rs, client.rs, queue.rs, types.rs)
|
|
390
392
|
daemon/ # Bridge daemon (mod.rs, event_loop.rs, socket_handlers.rs,
|
|
391
|
-
# telegram_handlers.rs, callback_handlers.rs, cleanup.rs,
|
|
393
|
+
# telegram_handlers.rs, callback_handlers.rs, cleanup.rs,
|
|
394
|
+
# reconcile.rs, files.rs)
|
|
392
395
|
service/ # OS service management (mod.rs, systemd.rs, launchd.rs, env.rs)
|
|
393
396
|
|
|
394
|
-
rust-crates/ctm/tests/ #
|
|
397
|
+
rust-crates/ctm/tests/ # 11 integration test files
|
|
395
398
|
```
|
|
396
399
|
|
|
397
400
|
</details>
|
package/SECURITY.md
CHANGED
|
@@ -62,11 +62,11 @@ metacharacters are rejected.
|
|
|
62
62
|
|
|
63
63
|
### 2. Bot Token Scrubbing
|
|
64
64
|
|
|
65
|
-
**
|
|
65
|
+
**Files:** `rust-crates/ctm/src/main.rs` (subscriber layer), `rust-crates/ctm/src/bot/mod.rs` (regex)
|
|
66
66
|
|
|
67
|
-
A `tracing` subscriber layer applies `scrub_bot_token()`
|
|
68
|
-
The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token
|
|
69
|
-
in API URLs and replaces it with `bot
|
|
67
|
+
A `tracing` subscriber layer (`ScrubWriter` in `main.rs`) applies `scrub_bot_token()`
|
|
68
|
+
to all log output. The regex `bot\d+:[A-Za-z0-9_-]+/` matches the Telegram bot token
|
|
69
|
+
pattern in API URLs and replaces it with `bot[REDACTED]/`.
|
|
70
70
|
|
|
71
71
|
All log output goes to stderr via the `tracing` subscriber. There is no
|
|
72
72
|
file transport, so tokens cannot leak into log files on disk.
|
|
@@ -79,8 +79,9 @@ tokens from error messages before logging.
|
|
|
79
79
|
**File:** `rust-crates/ctm/src/daemon/telegram_handlers.rs`
|
|
80
80
|
|
|
81
81
|
A chat ID check verifies `chat.id` against the configured `chat_id` on
|
|
82
|
-
every incoming update. Updates from unauthorized chats
|
|
83
|
-
|
|
82
|
+
every incoming update. Updates from unauthorized chats are silently dropped
|
|
83
|
+
(logged as a warning, no reply) — by design, since replying would confirm the
|
|
84
|
+
bot's existence and function to an attacker (ADR-006 L4.6).
|
|
84
85
|
|
|
85
86
|
Approval callback handlers (`approve:`, `reject:`, `abort:`), answer
|
|
86
87
|
handlers (`answer:`, `toggle:`, `submit:`), all verify the chat ID
|
|
@@ -94,7 +95,7 @@ different chat.
|
|
|
94
95
|
|
|
95
96
|
Session IDs from hook events are validated before any database operation:
|
|
96
97
|
- Maximum length: 128 characters
|
|
97
|
-
- Character set: `[a-zA-Z0-9_
|
|
98
|
+
- Character set: `[a-zA-Z0-9_.-]` only
|
|
98
99
|
- Empty/null values are rejected
|
|
99
100
|
|
|
100
101
|
Messages with invalid session IDs are dropped with a warning log.
|
|
@@ -118,10 +119,10 @@ socket.
|
|
|
118
119
|
|
|
119
120
|
**File:** `rust-crates/ctm/src/config.rs`
|
|
120
121
|
|
|
121
|
-
`
|
|
122
|
+
`validate_socket_path()` rejects socket paths that:
|
|
122
123
|
- Contain `..` (directory traversal)
|
|
123
124
|
- Are not absolute (do not start with `/`)
|
|
124
|
-
- Exceed
|
|
125
|
+
- Exceed 104 characters (the AF_UNIX `sun_path` limit)
|
|
125
126
|
|
|
126
127
|
Invalid paths fall back to the default socket path in the config directory.
|
|
127
128
|
|
|
@@ -151,7 +152,7 @@ handler logs a warning and exits cleanly without processing the payload.
|
|
|
151
152
|
|
|
152
153
|
### 10. Download File Handling
|
|
153
154
|
|
|
154
|
-
**File:** `rust-crates/ctm/src/daemon/
|
|
155
|
+
**File:** `rust-crates/ctm/src/daemon/files.rs`
|
|
155
156
|
|
|
156
157
|
Downloaded files from Telegram are handled with several protections:
|
|
157
158
|
- The downloads directory is created with mode 0o700
|
|
@@ -179,11 +180,11 @@ Downloaded files from Telegram are handled with several protections:
|
|
|
179
180
|
|
|
180
181
|
| Boundary | Validation | Enforcement |
|
|
181
182
|
|---|---|---|
|
|
182
|
-
| Socket messages: session ID | 128 char max, `[a-zA-Z0-9_
|
|
183
|
+
| Socket messages: session ID | 128 char max, `[a-zA-Z0-9_.-]` | `socket_handlers.rs` — `is_valid_session_id()` |
|
|
183
184
|
| Socket lines | 1 MiB max per NDJSON line | `socket.rs` — `MAX_LINE_BYTES` |
|
|
184
185
|
| Socket connections | 64 concurrent max | `socket.rs` — `MAX_CONNECTIONS` |
|
|
185
186
|
| Hook stdin | 1 MiB max | `hook.rs` — `MAX_STDIN_BYTES` |
|
|
186
|
-
| Socket paths | No `..`, absolute only,
|
|
187
|
+
| Socket paths | No `..`, absolute only, 104 char max | `config.rs` — `validate_socket_path()` |
|
|
187
188
|
| Slash commands | Character whitelist: `[a-zA-Z0-9_- /]` | `injector.rs` — `is_valid_slash_command()` |
|
|
188
189
|
| Download filenames | Sanitized, UUID-prefixed, no `..`, 200 char max | `telegram_handlers.rs` — `sanitize_filename()` |
|
|
189
190
|
| Download file size | 20 MB max | Telegram Bot API server-side limit |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-telegram-mirror",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "Bidirectional Telegram integration for Claude Code CLI - monitor and control your Claude Code sessions from Telegram",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-telegram-mirror": "./scripts/ctm-wrapper.cjs",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"optionalDependencies": {
|
|
52
|
-
"@agidreams/ctm-linux-x64": "0.2.
|
|
53
|
-
"@agidreams/ctm-linux-arm64": "0.2.
|
|
54
|
-
"@agidreams/ctm-darwin-arm64": "0.2.
|
|
55
|
-
"@agidreams/ctm-darwin-x64": "0.2.
|
|
52
|
+
"@agidreams/ctm-linux-x64": "0.2.24",
|
|
53
|
+
"@agidreams/ctm-linux-arm64": "0.2.24",
|
|
54
|
+
"@agidreams/ctm-darwin-arm64": "0.2.24",
|
|
55
|
+
"@agidreams/ctm-darwin-x64": "0.2.24"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
58
|
"node": ">=18.0.0"
|
package/postinstall.cjs
CHANGED
|
@@ -7,6 +7,71 @@
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* macOS hardening: a binary delivered via npm carries only the ad-hoc signature
|
|
14
|
+
* the build toolchain applied — it is NOT Developer-ID signed or notarized. On
|
|
15
|
+
* modern macOS the kernel can SIGKILL such a binary for a code-signing /
|
|
16
|
+
* launch-constraint violation (EXC_CRASH / "Code Signature Invalid"),
|
|
17
|
+
* especially on the first launchd-spawned launch after install. This pass is
|
|
18
|
+
* defense-in-depth: strip any quarantine flag, ensure the binary has a coherent
|
|
19
|
+
* (at least ad-hoc) signature, and smoke-test that it can actually exec. It
|
|
20
|
+
* never throws — install must still succeed — but it warns loudly so the user
|
|
21
|
+
* is not left with a binary the OS silently refuses to run.
|
|
22
|
+
*/
|
|
23
|
+
function hardenMacBinary(binary) {
|
|
24
|
+
if (process.platform !== 'darwin') return;
|
|
25
|
+
|
|
26
|
+
const run = (cmd, args) => {
|
|
27
|
+
try {
|
|
28
|
+
return { ok: true, out: execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) };
|
|
29
|
+
} catch (e) {
|
|
30
|
+
return { ok: false, out: (e.stdout || '') + (e.stderr || ''), err: e };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 1. Remove the quarantine xattr if a download path applied one. Best-effort.
|
|
35
|
+
run('xattr', ['-d', 'com.apple.quarantine', binary]);
|
|
36
|
+
|
|
37
|
+
// 2. If the signature is missing/invalid, re-apply an ad-hoc signature so the
|
|
38
|
+
// Mach-O is at least internally consistent and execable on Apple Silicon.
|
|
39
|
+
const verify = run('codesign', ['--verify', '--strict', binary]);
|
|
40
|
+
if (!verify.ok) {
|
|
41
|
+
const resign = run('codesign', ['--force', '--sign', '-', binary]);
|
|
42
|
+
if (!resign.ok) {
|
|
43
|
+
console.log('WARNING: Could not re-sign the native binary; macOS may refuse to run it.');
|
|
44
|
+
console.log(' Try manually: codesign --force --sign - "' + binary + '"');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Smoke-test exec. If the OS kills it here, the user would otherwise only
|
|
49
|
+
// discover it when the daemon silently fails to start.
|
|
50
|
+
const smoke = run(binary, ['--version']);
|
|
51
|
+
if (!smoke.ok) {
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('WARNING: The native ctm binary failed to execute on this machine.');
|
|
54
|
+
console.log(' This is typically a macOS code-signing / Gatekeeper rejection of a');
|
|
55
|
+
console.log(' non-notarized binary. To inspect:');
|
|
56
|
+
console.log(' codesign -dvvv "' + binary + '"');
|
|
57
|
+
console.log(' ls ~/Library/Logs/DiagnosticReports/ctm-*.ips');
|
|
58
|
+
console.log(' Or build from source: cd rust-crates && cargo build --release');
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Protect the native binary on every real install — independent of whether we
|
|
64
|
+
// also print the setup guidance below. Skipped under CI (the binary is built
|
|
65
|
+
// and verified there, not consumed).
|
|
66
|
+
if (!process.env.CI) {
|
|
67
|
+
try {
|
|
68
|
+
const { resolveBinary } = require('./scripts/resolve-binary.cjs');
|
|
69
|
+
const r = resolveBinary();
|
|
70
|
+
if (r) hardenMacBinary(r.binary);
|
|
71
|
+
} catch {
|
|
72
|
+
// resolve-binary unavailable — skip silently
|
|
73
|
+
}
|
|
74
|
+
}
|
|
10
75
|
|
|
11
76
|
// Don't show guidance during CI or if TELEGRAM_BOT_TOKEN is already set
|
|
12
77
|
if (process.env.CI || process.env.TELEGRAM_BOT_TOKEN) {
|