@gonzih/cc-tg 0.4.1 → 0.5.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/README.md +32 -0
- package/dist/bot.d.ts +10 -0
- package/dist/bot.js +181 -118
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Open your bot in Telegram and start chatting.
|
|
|
29
29
|
| `ANTHROPIC_API_KEY` | yes* | Alternative — API key from console.anthropic.com |
|
|
30
30
|
| `ALLOWED_USER_IDS` | no | Comma-separated Telegram user IDs. Leave empty to allow anyone |
|
|
31
31
|
| `CWD` | no | Working directory for Claude Code. Defaults to current directory |
|
|
32
|
+
| `THREAD_CWD_MAP` | no | JSON mapping of forum topic names or IDs to CWD paths (see [Multi-topic sessions](#multi-topic-sessions)) |
|
|
32
33
|
|
|
33
34
|
*One of `CLAUDE_CODE_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_API_KEY` required.
|
|
34
35
|
|
|
@@ -118,6 +119,37 @@ Manage the cc-agent MCP server from Telegram without SSH:
|
|
|
118
119
|
### Self-restart
|
|
119
120
|
`/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.
|
|
120
121
|
|
|
122
|
+
### Multi-topic sessions
|
|
123
|
+
|
|
124
|
+
When you use cc-tg in a **Telegram group with Topics enabled** (a "Forum" group), each topic gets its own **isolated Claude Code session**. One bot token, one daemon, unlimited isolated project contexts.
|
|
125
|
+
|
|
126
|
+
**How it works:**
|
|
127
|
+
- Session key = `chatId:threadId` for forum topics
|
|
128
|
+
- Session key = `chatId:main` for direct messages and non-topic groups (backward compatible)
|
|
129
|
+
- Commands like `/reset`, `/stop`, `/status` are scoped to the current topic
|
|
130
|
+
|
|
131
|
+
**Setup:**
|
|
132
|
+
1. Create a Telegram group → Settings → Topics → Enable
|
|
133
|
+
2. Create topics for each project (e.g. "Simorgh", "LeWM", "EcoClaw")
|
|
134
|
+
3. Each topic now has its own isolated Claude context
|
|
135
|
+
|
|
136
|
+
**Optional: route topics to different working directories**
|
|
137
|
+
|
|
138
|
+
Set `THREAD_CWD_MAP` to a JSON string mapping topic names (or thread IDs) to absolute paths:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
THREAD_CWD_MAP='{"Simorgh":"/Users/you/simorgh-app","LeWM":"/Users/you/le-wm","EcoClaw":"/Users/you/ecoclaw"}'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When cc-tg creates a new session for a topic, it looks up the topic name in this map and starts Claude in the corresponding directory. If no match is found, falls back to `CWD`.
|
|
145
|
+
|
|
146
|
+
You can also map by numeric thread ID:
|
|
147
|
+
```bash
|
|
148
|
+
THREAD_CWD_MAP='{"12345":"/Users/you/project-a","67890":"/Users/you/project-b"}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
If `THREAD_CWD_MAP` is not set, all topics share the same CWD — context isolation still works, just without directory routing.
|
|
152
|
+
|
|
121
153
|
### Typing indicator
|
|
122
154
|
While Claude is working, the bot sends a continuous typing indicator. Works for both regular messages and cron job execution.
|
|
123
155
|
|
package/dist/bot.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare class CcTgBot {
|
|
|
21
21
|
private botId;
|
|
22
22
|
constructor(opts: BotOptions);
|
|
23
23
|
private registerBotCommands;
|
|
24
|
+
/** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
|
|
25
|
+
private sessionKey;
|
|
26
|
+
/**
|
|
27
|
+
* Send a message back to the correct thread (or plain chat if no thread).
|
|
28
|
+
* When threadId is undefined, calls sendMessage with exactly 2 args to preserve
|
|
29
|
+
* backward-compatible call signatures (no extra options object).
|
|
30
|
+
*/
|
|
31
|
+
private replyToChat;
|
|
32
|
+
/** Parse THREAD_CWD_MAP env var — maps thread name or thread_id to a CWD path */
|
|
33
|
+
private getThreadCwdMap;
|
|
24
34
|
private isAllowed;
|
|
25
35
|
private handleTelegram;
|
|
26
36
|
private handleVoice;
|
package/dist/bot.js
CHANGED
|
@@ -185,6 +185,37 @@ export class CcTgBot {
|
|
|
185
185
|
.then(() => console.log("[tg] bot commands registered"))
|
|
186
186
|
.catch((err) => console.error("[tg] setMyCommands failed:", err.message));
|
|
187
187
|
}
|
|
188
|
+
/** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
|
|
189
|
+
sessionKey(chatId, threadId) {
|
|
190
|
+
return `${chatId}:${threadId ?? 'main'}`;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Send a message back to the correct thread (or plain chat if no thread).
|
|
194
|
+
* When threadId is undefined, calls sendMessage with exactly 2 args to preserve
|
|
195
|
+
* backward-compatible call signatures (no extra options object).
|
|
196
|
+
*/
|
|
197
|
+
replyToChat(chatId, text, threadId, opts) {
|
|
198
|
+
if (threadId !== undefined) {
|
|
199
|
+
return this.bot.sendMessage(chatId, text, { ...opts, message_thread_id: threadId });
|
|
200
|
+
}
|
|
201
|
+
if (opts) {
|
|
202
|
+
return this.bot.sendMessage(chatId, text, opts);
|
|
203
|
+
}
|
|
204
|
+
return this.bot.sendMessage(chatId, text);
|
|
205
|
+
}
|
|
206
|
+
/** Parse THREAD_CWD_MAP env var — maps thread name or thread_id to a CWD path */
|
|
207
|
+
getThreadCwdMap() {
|
|
208
|
+
const raw = process.env.THREAD_CWD_MAP;
|
|
209
|
+
if (!raw)
|
|
210
|
+
return {};
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(raw);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
console.warn('[cc-tg] THREAD_CWD_MAP is not valid JSON, ignoring');
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
188
219
|
isAllowed(userId) {
|
|
189
220
|
if (!this.opts.allowedUserIds?.length)
|
|
190
221
|
return true;
|
|
@@ -193,8 +224,16 @@ export class CcTgBot {
|
|
|
193
224
|
async handleTelegram(msg) {
|
|
194
225
|
const chatId = msg.chat.id;
|
|
195
226
|
const userId = msg.from?.id ?? chatId;
|
|
227
|
+
// Forum topic thread_id — undefined for DMs and non-topic group messages
|
|
228
|
+
const threadId = msg.message_thread_id;
|
|
229
|
+
// Thread name is available on the service message that creates a new topic.
|
|
230
|
+
// forum_topic_created is not in older @types/node-telegram-bot-api versions, so cast via unknown.
|
|
231
|
+
const rawMsg = msg;
|
|
232
|
+
const threadName = rawMsg.forum_topic_created
|
|
233
|
+
? rawMsg.forum_topic_created.name
|
|
234
|
+
: undefined;
|
|
196
235
|
if (!this.isAllowed(userId)) {
|
|
197
|
-
await this.
|
|
236
|
+
await this.replyToChat(chatId, "Not authorized.", threadId);
|
|
198
237
|
return;
|
|
199
238
|
}
|
|
200
239
|
// Group chat handling
|
|
@@ -215,17 +254,17 @@ export class CcTgBot {
|
|
|
215
254
|
}
|
|
216
255
|
// Voice message — transcribe then feed as text
|
|
217
256
|
if (msg.voice || msg.audio) {
|
|
218
|
-
await this.handleVoice(chatId, msg);
|
|
257
|
+
await this.handleVoice(chatId, msg, threadId, threadName);
|
|
219
258
|
return;
|
|
220
259
|
}
|
|
221
260
|
// Photo — send as base64 image content block to Claude
|
|
222
261
|
if (msg.photo?.length) {
|
|
223
|
-
await this.handlePhoto(chatId, msg);
|
|
262
|
+
await this.handlePhoto(chatId, msg, threadId, threadName);
|
|
224
263
|
return;
|
|
225
264
|
}
|
|
226
265
|
// Document — download to CWD/.cc-tg/uploads/, tell Claude the path
|
|
227
266
|
if (msg.document) {
|
|
228
|
-
await this.handleDocument(chatId, msg);
|
|
267
|
+
await this.handleDocument(chatId, msg, threadId, threadName);
|
|
229
268
|
return;
|
|
230
269
|
}
|
|
231
270
|
let text = msg.text?.trim();
|
|
@@ -235,68 +274,69 @@ export class CcTgBot {
|
|
|
235
274
|
if (this.botUsername) {
|
|
236
275
|
text = text.replace(new RegExp(`@${this.botUsername}\\s*`, "g"), "").trim();
|
|
237
276
|
}
|
|
277
|
+
const sessionKey = this.sessionKey(chatId, threadId);
|
|
238
278
|
// /start or /reset — kill existing session and ack
|
|
239
279
|
if (text === "/start" || text === "/reset") {
|
|
240
|
-
this.killSession(chatId);
|
|
241
|
-
await this.
|
|
280
|
+
this.killSession(chatId, true, threadId);
|
|
281
|
+
await this.replyToChat(chatId, "Session reset. Send a message to start.", threadId);
|
|
242
282
|
return;
|
|
243
283
|
}
|
|
244
284
|
// /stop — kill active session (interrupt running Claude task)
|
|
245
285
|
if (text === "/stop") {
|
|
246
|
-
const has = this.sessions.has(
|
|
247
|
-
this.killSession(chatId);
|
|
248
|
-
await this.
|
|
286
|
+
const has = this.sessions.has(sessionKey);
|
|
287
|
+
this.killSession(chatId, true, threadId);
|
|
288
|
+
await this.replyToChat(chatId, has ? "Stopped." : "No active session.", threadId);
|
|
249
289
|
return;
|
|
250
290
|
}
|
|
251
291
|
// /help — list all commands
|
|
252
292
|
if (text === "/help") {
|
|
253
293
|
const lines = BOT_COMMANDS.map((c) => `/${c.command} — ${c.description}`);
|
|
254
|
-
await this.
|
|
294
|
+
await this.replyToChat(chatId, lines.join("\n"), threadId);
|
|
255
295
|
return;
|
|
256
296
|
}
|
|
257
297
|
// /status
|
|
258
298
|
if (text === "/status") {
|
|
259
|
-
const has = this.sessions.has(
|
|
299
|
+
const has = this.sessions.has(sessionKey);
|
|
260
300
|
let status = has ? "Session active." : "No active session.";
|
|
261
301
|
const sleeping = this.pendingRetries.size;
|
|
262
302
|
if (sleeping > 0)
|
|
263
303
|
status += `\n⏸ ${sleeping} request(s) sleeping (usage limit).`;
|
|
264
|
-
await this.
|
|
304
|
+
await this.replyToChat(chatId, status, threadId);
|
|
265
305
|
return;
|
|
266
306
|
}
|
|
267
307
|
// /cron <schedule> <prompt> | /cron list | /cron clear | /cron remove <id>
|
|
268
308
|
if (text.startsWith("/cron")) {
|
|
269
|
-
await this.handleCron(chatId, text);
|
|
309
|
+
await this.handleCron(chatId, text, threadId);
|
|
270
310
|
return;
|
|
271
311
|
}
|
|
272
312
|
// /reload_mcp — kill cc-agent process so Claude Code auto-restarts it
|
|
273
313
|
if (text === "/reload_mcp") {
|
|
274
|
-
await this.handleReloadMcp(chatId);
|
|
314
|
+
await this.handleReloadMcp(chatId, threadId);
|
|
275
315
|
return;
|
|
276
316
|
}
|
|
277
317
|
// /mcp_status — run `claude mcp list` and show connection status
|
|
278
318
|
if (text === "/mcp_status") {
|
|
279
|
-
await this.handleMcpStatus(chatId);
|
|
319
|
+
await this.handleMcpStatus(chatId, threadId);
|
|
280
320
|
return;
|
|
281
321
|
}
|
|
282
322
|
// /mcp_version — show published npm version and cached npx entries
|
|
283
323
|
if (text === "/mcp_version") {
|
|
284
|
-
await this.handleMcpVersion(chatId);
|
|
324
|
+
await this.handleMcpVersion(chatId, threadId);
|
|
285
325
|
return;
|
|
286
326
|
}
|
|
287
327
|
// /clear_npx_cache — wipe ~/.npm/_npx/ then restart cc-agent
|
|
288
328
|
if (text === "/clear_npx_cache") {
|
|
289
|
-
await this.handleClearNpxCache(chatId);
|
|
329
|
+
await this.handleClearNpxCache(chatId, threadId);
|
|
290
330
|
return;
|
|
291
331
|
}
|
|
292
332
|
// /restart — restart the bot process in-place
|
|
293
333
|
if (text === "/restart") {
|
|
294
|
-
await this.handleRestart(chatId);
|
|
334
|
+
await this.handleRestart(chatId, threadId);
|
|
295
335
|
return;
|
|
296
336
|
}
|
|
297
337
|
// /get_file <path> — send a file from the server to the user
|
|
298
338
|
if (text.startsWith("/get_file")) {
|
|
299
|
-
await this.handleGetFile(chatId, text);
|
|
339
|
+
await this.handleGetFile(chatId, text, threadId);
|
|
300
340
|
return;
|
|
301
341
|
}
|
|
302
342
|
// /cost — show session token usage and cost
|
|
@@ -312,10 +352,10 @@ export class CcTgBot {
|
|
|
312
352
|
catch (err) {
|
|
313
353
|
console.error("[cost] cc-agent cost_summary failed:", err.message);
|
|
314
354
|
}
|
|
315
|
-
await this.
|
|
355
|
+
await this.replyToChat(chatId, reply, threadId);
|
|
316
356
|
return;
|
|
317
357
|
}
|
|
318
|
-
const session = this.getOrCreateSession(chatId);
|
|
358
|
+
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
319
359
|
try {
|
|
320
360
|
const prompt = buildPromptWithReplyContext(text, msg);
|
|
321
361
|
session.currentPrompt = prompt;
|
|
@@ -323,11 +363,11 @@ export class CcTgBot {
|
|
|
323
363
|
this.startTyping(chatId, session);
|
|
324
364
|
}
|
|
325
365
|
catch (err) {
|
|
326
|
-
await this.
|
|
327
|
-
this.killSession(chatId);
|
|
366
|
+
await this.replyToChat(chatId, `Error sending to Claude: ${err.message}`, threadId);
|
|
367
|
+
this.killSession(chatId, true, threadId);
|
|
328
368
|
}
|
|
329
369
|
}
|
|
330
|
-
async handleVoice(chatId, msg) {
|
|
370
|
+
async handleVoice(chatId, msg, threadId, threadName) {
|
|
331
371
|
const fileId = msg.voice?.file_id ?? msg.audio?.file_id;
|
|
332
372
|
if (!fileId)
|
|
333
373
|
return;
|
|
@@ -338,11 +378,11 @@ export class CcTgBot {
|
|
|
338
378
|
const transcript = await transcribeVoice(fileLink);
|
|
339
379
|
console.log(`[voice:${chatId}] transcribed: ${transcript}`);
|
|
340
380
|
if (!transcript || transcript === "[empty transcription]") {
|
|
341
|
-
await this.
|
|
381
|
+
await this.replyToChat(chatId, "Could not transcribe voice message.", threadId);
|
|
342
382
|
return;
|
|
343
383
|
}
|
|
344
384
|
// Feed transcript into Claude as if user typed it
|
|
345
|
-
const session = this.getOrCreateSession(chatId);
|
|
385
|
+
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
346
386
|
try {
|
|
347
387
|
const prompt = buildPromptWithReplyContext(transcript, msg);
|
|
348
388
|
session.currentPrompt = prompt;
|
|
@@ -350,16 +390,16 @@ export class CcTgBot {
|
|
|
350
390
|
this.startTyping(chatId, session);
|
|
351
391
|
}
|
|
352
392
|
catch (err) {
|
|
353
|
-
await this.
|
|
354
|
-
this.killSession(chatId);
|
|
393
|
+
await this.replyToChat(chatId, `Error sending to Claude: ${err.message}`, threadId);
|
|
394
|
+
this.killSession(chatId, true, threadId);
|
|
355
395
|
}
|
|
356
396
|
}
|
|
357
397
|
catch (err) {
|
|
358
398
|
console.error(`[voice:${chatId}] error:`, err.message);
|
|
359
|
-
await this.
|
|
399
|
+
await this.replyToChat(chatId, `Voice transcription failed: ${err.message}`, threadId);
|
|
360
400
|
}
|
|
361
401
|
}
|
|
362
|
-
async handlePhoto(chatId, msg) {
|
|
402
|
+
async handlePhoto(chatId, msg, threadId, threadName) {
|
|
363
403
|
// Pick highest resolution photo
|
|
364
404
|
const photos = msg.photo;
|
|
365
405
|
const best = photos[photos.length - 1];
|
|
@@ -370,16 +410,16 @@ export class CcTgBot {
|
|
|
370
410
|
const fileLink = await this.bot.getFileLink(best.file_id);
|
|
371
411
|
const imageData = await fetchAsBase64(fileLink);
|
|
372
412
|
// Telegram photos are always JPEG
|
|
373
|
-
const session = this.getOrCreateSession(chatId);
|
|
413
|
+
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
374
414
|
session.claude.sendImage(imageData, "image/jpeg", caption);
|
|
375
415
|
this.startTyping(chatId, session);
|
|
376
416
|
}
|
|
377
417
|
catch (err) {
|
|
378
418
|
console.error(`[photo:${chatId}] error:`, err.message);
|
|
379
|
-
await this.
|
|
419
|
+
await this.replyToChat(chatId, `Failed to process image: ${err.message}`, threadId);
|
|
380
420
|
}
|
|
381
421
|
}
|
|
382
|
-
async handleDocument(chatId, msg) {
|
|
422
|
+
async handleDocument(chatId, msg, threadId, threadName) {
|
|
383
423
|
const doc = msg.document;
|
|
384
424
|
const caption = msg.caption?.trim();
|
|
385
425
|
const fileName = doc.file_name ?? `file_${doc.file_id}`;
|
|
@@ -395,21 +435,33 @@ export class CcTgBot {
|
|
|
395
435
|
const prompt = caption
|
|
396
436
|
? `${caption}\n\nATTACHMENTS: [${fileName}](${destPath})`
|
|
397
437
|
: `ATTACHMENTS: [${fileName}](${destPath})`;
|
|
398
|
-
const session = this.getOrCreateSession(chatId);
|
|
438
|
+
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
399
439
|
session.claude.sendPrompt(prompt);
|
|
400
440
|
this.startTyping(chatId, session);
|
|
401
441
|
}
|
|
402
442
|
catch (err) {
|
|
403
443
|
console.error(`[doc:${chatId}] error:`, err.message);
|
|
404
|
-
await this.
|
|
444
|
+
await this.replyToChat(chatId, `Failed to receive document: ${err.message}`, threadId);
|
|
405
445
|
}
|
|
406
446
|
}
|
|
407
|
-
getOrCreateSession(chatId) {
|
|
408
|
-
const
|
|
447
|
+
getOrCreateSession(chatId, threadId, threadName) {
|
|
448
|
+
const key = this.sessionKey(chatId, threadId);
|
|
449
|
+
const existing = this.sessions.get(key);
|
|
409
450
|
if (existing && !existing.claude.exited)
|
|
410
451
|
return existing;
|
|
452
|
+
// Determine CWD for this thread — check THREAD_CWD_MAP by name then by ID
|
|
453
|
+
let sessionCwd = this.opts.cwd;
|
|
454
|
+
const threadCwdMap = this.getThreadCwdMap();
|
|
455
|
+
if (threadName && threadCwdMap[threadName]) {
|
|
456
|
+
sessionCwd = threadCwdMap[threadName];
|
|
457
|
+
console.log(`[cc-tg] thread "${threadName}" → cwd: ${sessionCwd}`);
|
|
458
|
+
}
|
|
459
|
+
else if (threadId !== undefined && threadCwdMap[String(threadId)]) {
|
|
460
|
+
sessionCwd = threadCwdMap[String(threadId)];
|
|
461
|
+
console.log(`[cc-tg] thread ${threadId} → cwd: ${sessionCwd}`);
|
|
462
|
+
}
|
|
411
463
|
const claude = new ClaudeProcess({
|
|
412
|
-
cwd:
|
|
464
|
+
cwd: sessionCwd,
|
|
413
465
|
token: getCurrentToken() || this.opts.claudeToken,
|
|
414
466
|
});
|
|
415
467
|
const session = {
|
|
@@ -420,6 +472,7 @@ export class CcTgBot {
|
|
|
420
472
|
writtenFiles: new Set(),
|
|
421
473
|
currentPrompt: "",
|
|
422
474
|
isRetry: false,
|
|
475
|
+
threadId,
|
|
423
476
|
};
|
|
424
477
|
claude.on("usage", (usage) => {
|
|
425
478
|
this.costStore.addUsage(chatId, usage);
|
|
@@ -428,33 +481,33 @@ export class CcTgBot {
|
|
|
428
481
|
// Verbose logging — log every message type and subtype
|
|
429
482
|
const subtype = msg.payload.subtype ?? "";
|
|
430
483
|
const toolName = this.extractToolName(msg);
|
|
431
|
-
const logParts = [`[claude:${
|
|
484
|
+
const logParts = [`[claude:${key}] msg=${msg.type}`];
|
|
432
485
|
if (subtype)
|
|
433
486
|
logParts.push(`subtype=${subtype}`);
|
|
434
487
|
if (toolName)
|
|
435
488
|
logParts.push(`tool=${toolName}`);
|
|
436
489
|
console.log(logParts.join(" "));
|
|
437
490
|
// Track files written by Write/Edit tool calls
|
|
438
|
-
this.trackWrittenFiles(msg, session,
|
|
491
|
+
this.trackWrittenFiles(msg, session, sessionCwd);
|
|
439
492
|
this.handleClaudeMessage(chatId, session, msg);
|
|
440
493
|
});
|
|
441
494
|
claude.on("stderr", (data) => {
|
|
442
495
|
const line = data.trim();
|
|
443
496
|
if (line)
|
|
444
|
-
console.error(`[claude:${
|
|
497
|
+
console.error(`[claude:${key}:stderr]`, line);
|
|
445
498
|
});
|
|
446
499
|
claude.on("exit", (code) => {
|
|
447
|
-
console.log(`[claude:${
|
|
500
|
+
console.log(`[claude:${key}] exited code=${code}`);
|
|
448
501
|
this.stopTyping(session);
|
|
449
|
-
this.sessions.delete(
|
|
502
|
+
this.sessions.delete(key);
|
|
450
503
|
});
|
|
451
504
|
claude.on("error", (err) => {
|
|
452
|
-
console.error(`[claude:${
|
|
505
|
+
console.error(`[claude:${key}] process error: ${err.message}`);
|
|
453
506
|
this.bot.sendMessage(chatId, `Claude process error: ${err.message}`).catch(() => { });
|
|
454
507
|
this.stopTyping(session);
|
|
455
|
-
this.sessions.delete(
|
|
508
|
+
this.sessions.delete(key);
|
|
456
509
|
});
|
|
457
|
-
this.sessions.set(
|
|
510
|
+
this.sessions.set(key, session);
|
|
458
511
|
return session;
|
|
459
512
|
}
|
|
460
513
|
handleClaudeMessage(chatId, session, msg) {
|
|
@@ -470,13 +523,15 @@ export class CcTgBot {
|
|
|
470
523
|
// Check for usage/rate limit signals before forwarding to Telegram
|
|
471
524
|
const sig = detectUsageLimit(text);
|
|
472
525
|
if (sig.detected) {
|
|
526
|
+
const threadId = session.threadId;
|
|
527
|
+
const retryKey = this.sessionKey(chatId, threadId);
|
|
473
528
|
const lastPrompt = session.currentPrompt;
|
|
474
|
-
const prevRetry = this.pendingRetries.get(
|
|
529
|
+
const prevRetry = this.pendingRetries.get(retryKey);
|
|
475
530
|
const attempt = (prevRetry?.attempt ?? 0) + 1;
|
|
476
531
|
if (prevRetry)
|
|
477
532
|
clearTimeout(prevRetry.timer);
|
|
478
|
-
this.
|
|
479
|
-
this.killSession(chatId);
|
|
533
|
+
this.replyToChat(chatId, sig.humanMessage, threadId).catch(() => { });
|
|
534
|
+
this.killSession(chatId, true, threadId);
|
|
480
535
|
// Token rotation: if this is a usage_exhausted signal and we have multiple
|
|
481
536
|
// tokens, rotate to the next one and retry immediately instead of sleeping.
|
|
482
537
|
// Only rotate if we haven't yet cycled through all tokens (attempt <= count-1).
|
|
@@ -486,40 +541,40 @@ export class CcTgBot {
|
|
|
486
541
|
const newIdx = getTokenIndex();
|
|
487
542
|
const total = getTokenCount();
|
|
488
543
|
console.log(`[cc-tg] Token ${prevIdx + 1}/${total} exhausted, rotating to token ${newIdx + 1}/${total}`);
|
|
489
|
-
this.
|
|
490
|
-
this.pendingRetries.set(
|
|
544
|
+
this.replyToChat(chatId, `🔄 Token ${prevIdx + 1}/${total} exhausted, switching to token ${newIdx + 1}/${total}...`, threadId).catch(() => { });
|
|
545
|
+
this.pendingRetries.set(retryKey, { text: lastPrompt, attempt, timer: setTimeout(() => { }, 0) });
|
|
491
546
|
try {
|
|
492
|
-
const retrySession = this.getOrCreateSession(chatId);
|
|
547
|
+
const retrySession = this.getOrCreateSession(chatId, threadId);
|
|
493
548
|
retrySession.currentPrompt = lastPrompt;
|
|
494
549
|
retrySession.isRetry = true;
|
|
495
550
|
retrySession.claude.sendPrompt(lastPrompt);
|
|
496
551
|
this.startTyping(chatId, retrySession);
|
|
497
552
|
}
|
|
498
553
|
catch (err) {
|
|
499
|
-
this.
|
|
554
|
+
this.replyToChat(chatId, `❌ Failed to retry with rotated token: ${err.message}`, threadId).catch(() => { });
|
|
500
555
|
}
|
|
501
556
|
return;
|
|
502
557
|
}
|
|
503
558
|
if (attempt > 3) {
|
|
504
|
-
this.
|
|
505
|
-
this.pendingRetries.delete(
|
|
559
|
+
this.replyToChat(chatId, "❌ Claude usage limit persists after 3 retries. Please try again later.", threadId).catch(() => { });
|
|
560
|
+
this.pendingRetries.delete(retryKey);
|
|
506
561
|
return;
|
|
507
562
|
}
|
|
508
|
-
console.log(`[usage-limit:${
|
|
563
|
+
console.log(`[usage-limit:${retryKey}] ${sig.reason} — scheduling retry attempt=${attempt} in ${sig.retryAfterMs}ms`);
|
|
509
564
|
const timer = setTimeout(() => {
|
|
510
|
-
this.pendingRetries.delete(
|
|
565
|
+
this.pendingRetries.delete(retryKey);
|
|
511
566
|
try {
|
|
512
|
-
const retrySession = this.getOrCreateSession(chatId);
|
|
567
|
+
const retrySession = this.getOrCreateSession(chatId, threadId);
|
|
513
568
|
retrySession.currentPrompt = lastPrompt;
|
|
514
569
|
retrySession.isRetry = true;
|
|
515
570
|
retrySession.claude.sendPrompt(lastPrompt);
|
|
516
571
|
this.startTyping(chatId, retrySession);
|
|
517
572
|
}
|
|
518
573
|
catch (err) {
|
|
519
|
-
this.
|
|
574
|
+
this.replyToChat(chatId, `❌ Failed to retry: ${err.message}`, threadId).catch(() => { });
|
|
520
575
|
}
|
|
521
576
|
}, sig.retryAfterMs);
|
|
522
|
-
this.pendingRetries.set(
|
|
577
|
+
this.pendingRetries.set(retryKey, { text: lastPrompt, attempt, timer });
|
|
523
578
|
return;
|
|
524
579
|
}
|
|
525
580
|
// Accumulate text and debounce — Claude streams chunks rapidly
|
|
@@ -553,10 +608,11 @@ export class CcTgBot {
|
|
|
553
608
|
// Format for Telegram HTML and split if needed (max 4096 chars)
|
|
554
609
|
const formatted = formatForTelegram(text);
|
|
555
610
|
const chunks = splitLongMessage(formatted);
|
|
611
|
+
const threadId = session.threadId;
|
|
556
612
|
for (const chunk of chunks) {
|
|
557
|
-
this.
|
|
613
|
+
this.replyToChat(chatId, chunk, threadId, { parse_mode: "HTML" }).catch(() => {
|
|
558
614
|
// HTML parse failed — retry as plain text
|
|
559
|
-
this.
|
|
615
|
+
this.replyToChat(chatId, chunk, threadId).catch((err) => console.error(`[tg:${chatId}] send failed:`, err.message));
|
|
560
616
|
});
|
|
561
617
|
}
|
|
562
618
|
// Hybrid file upload: find files mentioned in result text that Claude actually wrote
|
|
@@ -729,11 +785,12 @@ export class CcTgBot {
|
|
|
729
785
|
const MAX_TG_FILE_BYTES = 50 * 1024 * 1024;
|
|
730
786
|
if (fileSize > MAX_TG_FILE_BYTES) {
|
|
731
787
|
const mb = (fileSize / (1024 * 1024)).toFixed(1);
|
|
732
|
-
this.
|
|
788
|
+
this.replyToChat(chatId, `File too large for Telegram (${mb}mb). Find it at: ${filePath}`, session.threadId).catch(() => { });
|
|
733
789
|
continue;
|
|
734
790
|
}
|
|
735
791
|
console.log(`[claude:files] uploading to telegram: ${filePath}`);
|
|
736
|
-
|
|
792
|
+
const docOpts = session.threadId ? { message_thread_id: session.threadId } : undefined;
|
|
793
|
+
this.bot.sendDocument(chatId, filePath, docOpts).catch((err) => console.error(`[tg:${chatId}] sendDocument failed for ${filePath}:`, err.message));
|
|
737
794
|
}
|
|
738
795
|
// Clear written files for next turn
|
|
739
796
|
session.writtenFiles.clear();
|
|
@@ -822,83 +879,83 @@ export class CcTgBot {
|
|
|
822
879
|
});
|
|
823
880
|
cronProcess.sendPrompt(taskPrompt);
|
|
824
881
|
}
|
|
825
|
-
async handleCron(chatId, text) {
|
|
882
|
+
async handleCron(chatId, text, threadId) {
|
|
826
883
|
const args = text.slice("/cron".length).trim();
|
|
827
884
|
// /cron list
|
|
828
885
|
if (args === "list" || args === "") {
|
|
829
886
|
const jobs = this.cron.list(chatId);
|
|
830
887
|
if (!jobs.length) {
|
|
831
|
-
await this.
|
|
888
|
+
await this.replyToChat(chatId, "No cron jobs.", threadId);
|
|
832
889
|
return;
|
|
833
890
|
}
|
|
834
891
|
const lines = jobs.map((j, i) => {
|
|
835
892
|
const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
|
|
836
893
|
return `#${i + 1} ${j.schedule} — "${short}"`;
|
|
837
894
|
});
|
|
838
|
-
await this.
|
|
895
|
+
await this.replyToChat(chatId, `Cron jobs (${jobs.length}):\n${lines.join("\n")}`, threadId);
|
|
839
896
|
return;
|
|
840
897
|
}
|
|
841
898
|
// /cron clear
|
|
842
899
|
if (args === "clear") {
|
|
843
900
|
const n = this.cron.clearAll(chatId);
|
|
844
|
-
await this.
|
|
901
|
+
await this.replyToChat(chatId, `Cleared ${n} cron job(s).`, threadId);
|
|
845
902
|
return;
|
|
846
903
|
}
|
|
847
904
|
// /cron remove <id>
|
|
848
905
|
if (args.startsWith("remove ")) {
|
|
849
906
|
const id = args.slice("remove ".length).trim();
|
|
850
907
|
const ok = this.cron.remove(chatId, id);
|
|
851
|
-
await this.
|
|
908
|
+
await this.replyToChat(chatId, ok ? `Removed ${id}.` : `Not found: ${id}`, threadId);
|
|
852
909
|
return;
|
|
853
910
|
}
|
|
854
911
|
// /cron edit [<#> ...]
|
|
855
912
|
if (args === "edit" || args.startsWith("edit ")) {
|
|
856
|
-
await this.handleCronEdit(chatId, args.slice("edit".length).trim());
|
|
913
|
+
await this.handleCronEdit(chatId, args.slice("edit".length).trim(), threadId);
|
|
857
914
|
return;
|
|
858
915
|
}
|
|
859
916
|
// /cron every 1h <prompt>
|
|
860
917
|
const scheduleMatch = args.match(/^(every\s+\d+[mhd])\s+(.+)$/i);
|
|
861
918
|
if (!scheduleMatch) {
|
|
862
|
-
await this.
|
|
919
|
+
await this.replyToChat(chatId, "Usage:\n/cron every 1h <prompt>\n/cron list\n/cron edit\n/cron remove <id>\n/cron clear", threadId);
|
|
863
920
|
return;
|
|
864
921
|
}
|
|
865
922
|
const schedule = scheduleMatch[1];
|
|
866
923
|
const prompt = scheduleMatch[2];
|
|
867
924
|
const job = this.cron.add(chatId, schedule, prompt);
|
|
868
925
|
if (!job) {
|
|
869
|
-
await this.
|
|
926
|
+
await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
|
|
870
927
|
return;
|
|
871
928
|
}
|
|
872
|
-
await this.
|
|
929
|
+
await this.replyToChat(chatId, `Cron set [${job.id}]: ${schedule} — "${prompt}"`, threadId);
|
|
873
930
|
}
|
|
874
|
-
async handleCronEdit(chatId, editArgs) {
|
|
931
|
+
async handleCronEdit(chatId, editArgs, threadId) {
|
|
875
932
|
const jobs = this.cron.list(chatId);
|
|
876
933
|
// No args — show numbered list with edit instructions
|
|
877
934
|
if (!editArgs) {
|
|
878
935
|
if (!jobs.length) {
|
|
879
|
-
await this.
|
|
936
|
+
await this.replyToChat(chatId, "No cron jobs to edit.", threadId);
|
|
880
937
|
return;
|
|
881
938
|
}
|
|
882
939
|
const lines = jobs.map((j, i) => {
|
|
883
940
|
const short = j.prompt.length > 50 ? j.prompt.slice(0, 50) + "…" : j.prompt;
|
|
884
941
|
return `#${i + 1} ${j.schedule} — "${short}"`;
|
|
885
942
|
});
|
|
886
|
-
await this.
|
|
943
|
+
await this.replyToChat(chatId, `Cron jobs:\n${lines.join("\n")}\n\n` +
|
|
887
944
|
"Edit options:\n" +
|
|
888
945
|
"/cron edit <#> every <N><unit> <new prompt>\n" +
|
|
889
946
|
"/cron edit <#> schedule every <N><unit>\n" +
|
|
890
|
-
"/cron edit <#> prompt <new prompt>");
|
|
947
|
+
"/cron edit <#> prompt <new prompt>", threadId);
|
|
891
948
|
return;
|
|
892
949
|
}
|
|
893
950
|
// Expect: <index> <rest>
|
|
894
951
|
const indexMatch = editArgs.match(/^(\d+)\s+(.+)$/);
|
|
895
952
|
if (!indexMatch) {
|
|
896
|
-
await this.
|
|
953
|
+
await this.replyToChat(chatId, "Usage: /cron edit <#> every <N><unit> <new prompt>", threadId);
|
|
897
954
|
return;
|
|
898
955
|
}
|
|
899
956
|
const index = parseInt(indexMatch[1], 10) - 1;
|
|
900
957
|
if (index < 0 || index >= jobs.length) {
|
|
901
|
-
await this.
|
|
958
|
+
await this.replyToChat(chatId, `Invalid job number. Use /cron edit to see the list.`, threadId);
|
|
902
959
|
return;
|
|
903
960
|
}
|
|
904
961
|
const job = jobs[index];
|
|
@@ -908,13 +965,13 @@ export class CcTgBot {
|
|
|
908
965
|
const newSchedule = editCmd.slice("schedule ".length).trim();
|
|
909
966
|
const result = this.cron.update(chatId, job.id, { schedule: newSchedule });
|
|
910
967
|
if (result === null) {
|
|
911
|
-
await this.
|
|
968
|
+
await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
|
|
912
969
|
}
|
|
913
970
|
else if (result === false) {
|
|
914
|
-
await this.
|
|
971
|
+
await this.replyToChat(chatId, "Job not found.", threadId);
|
|
915
972
|
}
|
|
916
973
|
else {
|
|
917
|
-
await this.
|
|
974
|
+
await this.replyToChat(chatId, `#${index + 1} schedule updated to ${newSchedule}.`, threadId);
|
|
918
975
|
}
|
|
919
976
|
return;
|
|
920
977
|
}
|
|
@@ -923,10 +980,10 @@ export class CcTgBot {
|
|
|
923
980
|
const newPrompt = editCmd.slice("prompt ".length).trim();
|
|
924
981
|
const result = this.cron.update(chatId, job.id, { prompt: newPrompt });
|
|
925
982
|
if (result === false) {
|
|
926
|
-
await this.
|
|
983
|
+
await this.replyToChat(chatId, "Job not found.", threadId);
|
|
927
984
|
}
|
|
928
985
|
else {
|
|
929
|
-
await this.
|
|
986
|
+
await this.replyToChat(chatId, `#${index + 1} prompt updated to "${newPrompt}".`, threadId);
|
|
930
987
|
}
|
|
931
988
|
return;
|
|
932
989
|
}
|
|
@@ -937,20 +994,20 @@ export class CcTgBot {
|
|
|
937
994
|
const newPrompt = fullMatch[2];
|
|
938
995
|
const result = this.cron.update(chatId, job.id, { schedule: newSchedule, prompt: newPrompt });
|
|
939
996
|
if (result === null) {
|
|
940
|
-
await this.
|
|
997
|
+
await this.replyToChat(chatId, "Invalid schedule. Use: every 30m / every 2h / every 1d", threadId);
|
|
941
998
|
}
|
|
942
999
|
else if (result === false) {
|
|
943
|
-
await this.
|
|
1000
|
+
await this.replyToChat(chatId, "Job not found.", threadId);
|
|
944
1001
|
}
|
|
945
1002
|
else {
|
|
946
|
-
await this.
|
|
1003
|
+
await this.replyToChat(chatId, `#${index + 1} updated: ${newSchedule} — "${newPrompt}"`, threadId);
|
|
947
1004
|
}
|
|
948
1005
|
return;
|
|
949
1006
|
}
|
|
950
|
-
await this.
|
|
1007
|
+
await this.replyToChat(chatId, "Edit options:\n" +
|
|
951
1008
|
"/cron edit <#> every <N><unit> <new prompt>\n" +
|
|
952
1009
|
"/cron edit <#> schedule every <N><unit>\n" +
|
|
953
|
-
"/cron edit <#> prompt <new prompt>");
|
|
1010
|
+
"/cron edit <#> prompt <new prompt>", threadId);
|
|
954
1011
|
}
|
|
955
1012
|
/** Find cc-agent PIDs via pgrep. Returns array of numeric PIDs. */
|
|
956
1013
|
findCcAgentPids() {
|
|
@@ -977,33 +1034,33 @@ export class CcTgBot {
|
|
|
977
1034
|
}
|
|
978
1035
|
return pids;
|
|
979
1036
|
}
|
|
980
|
-
async handleReloadMcp(chatId) {
|
|
981
|
-
await this.
|
|
1037
|
+
async handleReloadMcp(chatId, threadId) {
|
|
1038
|
+
await this.replyToChat(chatId, "Clearing npx cache and reloading MCP...", threadId);
|
|
982
1039
|
try {
|
|
983
1040
|
const home = process.env.HOME ?? "~";
|
|
984
1041
|
execSync(`rm -rf "${home}/.npm/_npx/"`, { encoding: "utf8", shell: "/bin/sh" });
|
|
985
1042
|
console.log("[mcp] cleared ~/.npm/_npx/");
|
|
986
1043
|
}
|
|
987
1044
|
catch (err) {
|
|
988
|
-
await this.
|
|
1045
|
+
await this.replyToChat(chatId, `Warning: failed to clear npx cache: ${err.message}`, threadId);
|
|
989
1046
|
}
|
|
990
1047
|
const pids = this.killCcAgent();
|
|
991
1048
|
if (pids.length === 0) {
|
|
992
|
-
await this.
|
|
1049
|
+
await this.replyToChat(chatId, "NPX cache cleared. No cc-agent process found — MCP will start fresh on the next agent call.", threadId);
|
|
993
1050
|
return;
|
|
994
1051
|
}
|
|
995
|
-
await this.
|
|
1052
|
+
await this.replyToChat(chatId, `NPX cache cleared. Sent SIGTERM to cc-agent (pid${pids.length > 1 ? "s" : ""}: ${pids.join(", ")}).\nMCP restarted. New process will load on next agent call.`, threadId);
|
|
996
1053
|
}
|
|
997
|
-
async handleMcpStatus(chatId) {
|
|
1054
|
+
async handleMcpStatus(chatId, threadId) {
|
|
998
1055
|
try {
|
|
999
1056
|
const output = execSync("claude mcp list", { encoding: "utf8", shell: "/bin/sh" }).trim();
|
|
1000
|
-
await this.
|
|
1057
|
+
await this.replyToChat(chatId, `MCP server status:\n\n${output || "(no output)"}`, threadId);
|
|
1001
1058
|
}
|
|
1002
1059
|
catch (err) {
|
|
1003
|
-
await this.
|
|
1060
|
+
await this.replyToChat(chatId, `Failed to run claude mcp list: ${err.message}`, threadId);
|
|
1004
1061
|
}
|
|
1005
1062
|
}
|
|
1006
|
-
async handleMcpVersion(chatId) {
|
|
1063
|
+
async handleMcpVersion(chatId, threadId) {
|
|
1007
1064
|
let npmVersion = "unknown";
|
|
1008
1065
|
let cacheEntries = "(unavailable)";
|
|
1009
1066
|
try {
|
|
@@ -1020,9 +1077,9 @@ export class CcTgBot {
|
|
|
1020
1077
|
catch {
|
|
1021
1078
|
cacheEntries = "(empty or not found)";
|
|
1022
1079
|
}
|
|
1023
|
-
await this.
|
|
1080
|
+
await this.replyToChat(chatId, `cc-agent npm version: ${npmVersion}\n\nnpx cache (~/.npm/_npx/):\n${cacheEntries}`, threadId);
|
|
1024
1081
|
}
|
|
1025
|
-
async handleClearNpxCache(chatId) {
|
|
1082
|
+
async handleClearNpxCache(chatId, threadId) {
|
|
1026
1083
|
const home = process.env.HOME ?? "/tmp";
|
|
1027
1084
|
const cleared = [];
|
|
1028
1085
|
const failed = [];
|
|
@@ -1045,10 +1102,10 @@ export class CcTgBot {
|
|
|
1045
1102
|
const clearNote = failed.length
|
|
1046
1103
|
? `Cleared: ${cleared.join(", ")}. Failed: ${failed.join(", ")}.`
|
|
1047
1104
|
: `Cleared: ${cleared.join(", ")}.`;
|
|
1048
|
-
await this.
|
|
1105
|
+
await this.replyToChat(chatId, `${clearNote}${pidNote} Next call picks up latest npm version.`, threadId);
|
|
1049
1106
|
}
|
|
1050
|
-
async handleRestart(chatId) {
|
|
1051
|
-
await this.
|
|
1107
|
+
async handleRestart(chatId, threadId) {
|
|
1108
|
+
await this.replyToChat(chatId, "Clearing cache and restarting... brb.", threadId);
|
|
1052
1109
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1053
1110
|
// Clear npm caches before restart so launchd brings up fresh version
|
|
1054
1111
|
const home = process.env.HOME ?? "/tmp";
|
|
@@ -1059,45 +1116,48 @@ export class CcTgBot {
|
|
|
1059
1116
|
catch { }
|
|
1060
1117
|
}
|
|
1061
1118
|
// Kill all active Claude sessions cleanly
|
|
1062
|
-
for (const
|
|
1063
|
-
this.
|
|
1119
|
+
for (const session of this.sessions.values()) {
|
|
1120
|
+
this.stopTyping(session);
|
|
1121
|
+
session.claude.kill();
|
|
1064
1122
|
}
|
|
1123
|
+
this.sessions.clear();
|
|
1065
1124
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
1066
1125
|
process.exit(0);
|
|
1067
1126
|
}
|
|
1068
|
-
async handleGetFile(chatId, text) {
|
|
1127
|
+
async handleGetFile(chatId, text, threadId) {
|
|
1069
1128
|
const arg = text.slice("/get_file".length).trim();
|
|
1070
1129
|
if (!arg) {
|
|
1071
|
-
await this.
|
|
1130
|
+
await this.replyToChat(chatId, "Usage: /get_file <path>", threadId);
|
|
1072
1131
|
return;
|
|
1073
1132
|
}
|
|
1074
1133
|
const filePath = resolve(arg);
|
|
1075
1134
|
const safeDirs = ["/tmp/", "/var/folders/", os.homedir() + "/Downloads/", this.opts.cwd ?? process.cwd()];
|
|
1076
1135
|
const inSafeDir = safeDirs.some(d => filePath.startsWith(d));
|
|
1077
1136
|
if (!inSafeDir) {
|
|
1078
|
-
await this.
|
|
1137
|
+
await this.replyToChat(chatId, "Access denied: path not in allowed directories", threadId);
|
|
1079
1138
|
return;
|
|
1080
1139
|
}
|
|
1081
1140
|
if (!existsSync(filePath)) {
|
|
1082
|
-
await this.
|
|
1141
|
+
await this.replyToChat(chatId, `File not found: ${filePath}`, threadId);
|
|
1083
1142
|
return;
|
|
1084
1143
|
}
|
|
1085
1144
|
if (!statSync(filePath).isFile()) {
|
|
1086
|
-
await this.
|
|
1145
|
+
await this.replyToChat(chatId, `Not a file: ${filePath}`, threadId);
|
|
1087
1146
|
return;
|
|
1088
1147
|
}
|
|
1089
1148
|
if (this.isSensitiveFile(filePath)) {
|
|
1090
|
-
await this.
|
|
1149
|
+
await this.replyToChat(chatId, "Access denied: sensitive file", threadId);
|
|
1091
1150
|
return;
|
|
1092
1151
|
}
|
|
1093
1152
|
const MAX_TG_FILE_BYTES = 50 * 1024 * 1024;
|
|
1094
1153
|
const fileSize = statSync(filePath).size;
|
|
1095
1154
|
if (fileSize > MAX_TG_FILE_BYTES) {
|
|
1096
1155
|
const mb = (fileSize / (1024 * 1024)).toFixed(1);
|
|
1097
|
-
await this.
|
|
1156
|
+
await this.replyToChat(chatId, `File too large for Telegram (${mb}mb). Find it at: ${filePath}`, threadId);
|
|
1098
1157
|
return;
|
|
1099
1158
|
}
|
|
1100
|
-
|
|
1159
|
+
const docOpts = threadId ? { message_thread_id: threadId } : undefined;
|
|
1160
|
+
await this.bot.sendDocument(chatId, filePath, docOpts);
|
|
1101
1161
|
}
|
|
1102
1162
|
callCcAgentTool(toolName, args = {}) {
|
|
1103
1163
|
return new Promise((resolve) => {
|
|
@@ -1170,12 +1230,13 @@ export class CcTgBot {
|
|
|
1170
1230
|
proc.on("exit", () => { clearTimeout(timeout); done(null); });
|
|
1171
1231
|
});
|
|
1172
1232
|
}
|
|
1173
|
-
killSession(chatId, keepCrons = true) {
|
|
1174
|
-
const
|
|
1233
|
+
killSession(chatId, keepCrons = true, threadId) {
|
|
1234
|
+
const key = this.sessionKey(chatId, threadId);
|
|
1235
|
+
const session = this.sessions.get(key);
|
|
1175
1236
|
if (session) {
|
|
1176
1237
|
this.stopTyping(session);
|
|
1177
1238
|
session.claude.kill();
|
|
1178
|
-
this.sessions.delete(
|
|
1239
|
+
this.sessions.delete(key);
|
|
1179
1240
|
}
|
|
1180
1241
|
if (!keepCrons)
|
|
1181
1242
|
this.cron.clearAll(chatId);
|
|
@@ -1185,9 +1246,11 @@ export class CcTgBot {
|
|
|
1185
1246
|
}
|
|
1186
1247
|
stop() {
|
|
1187
1248
|
this.bot.stopPolling();
|
|
1188
|
-
for (const
|
|
1189
|
-
this.
|
|
1249
|
+
for (const session of this.sessions.values()) {
|
|
1250
|
+
this.stopTyping(session);
|
|
1251
|
+
session.claude.kill();
|
|
1190
1252
|
}
|
|
1253
|
+
this.sessions.clear();
|
|
1191
1254
|
}
|
|
1192
1255
|
}
|
|
1193
1256
|
function buildPromptWithReplyContext(text, msg) {
|