@gonzih/cc-tg 0.2.9 → 0.2.11
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 +57 -34
- package/dist/bot.js +72 -31
- package/dist/index.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# cc-tg
|
|
2
2
|
|
|
3
|
-
Claude Code Telegram bot. Chat with Claude Code from Telegram — voice
|
|
3
|
+
Claude Code Telegram bot. Chat with Claude Code from Telegram — text, voice, images, files, scheduled prompts, and bot management commands.
|
|
4
|
+
|
|
5
|
+
Built by [@Gonzih](https://github.com/Gonzih).
|
|
4
6
|
|
|
5
7
|
## Quickstart
|
|
6
8
|
|
|
@@ -9,90 +11,107 @@ Claude Code Telegram bot. Chat with Claude Code from Telegram — voice messages
|
|
|
9
11
|
**Step 2** — run:
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
TELEGRAM_BOT_TOKEN=your_bot_token
|
|
14
|
+
TELEGRAM_BOT_TOKEN=your_bot_token CLAUDE_CODE_OAUTH_TOKEN=your_claude_token npx @gonzih/cc-tg
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
Open your bot in Telegram and start chatting.
|
|
16
18
|
|
|
17
19
|
## Environment variables
|
|
18
20
|
|
|
19
21
|
| Variable | Required | Description |
|
|
20
|
-
|
|
22
|
+
|----------|----------|-------------|
|
|
21
23
|
| `TELEGRAM_BOT_TOKEN` | yes | From @BotFather |
|
|
22
|
-
| `
|
|
24
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | yes* | Claude Code OAuth token (starts with `sk-ant-oat`) |
|
|
23
25
|
| `ANTHROPIC_API_KEY` | yes* | Alternative — API key from console.anthropic.com |
|
|
24
26
|
| `ALLOWED_USER_IDS` | no | Comma-separated Telegram user IDs. Leave empty to allow anyone |
|
|
25
27
|
| `CWD` | no | Working directory for Claude Code. Defaults to current directory |
|
|
26
28
|
|
|
27
|
-
*One of `
|
|
28
|
-
|
|
29
|
-
## How to get your Claude Code token
|
|
29
|
+
*One of `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY` required.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
## Get your Claude Code token
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
npx @anthropic-ai/claude-code setup-token
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Opens a browser, logs in with your Anthropic account, prints a token starting with `sk-ant-oat`.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Get your Telegram user ID
|
|
40
40
|
|
|
41
|
-
Message [@userinfobot](https://t.me/userinfobot)
|
|
41
|
+
Message [@userinfobot](https://t.me/userinfobot) — it replies with your numeric ID.
|
|
42
42
|
|
|
43
43
|
## Bot commands
|
|
44
44
|
|
|
45
45
|
| Command | Action |
|
|
46
|
-
|
|
46
|
+
|---------|--------|
|
|
47
47
|
| `/start` or `/reset` | Kill current Claude session and start fresh |
|
|
48
48
|
| `/stop` | Interrupt the running Claude task |
|
|
49
49
|
| `/status` | Check if a session is active |
|
|
50
|
+
| `/help` | Show all available commands |
|
|
50
51
|
| `/cron every 1h <prompt>` | Schedule a recurring prompt |
|
|
51
|
-
| `/cron list` | Show active cron jobs |
|
|
52
|
+
| `/cron list` | Show active cron jobs (numbered) |
|
|
53
|
+
| `/cron edit <#> [schedule/prompt] <value>` | Edit a cron job in place |
|
|
52
54
|
| `/cron remove <id>` | Remove a specific cron job |
|
|
53
55
|
| `/cron clear` | Remove all cron jobs |
|
|
56
|
+
| `/reload_mcp` | Reload cc-agent MCP server without dropping your Claude session |
|
|
57
|
+
| `/mcp_version` | Show latest published cc-agent npm version and current cache |
|
|
58
|
+
| `/clear_npx_cache` | Clear npx cache and reload cc-agent (upgrades to latest version) |
|
|
59
|
+
| `/restart` | Self-restart the cc-tg bot process (no SSH needed) |
|
|
54
60
|
| Any text | Sent directly to Claude Code |
|
|
55
61
|
| Voice message | Transcribed via whisper.cpp and sent to Claude |
|
|
56
|
-
| Photo | Sent as native image input to Claude
|
|
57
|
-
| Document / file | Downloaded to `<CWD>/.cc-tg/uploads/`, path passed to Claude
|
|
62
|
+
| Photo | Sent as native image input to Claude |
|
|
63
|
+
| Document / file | Downloaded to `<CWD>/.cc-tg/uploads/`, path passed to Claude |
|
|
58
64
|
|
|
59
65
|
## Features
|
|
60
66
|
|
|
61
67
|
### Persistent sessions
|
|
62
|
-
Each Telegram chat ID gets its own isolated Claude Code subprocess. Sessions survive between messages — Claude remembers context
|
|
68
|
+
Each Telegram chat ID gets its own isolated Claude Code subprocess. Sessions survive between messages — Claude remembers context. `/reset` starts fresh.
|
|
63
69
|
|
|
64
70
|
### Voice messages
|
|
65
|
-
Send a voice message →
|
|
71
|
+
Send a voice message → transcribed via whisper.cpp → fed to Claude as text. Requires `whisper-cpp` and `ffmpeg` on the host.
|
|
66
72
|
|
|
67
73
|
### Images
|
|
68
|
-
Send a photo →
|
|
74
|
+
Send a photo → base64-encoded → sent to Claude as a native image content block. Claude sees the full image. Caption included as text.
|
|
69
75
|
|
|
70
76
|
### Documents
|
|
71
|
-
Send any file
|
|
77
|
+
Send any file → downloaded to `<CWD>/.cc-tg/uploads/<filename>` → Claude receives the path as `ATTACHMENTS: [filename](path)` and can read/process it directly. Works for PDFs, CSVs, code files, etc.
|
|
72
78
|
|
|
73
79
|
### File delivery
|
|
74
|
-
When Claude writes a file and mentions it in the response, the bot automatically uploads it to Telegram.
|
|
80
|
+
When Claude writes a file and mentions it in the response, the bot automatically uploads it to Telegram. Tracks `Write`/`Edit` tool calls during the session, cross-references with filenames in the final response.
|
|
75
81
|
|
|
76
82
|
### Cron jobs
|
|
77
|
-
Schedule recurring prompts
|
|
83
|
+
Schedule recurring prompts on a timer:
|
|
78
84
|
|
|
79
85
|
```
|
|
80
|
-
/cron every 1h check
|
|
81
|
-
/cron every 6h run
|
|
82
|
-
/cron every 30m ping the API and alert
|
|
86
|
+
/cron every 1h check logs and summarize new alerts
|
|
87
|
+
/cron every 6h run market scan and save to daily-report.md
|
|
88
|
+
/cron every 30m ping the API and alert if anything looks off
|
|
83
89
|
```
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
Edit without removing and re-adding:
|
|
92
|
+
```
|
|
93
|
+
/cron edit 1 every 2h updated task description
|
|
94
|
+
/cron edit 1 schedule every 4h
|
|
95
|
+
/cron edit 1 prompt new task text only
|
|
96
|
+
```
|
|
86
97
|
|
|
87
|
-
|
|
88
|
-
While Claude is working, the bot sends a continuous typing indicator so you know it's active.
|
|
98
|
+
Cron jobs persist to `<CWD>/.cc-tg/crons.json` and restore on restart. Output is prefixed with `CRON: <prompt>`. Files written by cron jobs are uploaded automatically.
|
|
89
99
|
|
|
90
|
-
###
|
|
91
|
-
|
|
100
|
+
### MCP management commands
|
|
101
|
+
Manage the cc-agent MCP server from Telegram without SSH:
|
|
92
102
|
|
|
93
|
-
|
|
103
|
+
- `/reload_mcp` — sends SIGTERM to the cc-agent process; Claude Code auto-restarts it on next tool call. Useful after updating cc-agent config.
|
|
104
|
+
- `/mcp_version` — shows the latest `@gonzih/cc-agent` version on npm and what's in your local npx cache.
|
|
105
|
+
- `/clear_npx_cache` — deletes `~/.npm/_npx/` and kills cc-agent, forcing a fresh download of the latest version on next use.
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
### Self-restart
|
|
108
|
+
`/restart` — spawns a detached child process with the same Node binary and args, sends you a confirmation message, then exits. The new process inherits all environment variables. No SSH required to restart the bot after updates.
|
|
109
|
+
|
|
110
|
+
### Typing indicator
|
|
111
|
+
While Claude is working, the bot sends a continuous typing indicator. Works for both regular messages and cron job execution.
|
|
112
|
+
|
|
113
|
+
### Bot command menu
|
|
114
|
+
All commands are registered with Telegram's `/` menu via `setMyCommands` on startup — no need to remember commands.
|
|
96
115
|
|
|
97
116
|
## Run persistently
|
|
98
117
|
|
|
@@ -115,7 +134,7 @@ Spawns a `claude` CLI subprocess per chat session using the stream-JSON protocol
|
|
|
115
134
|
<dict>
|
|
116
135
|
<key>TELEGRAM_BOT_TOKEN</key>
|
|
117
136
|
<string>your_token</string>
|
|
118
|
-
<key>
|
|
137
|
+
<key>CLAUDE_CODE_OAUTH_TOKEN</key>
|
|
119
138
|
<string>your_claude_token</string>
|
|
120
139
|
<key>ALLOWED_USER_IDS</key>
|
|
121
140
|
<string>your_telegram_id</string>
|
|
@@ -152,7 +171,7 @@ Description=cc-tg Claude Code Telegram bot
|
|
|
152
171
|
|
|
153
172
|
[Service]
|
|
154
173
|
Environment=TELEGRAM_BOT_TOKEN=xxx
|
|
155
|
-
Environment=
|
|
174
|
+
Environment=CLAUDE_CODE_OAUTH_TOKEN=yyy
|
|
156
175
|
Environment=ALLOWED_USER_IDS=123456789
|
|
157
176
|
Environment=CWD=/home/you/your-project
|
|
158
177
|
WorkingDirectory=/home/you/your-project
|
|
@@ -168,3 +187,7 @@ WantedBy=multi-user.target
|
|
|
168
187
|
- Node.js 18+
|
|
169
188
|
- `claude` CLI: `npm install -g @anthropic-ai/claude-code`
|
|
170
189
|
- Voice transcription (optional): `whisper-cpp` + `ffmpeg`
|
|
190
|
+
|
|
191
|
+
## Related
|
|
192
|
+
|
|
193
|
+
- [cc-agent](https://github.com/Gonzih/cc-agent) — MCP server for spawning Claude Code subagents by [@Gonzih](https://github.com/Gonzih)
|
package/dist/bot.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import TelegramBot from "node-telegram-bot-api";
|
|
6
6
|
import { existsSync, createWriteStream, mkdirSync } from "fs";
|
|
7
7
|
import { resolve, basename, join } from "path";
|
|
8
|
+
import os from "os";
|
|
8
9
|
import { execSync, spawn } from "child_process";
|
|
9
10
|
import https from "https";
|
|
10
11
|
import http from "http";
|
|
@@ -334,21 +335,44 @@ export class CcTgBot {
|
|
|
334
335
|
if (block.type !== "tool_use")
|
|
335
336
|
continue;
|
|
336
337
|
const name = block.name;
|
|
337
|
-
if (!["Write", "Edit", "NotebookEdit"].includes(name))
|
|
338
|
-
continue;
|
|
339
338
|
const input = block.input;
|
|
340
339
|
if (!input)
|
|
341
340
|
continue;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
341
|
+
if (["Write", "Edit", "NotebookEdit"].includes(name)) {
|
|
342
|
+
// Write tool uses file_path, Edit uses file_path
|
|
343
|
+
const filePath = input.file_path ?? input.path;
|
|
344
|
+
if (!filePath)
|
|
345
|
+
continue;
|
|
346
|
+
// Resolve relative paths against cwd
|
|
347
|
+
const resolved = filePath.startsWith("/")
|
|
348
|
+
? filePath
|
|
349
|
+
: resolve(cwd ?? process.cwd(), filePath);
|
|
350
|
+
console.log(`[claude:files] tracked written file: ${resolved}`);
|
|
351
|
+
session.writtenFiles.add(resolved);
|
|
352
|
+
}
|
|
353
|
+
else if (name === "Bash") {
|
|
354
|
+
const cmd = input.command ?? "";
|
|
355
|
+
// yt-dlp / ffmpeg -o "path"
|
|
356
|
+
const oFlag = cmd.match(/-o\s+["']?([^\s"']+\.[\w]{1,10})["']?/);
|
|
357
|
+
if (oFlag)
|
|
358
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), oFlag[1]));
|
|
359
|
+
// mv source dest — track dest
|
|
360
|
+
const mvMatch = cmd.match(/\bmv\s+\S+\s+["']?([^\s"']+)["']?$/);
|
|
361
|
+
if (mvMatch)
|
|
362
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), mvMatch[1]));
|
|
363
|
+
// cp source dest — track dest
|
|
364
|
+
const cpMatch = cmd.match(/\bcp\s+\S+\s+["']?([^\s"']+)["']?$/);
|
|
365
|
+
if (cpMatch)
|
|
366
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), cpMatch[1]));
|
|
367
|
+
// curl -o path or wget -O path
|
|
368
|
+
const curlMatch = cmd.match(/curl\s+.*?-o\s+["']?([^\s"']+)["']?/);
|
|
369
|
+
if (curlMatch)
|
|
370
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), curlMatch[1]));
|
|
371
|
+
// wget -O path
|
|
372
|
+
const wgetMatch = cmd.match(/wget\s+.*?-O\s+["']?([^\s"']+)["']?/);
|
|
373
|
+
if (wgetMatch)
|
|
374
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), wgetMatch[1]));
|
|
375
|
+
}
|
|
352
376
|
}
|
|
353
377
|
}
|
|
354
378
|
isSensitiveFile(filePath) {
|
|
@@ -362,8 +386,6 @@ export class CcTgBot {
|
|
|
362
386
|
return sensitivePatterns.some((p) => p.test(name));
|
|
363
387
|
}
|
|
364
388
|
uploadMentionedFiles(chatId, resultText, session) {
|
|
365
|
-
if (session.writtenFiles.size === 0)
|
|
366
|
-
return;
|
|
367
389
|
// Extract file path candidates from result text
|
|
368
390
|
// Match: /absolute/path/file.ext or relative like ./foo/bar.csv or just foo.pdf
|
|
369
391
|
const pathPattern = /(?:^|[\s`'"(])(\/?[\w.\-/]+\.[\w]{1,10})(?:[\s`'")\n]|$)/gm;
|
|
@@ -372,24 +394,38 @@ export class CcTgBot {
|
|
|
372
394
|
while ((match = pathPattern.exec(resultText)) !== null) {
|
|
373
395
|
candidates.add(match[1]);
|
|
374
396
|
}
|
|
397
|
+
const safeDirs = ["/tmp/", "/var/folders/", os.homedir() + "/Downloads/"];
|
|
398
|
+
const isSafeDir = (p) => safeDirs.some(d => p.startsWith(d)) || p.startsWith(this.opts.cwd ?? process.cwd());
|
|
375
399
|
const toUpload = [];
|
|
400
|
+
if (session.writtenFiles.size > 0) {
|
|
401
|
+
for (const candidate of candidates) {
|
|
402
|
+
// Try as-is (absolute), or resolve against cwd
|
|
403
|
+
const resolved = candidate.startsWith("/")
|
|
404
|
+
? candidate
|
|
405
|
+
: resolve(this.opts.cwd ?? process.cwd(), candidate);
|
|
406
|
+
if (session.writtenFiles.has(resolved) && existsSync(resolved)) {
|
|
407
|
+
toUpload.push(resolved);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// Also check by basename — result might mention just the filename
|
|
411
|
+
for (const written of session.writtenFiles) {
|
|
412
|
+
if (basename(written) === basename(candidate) && existsSync(written)) {
|
|
413
|
+
toUpload.push(written);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Also upload files mentioned in result text that exist in safe dirs
|
|
421
|
+
// even if not tracked via Write tool
|
|
376
422
|
for (const candidate of candidates) {
|
|
377
|
-
// Try as-is (absolute), or resolve against cwd
|
|
378
423
|
const resolved = candidate.startsWith("/")
|
|
379
424
|
? candidate
|
|
380
425
|
: resolve(this.opts.cwd ?? process.cwd(), candidate);
|
|
381
|
-
if (
|
|
426
|
+
if (existsSync(resolved) && isSafeDir(resolved) && !toUpload.includes(resolved)) {
|
|
382
427
|
toUpload.push(resolved);
|
|
383
428
|
}
|
|
384
|
-
else {
|
|
385
|
-
// Also check by basename — result might mention just the filename
|
|
386
|
-
for (const written of session.writtenFiles) {
|
|
387
|
-
if (basename(written) === basename(candidate) && existsSync(written)) {
|
|
388
|
-
toUpload.push(written);
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
429
|
}
|
|
394
430
|
// Deduplicate and filter sensitive files
|
|
395
431
|
const unique = [...new Set(toUpload)];
|
|
@@ -616,12 +652,17 @@ export class CcTgBot {
|
|
|
616
652
|
await this.bot.sendMessage(chatId, "Restarting bot... brb.");
|
|
617
653
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
618
654
|
this.stop();
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
655
|
+
const isLaunchd = process.ppid === 1;
|
|
656
|
+
if (!isLaunchd) {
|
|
657
|
+
// Running manually — spawn a replacement process
|
|
658
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
659
|
+
detached: true,
|
|
660
|
+
stdio: "ignore",
|
|
661
|
+
env: process.env,
|
|
662
|
+
});
|
|
663
|
+
child.unref();
|
|
664
|
+
}
|
|
665
|
+
// If launchd-managed, just exit — launchd will restart us
|
|
625
666
|
process.exit(0);
|
|
626
667
|
}
|
|
627
668
|
killSession(chatId, keepCrons = true) {
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,33 @@
|
|
|
13
13
|
* ALLOWED_USER_IDS — comma-separated Telegram user IDs (leave empty to allow all)
|
|
14
14
|
* CWD — working directory for Claude Code (default: process.cwd())
|
|
15
15
|
*/
|
|
16
|
+
import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
16
19
|
import { CcTgBot } from "./bot.js";
|
|
20
|
+
const LOCK_FILE = join(tmpdir(), "cc-tg.lock");
|
|
21
|
+
function acquireLock() {
|
|
22
|
+
if (existsSync(LOCK_FILE)) {
|
|
23
|
+
try {
|
|
24
|
+
const pid = parseInt(readFileSync(LOCK_FILE, "utf8").trim());
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
console.error(`[cc-tg] Another instance is already running (PID ${pid}). Exiting.`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// PID is dead — stale lock, take over
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
writeFileSync(LOCK_FILE, String(process.pid));
|
|
34
|
+
process.on("exit", () => { try {
|
|
35
|
+
unlinkSync(LOCK_FILE);
|
|
36
|
+
}
|
|
37
|
+
catch { } });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (!acquireLock()) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
17
43
|
function required(name) {
|
|
18
44
|
const val = process.env[name];
|
|
19
45
|
if (!val) {
|