@gonzih/cc-tg 0.2.4 → 0.2.6
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 +8 -0
- package/dist/bot.d.ts +3 -0
- package/dist/bot.js +127 -4
- package/dist/cron.d.ts +4 -0
- package/dist/cron.js +24 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ Message [@userinfobot](https://t.me/userinfobot) on Telegram — it replies with
|
|
|
53
53
|
| `/cron clear` | Remove all cron jobs |
|
|
54
54
|
| Any text | Sent directly to Claude Code |
|
|
55
55
|
| Voice message | Transcribed via whisper.cpp and sent to Claude |
|
|
56
|
+
| Photo | Sent as native image input to Claude (base64 content block) |
|
|
57
|
+
| Document / file | Downloaded to `<CWD>/.cc-tg/uploads/`, path passed to Claude as `ATTACHMENTS: [name](path)` |
|
|
56
58
|
|
|
57
59
|
## Features
|
|
58
60
|
|
|
@@ -62,6 +64,12 @@ Each Telegram chat ID gets its own isolated Claude Code subprocess. Sessions sur
|
|
|
62
64
|
### Voice messages
|
|
63
65
|
Send a voice message → automatically transcribed via whisper.cpp → fed into Claude as text. Requires `whisper-cpp` and `ffmpeg` installed on the host.
|
|
64
66
|
|
|
67
|
+
### Images
|
|
68
|
+
Send a photo → downloaded and base64-encoded → sent to Claude as a native image content block via the stream-JSON protocol. Claude sees the full image, no intermediate vision step. Caption (if any) is included as text alongside the image.
|
|
69
|
+
|
|
70
|
+
### Documents
|
|
71
|
+
Send any file as a document → 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
|
+
|
|
65
73
|
### File delivery
|
|
66
74
|
When Claude writes a file and mentions it in the response, the bot automatically uploads it to Telegram. Hybrid detection: tracks `Write`/`Edit` tool calls during the session, cross-references with filenames mentioned in the final response.
|
|
67
75
|
|
package/dist/bot.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class CcTgBot {
|
|
|
14
14
|
private opts;
|
|
15
15
|
private cron;
|
|
16
16
|
constructor(opts: BotOptions);
|
|
17
|
+
private registerBotCommands;
|
|
17
18
|
private isAllowed;
|
|
18
19
|
private handleTelegram;
|
|
19
20
|
private handleVoice;
|
|
@@ -25,9 +26,11 @@ export declare class CcTgBot {
|
|
|
25
26
|
private stopTyping;
|
|
26
27
|
private flushPending;
|
|
27
28
|
private trackWrittenFiles;
|
|
29
|
+
private isSensitiveFile;
|
|
28
30
|
private uploadMentionedFiles;
|
|
29
31
|
private extractToolName;
|
|
30
32
|
private handleCron;
|
|
33
|
+
private handleCronEdit;
|
|
31
34
|
private killSession;
|
|
32
35
|
stop(): void;
|
|
33
36
|
}
|
package/dist/bot.js
CHANGED
|
@@ -10,6 +10,14 @@ import http from "http";
|
|
|
10
10
|
import { ClaudeProcess, extractText } from "./claude.js";
|
|
11
11
|
import { transcribeVoice, isVoiceAvailable } from "./voice.js";
|
|
12
12
|
import { CronManager } from "./cron.js";
|
|
13
|
+
const BOT_COMMANDS = [
|
|
14
|
+
{ command: "start", description: "Reset session and start fresh" },
|
|
15
|
+
{ command: "reset", description: "Reset Claude session" },
|
|
16
|
+
{ command: "stop", description: "Stop the current Claude task" },
|
|
17
|
+
{ command: "status", description: "Check if a session is active" },
|
|
18
|
+
{ command: "help", description: "Show all available commands" },
|
|
19
|
+
{ command: "cron", description: "Manage cron jobs — add/list/edit/remove/clear" },
|
|
20
|
+
];
|
|
13
21
|
const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
|
|
14
22
|
const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
|
|
15
23
|
export class CcTgBot {
|
|
@@ -35,9 +43,15 @@ export class CcTgBot {
|
|
|
35
43
|
console.error(`[cron] failed to fire for chat=${chatId}:`, err.message);
|
|
36
44
|
}
|
|
37
45
|
});
|
|
46
|
+
this.registerBotCommands();
|
|
38
47
|
console.log("cc-tg bot started");
|
|
39
48
|
console.log(`[voice] whisper available: ${isVoiceAvailable()}`);
|
|
40
49
|
}
|
|
50
|
+
registerBotCommands() {
|
|
51
|
+
this.bot.setMyCommands(BOT_COMMANDS)
|
|
52
|
+
.then(() => console.log("[tg] bot commands registered"))
|
|
53
|
+
.catch((err) => console.error("[tg] setMyCommands failed:", err.message));
|
|
54
|
+
}
|
|
41
55
|
isAllowed(userId) {
|
|
42
56
|
if (!this.opts.allowedUserIds?.length)
|
|
43
57
|
return true;
|
|
@@ -81,6 +95,12 @@ export class CcTgBot {
|
|
|
81
95
|
await this.bot.sendMessage(chatId, has ? "Stopped." : "No active session.");
|
|
82
96
|
return;
|
|
83
97
|
}
|
|
98
|
+
// /help — list all commands
|
|
99
|
+
if (text === "/help") {
|
|
100
|
+
const lines = BOT_COMMANDS.map((c) => `/${c.command} — ${c.description}`);
|
|
101
|
+
await this.bot.sendMessage(chatId, lines.join("\n"));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
84
104
|
// /status
|
|
85
105
|
if (text === "/status") {
|
|
86
106
|
const has = this.sessions.has(chatId);
|
|
@@ -306,6 +326,16 @@ export class CcTgBot {
|
|
|
306
326
|
session.writtenFiles.add(resolved);
|
|
307
327
|
}
|
|
308
328
|
}
|
|
329
|
+
isSensitiveFile(filePath) {
|
|
330
|
+
const name = basename(filePath).toLowerCase();
|
|
331
|
+
const sensitivePatterns = [
|
|
332
|
+
/credential/i, /secret/i, /password/i, /passwd/i, /\.env/i,
|
|
333
|
+
/api[_-]?key/i, /token/i, /private[_-]?key/i, /id_rsa/i,
|
|
334
|
+
/\.pem$/i, /\.key$/i, /\.pfx$/i, /\.p12$/i,
|
|
335
|
+
/gmail/i, /oauth/i, /auth/i,
|
|
336
|
+
];
|
|
337
|
+
return sensitivePatterns.some((p) => p.test(name));
|
|
338
|
+
}
|
|
309
339
|
uploadMentionedFiles(chatId, resultText, session) {
|
|
310
340
|
if (session.writtenFiles.size === 0)
|
|
311
341
|
return;
|
|
@@ -336,9 +366,13 @@ export class CcTgBot {
|
|
|
336
366
|
}
|
|
337
367
|
}
|
|
338
368
|
}
|
|
339
|
-
// Deduplicate
|
|
369
|
+
// Deduplicate and filter sensitive files
|
|
340
370
|
const unique = [...new Set(toUpload)];
|
|
341
371
|
for (const filePath of unique) {
|
|
372
|
+
if (this.isSensitiveFile(filePath)) {
|
|
373
|
+
console.log(`[claude:files] skipping sensitive file: ${filePath}`);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
342
376
|
console.log(`[claude:files] uploading to telegram: ${filePath}`);
|
|
343
377
|
this.bot.sendDocument(chatId, filePath).catch((err) => console.error(`[tg:${chatId}] sendDocument failed for ${filePath}:`, err.message));
|
|
344
378
|
}
|
|
@@ -364,8 +398,11 @@ export class CcTgBot {
|
|
|
364
398
|
await this.bot.sendMessage(chatId, "No cron jobs.");
|
|
365
399
|
return;
|
|
366
400
|
}
|
|
367
|
-
const lines = jobs.map((j) =>
|
|
368
|
-
|
|
401
|
+
const lines = jobs.map((j, i) => {
|
|
402
|
+
const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
|
|
403
|
+
return `#${i + 1} ${j.schedule} — "${short}"`;
|
|
404
|
+
});
|
|
405
|
+
await this.bot.sendMessage(chatId, `Cron jobs (${jobs.length}):\n${lines.join("\n")}`);
|
|
369
406
|
return;
|
|
370
407
|
}
|
|
371
408
|
// /cron clear
|
|
@@ -381,10 +418,15 @@ export class CcTgBot {
|
|
|
381
418
|
await this.bot.sendMessage(chatId, ok ? `Removed ${id}.` : `Not found: ${id}`);
|
|
382
419
|
return;
|
|
383
420
|
}
|
|
421
|
+
// /cron edit [<#> ...]
|
|
422
|
+
if (args === "edit" || args.startsWith("edit ")) {
|
|
423
|
+
await this.handleCronEdit(chatId, args.slice("edit".length).trim());
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
384
426
|
// /cron every 1h <prompt>
|
|
385
427
|
const scheduleMatch = args.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
|
|
386
428
|
if (!scheduleMatch) {
|
|
387
|
-
await this.bot.sendMessage(chatId, "Usage:\n/cron every 1h <prompt>\n/cron list\n/cron remove <id>\n/cron clear");
|
|
429
|
+
await this.bot.sendMessage(chatId, "Usage:\n/cron every 1h <prompt>\n/cron list\n/cron edit\n/cron remove <id>\n/cron clear");
|
|
388
430
|
return;
|
|
389
431
|
}
|
|
390
432
|
const schedule = scheduleMatch[1];
|
|
@@ -396,6 +438,87 @@ export class CcTgBot {
|
|
|
396
438
|
}
|
|
397
439
|
await this.bot.sendMessage(chatId, `Cron set [${job.id}]: ${schedule} — "${prompt}"`);
|
|
398
440
|
}
|
|
441
|
+
async handleCronEdit(chatId, editArgs) {
|
|
442
|
+
const jobs = this.cron.list(chatId);
|
|
443
|
+
// No args — show numbered list with edit instructions
|
|
444
|
+
if (!editArgs) {
|
|
445
|
+
if (!jobs.length) {
|
|
446
|
+
await this.bot.sendMessage(chatId, "No cron jobs to edit.");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const lines = jobs.map((j, i) => {
|
|
450
|
+
const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
|
|
451
|
+
return `#${i + 1} ${j.schedule} — "${short}"`;
|
|
452
|
+
});
|
|
453
|
+
await this.bot.sendMessage(chatId, `Cron jobs:\n${lines.join("\n")}\n\n` +
|
|
454
|
+
"Edit options:\n" +
|
|
455
|
+
"/cron edit <#> every <N><unit> <new prompt>\n" +
|
|
456
|
+
"/cron edit <#> schedule every <N><unit>\n" +
|
|
457
|
+
"/cron edit <#> prompt <new prompt>");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Expect: <index> <rest>
|
|
461
|
+
const indexMatch = editArgs.match(/^(\d+)\s+(.+)$/);
|
|
462
|
+
if (!indexMatch) {
|
|
463
|
+
await this.bot.sendMessage(chatId, "Usage: /cron edit <#> every <N><unit> <new prompt>");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const index = parseInt(indexMatch[1], 10) - 1;
|
|
467
|
+
if (index < 0 || index >= jobs.length) {
|
|
468
|
+
await this.bot.sendMessage(chatId, `Invalid job number. Use /cron edit to see the list.`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const job = jobs[index];
|
|
472
|
+
const editCmd = indexMatch[2];
|
|
473
|
+
// /cron edit <#> schedule every <N><unit>
|
|
474
|
+
if (editCmd.startsWith("schedule ")) {
|
|
475
|
+
const newSchedule = editCmd.slice("schedule ".length).trim();
|
|
476
|
+
const result = this.cron.update(chatId, job.id, { schedule: newSchedule });
|
|
477
|
+
if (result === null) {
|
|
478
|
+
await this.bot.sendMessage(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d");
|
|
479
|
+
}
|
|
480
|
+
else if (result === false) {
|
|
481
|
+
await this.bot.sendMessage(chatId, "Job not found.");
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
await this.bot.sendMessage(chatId, `#${index + 1} schedule updated to ${newSchedule}.`);
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// /cron edit <#> prompt <new-prompt>
|
|
489
|
+
if (editCmd.startsWith("prompt ")) {
|
|
490
|
+
const newPrompt = editCmd.slice("prompt ".length).trim();
|
|
491
|
+
const result = this.cron.update(chatId, job.id, { prompt: newPrompt });
|
|
492
|
+
if (result === false) {
|
|
493
|
+
await this.bot.sendMessage(chatId, "Job not found.");
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
await this.bot.sendMessage(chatId, `#${index + 1} prompt updated to "${newPrompt}".`);
|
|
497
|
+
}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// /cron edit <#> every <N><unit> <new-prompt>
|
|
501
|
+
const fullMatch = editCmd.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
|
|
502
|
+
if (fullMatch) {
|
|
503
|
+
const newSchedule = fullMatch[1];
|
|
504
|
+
const newPrompt = fullMatch[2];
|
|
505
|
+
const result = this.cron.update(chatId, job.id, { schedule: newSchedule, prompt: newPrompt });
|
|
506
|
+
if (result === null) {
|
|
507
|
+
await this.bot.sendMessage(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d");
|
|
508
|
+
}
|
|
509
|
+
else if (result === false) {
|
|
510
|
+
await this.bot.sendMessage(chatId, "Job not found.");
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
await this.bot.sendMessage(chatId, `#${index + 1} updated: ${newSchedule} — "${newPrompt}"`);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
await this.bot.sendMessage(chatId, "Edit options:\n" +
|
|
518
|
+
"/cron edit <#> every <N><unit> <new prompt>\n" +
|
|
519
|
+
"/cron edit <#> schedule every <N><unit>\n" +
|
|
520
|
+
"/cron edit <#> prompt <new prompt>");
|
|
521
|
+
}
|
|
399
522
|
killSession(chatId, keepCrons = true) {
|
|
400
523
|
const session = this.sessions.get(chatId);
|
|
401
524
|
if (session) {
|
package/dist/cron.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export declare class CronManager {
|
|
|
23
23
|
remove(chatId: number, id: string): boolean;
|
|
24
24
|
clearAll(chatId: number): number;
|
|
25
25
|
list(chatId: number): CronJob[];
|
|
26
|
+
update(chatId: number, id: string, updates: {
|
|
27
|
+
schedule?: string;
|
|
28
|
+
prompt?: string;
|
|
29
|
+
}): CronJob | null | false;
|
|
26
30
|
private persist;
|
|
27
31
|
private load;
|
|
28
32
|
}
|
package/dist/cron.js
CHANGED
|
@@ -70,6 +70,30 @@ export class CronManager {
|
|
|
70
70
|
.filter((j) => j.chatId === chatId)
|
|
71
71
|
.map(({ timer: _t, ...j }) => j);
|
|
72
72
|
}
|
|
73
|
+
update(chatId, id, updates) {
|
|
74
|
+
const job = this.jobs.get(id);
|
|
75
|
+
if (!job || job.chatId !== chatId)
|
|
76
|
+
return false;
|
|
77
|
+
if (updates.schedule !== undefined) {
|
|
78
|
+
const intervalMs = CronManager.parseSchedule(updates.schedule);
|
|
79
|
+
if (!intervalMs)
|
|
80
|
+
return null;
|
|
81
|
+
job.intervalMs = intervalMs;
|
|
82
|
+
job.schedule = updates.schedule;
|
|
83
|
+
}
|
|
84
|
+
if (updates.prompt !== undefined) {
|
|
85
|
+
job.prompt = updates.prompt;
|
|
86
|
+
}
|
|
87
|
+
// Recreate timer so it uses updated intervalMs and always reads latest job.prompt
|
|
88
|
+
clearInterval(job.timer);
|
|
89
|
+
job.timer = setInterval(() => {
|
|
90
|
+
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
91
|
+
this.fire(job.chatId, job.prompt);
|
|
92
|
+
}, job.intervalMs);
|
|
93
|
+
this.persist();
|
|
94
|
+
const { timer: _t, ...cronJob } = job;
|
|
95
|
+
return cronJob;
|
|
96
|
+
}
|
|
73
97
|
persist() {
|
|
74
98
|
try {
|
|
75
99
|
const dir = join(this.storePath, "..");
|