@gonzih/cc-tg 0.9.51 → 0.9.54

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 CHANGED
@@ -32,6 +32,8 @@ Open your bot in Telegram and start chatting.
32
32
  | `ALLOWED_USER_IDS` | no | Comma-separated Telegram user IDs. Leave empty to allow anyone |
33
33
  | `CWD` | no | Working directory for Claude Code. Defaults to current directory |
34
34
  | `THREAD_CWD_MAP` | no | JSON mapping of forum topic names or IDs to CWD paths (see [Multi-topic sessions](#multi-topic-sessions)) |
35
+ | `CC_TG_AUTO_COMPACT_MESSAGES` | no | Automatically send `/compact` after this many messages in a session (default: 40, set to 0 to disable) |
36
+ | `CC_TG_COST_WARN_USD` | no | Send a cost warning notification when session cost exceeds this USD amount (default: 5.0, set to 0 to disable) |
35
37
 
36
38
  *One of `CLAUDE_CODE_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_API_KEY` required.
37
39
 
@@ -66,6 +68,8 @@ Message [@userinfobot](https://t.me/userinfobot) — it replies with your numeri
66
68
  | `/mcp_version` | Show latest published cc-agent npm version and current cache |
67
69
  | `/clear_npx_cache` | Clear npx cache and reload cc-agent (upgrades to latest version) |
68
70
  | `/get_file <path>` | Send a file from the server to this chat |
71
+ | `/effort <level>` | Set Claude effort level (`low` / `medium` / `high` / `xhigh` / `max` / `auto`) |
72
+ | `/compact` | Compact context history to free tokens |
69
73
  | `/restart` | Self-restart the cc-tg bot process (no SSH needed) |
70
74
  | Any text | Sent directly to Claude Code |
71
75
  | Voice message | Transcribed via whisper.cpp and sent to Claude |
package/dist/bot.d.ts CHANGED
@@ -97,8 +97,6 @@ export declare class CcTgBot {
97
97
  private handleGetFile;
98
98
  private handleDrivers;
99
99
  private handleAgents;
100
- private wikiKey;
101
- private wikiUpdatedKey;
102
100
  private handleWiki;
103
101
  private handleWikiList;
104
102
  private handleWikiShow;
@@ -107,6 +105,13 @@ export declare class CcTgBot {
107
105
  private handleWikiSync;
108
106
  private callCcAgentTool;
109
107
  private killSession;
108
+ private handleEffort;
109
+ private handleCompact;
110
+ /**
111
+ * If CC_TG_AUTO_COMPACT_MESSAGES threshold is reached, send /compact to the active Claude
112
+ * session before the next user message. Resets the per-session counter on fire.
113
+ */
114
+ private maybeSendAutoCompact;
110
115
  getMe(): Promise<TelegramBot.User>;
111
116
  stop(): void;
112
117
  }
package/dist/bot.js CHANGED
@@ -17,7 +17,7 @@ import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./to
17
17
  import { writeChatLog } from "./notifier.js";
18
18
  import { CronManager } from "./cron.js";
19
19
  import { parseRoutingTag, ensureMetaAgent, routeToMetaAgent } from "./router.js";
20
- import { VOICE_PENDING_KEY, VOICE_FAILED_KEY, TTL, metaAgentStatusKey } from "@gonzih/cc-wire";
20
+ import { VOICE_PENDING_KEY, VOICE_FAILED_KEY, TTL, metaAgentStatusKey, wikiKey, wikiUpdatedKey } from "@gonzih/cc-wire";
21
21
  const BOT_COMMANDS = [
22
22
  { command: "start", description: "Reset session and start fresh" },
23
23
  { command: "reset", description: "Reset Claude session" },
@@ -37,6 +37,8 @@ const BOT_COMMANDS = [
37
37
  { command: "drivers", description: "List available agent drivers" },
38
38
  { command: "agents", description: "Show running meta-agents and their live status" },
39
39
  { command: "wiki", description: "Manage wiki pages — list/show/update/delete/sync" },
40
+ { command: "effort", description: "Set effort level: low / medium / high / xhigh / max / auto" },
41
+ { command: "compact", description: "Compact context history to free tokens" },
40
42
  ];
41
43
  // Debounces streaming chunks. Resets on each chunk. Fires 800ms after last chunk.
42
44
  const FLUSH_DELAY_MS = 800;
@@ -463,6 +465,16 @@ export class CcTgBot {
463
465
  await this.handleWiki(chatId, text, threadId);
464
466
  return;
465
467
  }
468
+ // /effort <level> — forward effort level to active Claude session
469
+ if (text.startsWith("/effort")) {
470
+ await this.handleEffort(chatId, text, threadId);
471
+ return;
472
+ }
473
+ // /compact — compact context history in active Claude session
474
+ if (text === "/compact") {
475
+ await this.handleCompact(chatId, threadId);
476
+ return;
477
+ }
466
478
  // #tag / #org/repo routing — delegate to meta-agent instead of local Claude session
467
479
  if (this.redis) {
468
480
  const routing = parseRoutingTag(text);
@@ -514,6 +526,7 @@ export class CcTgBot {
514
526
  const enriched = await enrichPromptWithUrls(text);
515
527
  const prompt = buildPromptWithReplyContext(enriched, msg);
516
528
  session.currentPrompt = prompt;
529
+ this.maybeSendAutoCompact(session, chatId, threadId);
517
530
  session.claude.sendPrompt(stampPrompt(prompt));
518
531
  this.startTyping(chatId, session);
519
532
  this.writeChatMessage("user", "telegram", text, chatId);
@@ -789,6 +802,8 @@ export class CcTgBot {
789
802
  currentPrompt: "",
790
803
  isRetry: false,
791
804
  threadId,
805
+ messagesSinceCompact: 0,
806
+ costWarnSent: false,
792
807
  };
793
808
  claude.on("usage", (usage) => {
794
809
  this.costStore.addUsage(chatId, usage);
@@ -847,6 +862,7 @@ export class CcTgBot {
847
862
  return;
848
863
  this.stopTyping(session);
849
864
  this.costStore.incrementMessages(chatId);
865
+ session.messagesSinceCompact++;
850
866
  const text = extractText(msg);
851
867
  if (!text)
852
868
  return;
@@ -955,6 +971,17 @@ export class CcTgBot {
955
971
  catch (err) {
956
972
  console.error(`[tg:${chatId}] uploadMentionedFiles error:`, err.message);
957
973
  }
974
+ // Cost threshold warning — fires once per session when cost first exceeds the limit
975
+ if (!session.costWarnSent) {
976
+ const warnThreshold = parseFloat(process.env.CC_TG_COST_WARN_USD ?? "5.0");
977
+ if (!isNaN(warnThreshold) && warnThreshold > 0) {
978
+ const cost = this.costStore.get(chatId);
979
+ if (cost.totalCostUsd >= warnThreshold) {
980
+ session.costWarnSent = true;
981
+ this.replyToChat(chatId, `⚠️ Session cost: $${cost.totalCostUsd.toFixed(2)} — consider /compact or /reset`, threadId).catch(() => { });
982
+ }
983
+ }
984
+ }
958
985
  }
959
986
  trackWrittenFiles(msg, session, cwd) {
960
987
  // Only look at assistant messages with tool_use blocks
@@ -1409,13 +1436,6 @@ export class CcTgBot {
1409
1436
  await this.replyToChat(chatId, `Failed to get agents status: ${err.message}`, threadId);
1410
1437
  }
1411
1438
  }
1412
- // ─── Wiki helpers ───────────────────────────────────────────────────────────
1413
- wikiKey(repoSlug) {
1414
- return `cca:wiki:${repoSlug}`;
1415
- }
1416
- wikiUpdatedKey(repoSlug) {
1417
- return `cca:wiki:${repoSlug}:updated`;
1418
- }
1419
1439
  async handleWiki(chatId, text, threadId) {
1420
1440
  const args = text.slice("/wiki".length).trim();
1421
1441
  const parts = args.split(/\s+/);
@@ -1467,7 +1487,7 @@ export class CcTgBot {
1467
1487
  return;
1468
1488
  }
1469
1489
  if (repoSlug) {
1470
- const pages = await this.redis.hkeys(this.wikiKey(repoSlug));
1490
+ const pages = await this.redis.hkeys(wikiKey(repoSlug));
1471
1491
  if (!pages.length) {
1472
1492
  await this.replyToChat(chatId, `No wiki pages for "${repoSlug}".`, threadId);
1473
1493
  return;
@@ -1500,7 +1520,7 @@ export class CcTgBot {
1500
1520
  await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1501
1521
  return;
1502
1522
  }
1503
- const content = await this.redis.hget(this.wikiKey(repoSlug), pageName);
1523
+ const content = await this.redis.hget(wikiKey(repoSlug), pageName);
1504
1524
  if (!content) {
1505
1525
  await this.replyToChat(chatId, `Page "${pageName}" not found in "${repoSlug}".`, threadId);
1506
1526
  return;
@@ -1520,8 +1540,8 @@ export class CcTgBot {
1520
1540
  await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1521
1541
  return;
1522
1542
  }
1523
- await this.redis.hset(this.wikiKey(repoSlug), pageName, content);
1524
- await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1543
+ await this.redis.hset(wikiKey(repoSlug), pageName, content);
1544
+ await this.redis.set(wikiUpdatedKey(repoSlug), new Date().toISOString());
1525
1545
  await this.replyToChat(chatId, `Updated "${pageName}" in "${repoSlug}".`, threadId);
1526
1546
  }
1527
1547
  async handleWikiDelete(chatId, repoSlug, pageName, threadId) {
@@ -1529,9 +1549,9 @@ export class CcTgBot {
1529
1549
  await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1530
1550
  return;
1531
1551
  }
1532
- const deleted = await this.redis.hdel(this.wikiKey(repoSlug), pageName);
1552
+ const deleted = await this.redis.hdel(wikiKey(repoSlug), pageName);
1533
1553
  if (deleted) {
1534
- await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1554
+ await this.redis.set(wikiUpdatedKey(repoSlug), new Date().toISOString());
1535
1555
  await this.replyToChat(chatId, `Deleted "${pageName}" from "${repoSlug}".`, threadId);
1536
1556
  }
1537
1557
  else {
@@ -1567,7 +1587,7 @@ export class CcTgBot {
1567
1587
  const pageName = file.replace(/\.md$/, "");
1568
1588
  try {
1569
1589
  const content = readFileSync(join(wikiDir, file), "utf8");
1570
- await this.redis.hset(this.wikiKey(repoSlug), pageName, content);
1590
+ await this.redis.hset(wikiKey(repoSlug), pageName, content);
1571
1591
  synced++;
1572
1592
  }
1573
1593
  catch (err) {
@@ -1575,7 +1595,7 @@ export class CcTgBot {
1575
1595
  }
1576
1596
  }
1577
1597
  if (synced > 0) {
1578
- await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1598
+ await this.redis.set(wikiUpdatedKey(repoSlug), new Date().toISOString());
1579
1599
  }
1580
1600
  let reply = `Synced ${synced}/${files.length} pages to cca:wiki:${repoSlug}`;
1581
1601
  if (errors.length) {
@@ -1671,6 +1691,49 @@ export class CcTgBot {
1671
1691
  this.sessions.delete(key);
1672
1692
  }
1673
1693
  }
1694
+ async handleEffort(chatId, text, threadId) {
1695
+ const VALID_LEVELS = new Set(["low", "medium", "high", "xhigh", "max", "auto"]);
1696
+ const parts = text.trim().split(/\s+/);
1697
+ const level = parts[1]?.toLowerCase();
1698
+ if (!level || !VALID_LEVELS.has(level)) {
1699
+ await this.replyToChat(chatId, "Usage: /effort <level>\nValid levels: low, medium, high, xhigh, max, auto", threadId);
1700
+ return;
1701
+ }
1702
+ const sessionKey = this.sessionKey(chatId, threadId);
1703
+ const session = this.sessions.get(sessionKey);
1704
+ if (!session || session.claude.exited) {
1705
+ await this.replyToChat(chatId, `No active session. Start one, then use /effort ${level} to set the effort level.`, threadId);
1706
+ return;
1707
+ }
1708
+ session.claude.sendPrompt(`/effort ${level}`);
1709
+ await this.replyToChat(chatId, `⚡ Effort set to: ${level}`, threadId);
1710
+ }
1711
+ async handleCompact(chatId, threadId) {
1712
+ const sessionKey = this.sessionKey(chatId, threadId);
1713
+ const session = this.sessions.get(sessionKey);
1714
+ if (!session || session.claude.exited) {
1715
+ await this.replyToChat(chatId, "No active session.", threadId);
1716
+ return;
1717
+ }
1718
+ session.claude.sendPrompt("/compact");
1719
+ session.messagesSinceCompact = 0;
1720
+ await this.replyToChat(chatId, "🗜️ Compacting context history...", threadId);
1721
+ }
1722
+ /**
1723
+ * If CC_TG_AUTO_COMPACT_MESSAGES threshold is reached, send /compact to the active Claude
1724
+ * session before the next user message. Resets the per-session counter on fire.
1725
+ */
1726
+ maybeSendAutoCompact(session, chatId, threadId) {
1727
+ const threshold = parseInt(process.env.CC_TG_AUTO_COMPACT_MESSAGES ?? "40", 10);
1728
+ if (isNaN(threshold) || threshold <= 0)
1729
+ return;
1730
+ if (session.messagesSinceCompact >= threshold) {
1731
+ console.log(`[auto-compact:${chatId}] firing after ${session.messagesSinceCompact} messages`);
1732
+ session.messagesSinceCompact = 0;
1733
+ session.claude.sendPrompt("/compact");
1734
+ this.replyToChat(chatId, `🗜️ Auto-compacting context (${threshold} message limit reached)...`, threadId).catch(() => { });
1735
+ }
1736
+ }
1674
1737
  getMe() {
1675
1738
  return this.bot.getMe();
1676
1739
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.51",
3
+ "version": "0.9.54",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@gonzih/agent-ops": "^0.1.0",
22
- "@gonzih/cc-wire": "^0.1.0",
22
+ "@gonzih/cc-wire": "^0.1.4",
23
23
  "node-telegram-bot-api": "^0.66.0"
24
24
  },
25
25
  "devDependencies": {