@gonzih/cc-tg 0.9.22 → 0.9.23

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/dist/bot.d.ts CHANGED
@@ -74,6 +74,7 @@ export declare class CcTgBot {
74
74
  private runCronTask;
75
75
  private handleGetFile;
76
76
  private handleDrivers;
77
+ private handleAgents;
77
78
  private callCcAgentTool;
78
79
  private killSession;
79
80
  getMe(): Promise<TelegramBot.User>;
package/dist/bot.js CHANGED
@@ -33,6 +33,7 @@ const BOT_COMMANDS = [
33
33
  { command: "cron", description: "Manage cron jobs — add/list/edit/remove/clear" },
34
34
  { command: "voice_retry", description: "Retry failed voice message transcriptions" },
35
35
  { command: "drivers", description: "List available agent drivers" },
36
+ { command: "agents", description: "Show running meta-agents and their live status" },
36
37
  ];
37
38
  const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
38
39
  const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
@@ -73,22 +74,25 @@ function formatAgentCostSummary(text) {
73
74
  try {
74
75
  const data = JSON.parse(text);
75
76
  const totalCost = (data.total_cost_usd ?? data.total_cost ?? 0);
76
- const totalJobs = (data.total_jobs ?? data.job_count ?? 0);
77
77
  const byRepo = (data.by_repo ?? []);
78
- const lines = [
79
- "🤖 Agent jobs (all time)",
80
- `Total: $${totalCost.toFixed(2)} across ${totalJobs} jobs`,
81
- ];
78
+ if (byRepo.length === 0) {
79
+ return "No cost data available yet.";
80
+ }
81
+ const lines = ["💰 Cost Summary", ""];
82
+ // Align repo names with right-padded costs
83
+ const maxLen = Math.max(...byRepo.map((e) => (e.repo ?? e.repository ?? "unknown").length));
82
84
  for (const entry of byRepo) {
83
85
  const repo = (entry.repo ?? entry.repository ?? "unknown");
84
86
  const cost = (entry.cost_usd ?? entry.cost ?? 0);
85
- const jobs = (entry.job_count ?? entry.jobs ?? 0);
86
- lines.push(` ${repo}: $${cost.toFixed(2)} (${jobs} jobs)`);
87
+ const pad = " ".repeat(maxLen - repo.length + 3);
88
+ lines.push(`${repo}${pad}$${cost.toFixed(2)}`);
87
89
  }
90
+ lines.push("");
91
+ lines.push(`Total: $${totalCost.toFixed(2)}`);
88
92
  return lines.join("\n");
89
93
  }
90
94
  catch {
91
- return `🤖 Agent jobs (all time)\n${text}`;
95
+ return `💰 Cost Summary\n${text}`;
92
96
  }
93
97
  }
94
98
  class CostStore {
@@ -392,6 +396,11 @@ export class CcTgBot {
392
396
  await this.handleDrivers(chatId, threadId);
393
397
  return;
394
398
  }
399
+ // /agents — show running meta-agents and their live status
400
+ if (text === "/agents") {
401
+ await this.handleAgents(chatId, threadId);
402
+ return;
403
+ }
395
404
  const session = this.getOrCreateSession(chatId, threadId, threadName);
396
405
  try {
397
406
  const enriched = await enrichPromptWithUrls(text);
@@ -1236,6 +1245,70 @@ export class CcTgBot {
1236
1245
  await this.replyToChat(chatId, `Failed to list drivers: ${err.message}`, threadId);
1237
1246
  }
1238
1247
  }
1248
+ async handleAgents(chatId, threadId) {
1249
+ if (!this.redis) {
1250
+ await this.replyToChat(chatId, "Redis not configured — agents status unavailable.", threadId);
1251
+ return;
1252
+ }
1253
+ try {
1254
+ // Scan for all meta-agent status keys
1255
+ const keys = [];
1256
+ let cursor = "0";
1257
+ do {
1258
+ const [nextCursor, found] = await this.redis.scan(cursor, "MATCH", "cca:meta-agent:status:*", "COUNT", 100);
1259
+ cursor = nextCursor;
1260
+ keys.push(...found);
1261
+ } while (cursor !== "0");
1262
+ if (keys.length === 0) {
1263
+ await this.replyToChat(chatId, "No active meta-agents.", threadId);
1264
+ return;
1265
+ }
1266
+ const statuses = await Promise.all(keys.sort().map(async (key) => ({ key, raw: await this.redis.get(key) })));
1267
+ const lines = ["🤖 Active Agents", ""];
1268
+ for (const { key, raw } of statuses) {
1269
+ const namespace = key.replace("cca:meta-agent:status:", "");
1270
+ if (!raw) {
1271
+ lines.push(`${namespace} — status unknown`);
1272
+ continue;
1273
+ }
1274
+ try {
1275
+ const status = JSON.parse(raw);
1276
+ const state = status.status ?? "unknown";
1277
+ const turns = status.turn ?? status.turn_count ?? 0;
1278
+ const tool = status.current_tool;
1279
+ const lastActivity = status.last_activity ?? status.updated_at;
1280
+ let ageStr = "";
1281
+ if (lastActivity) {
1282
+ const ageSec = Math.floor((Date.now() - new Date(lastActivity).getTime()) / 1000);
1283
+ if (ageSec < 60)
1284
+ ageStr = `${ageSec}s ago`;
1285
+ else if (ageSec < 3600)
1286
+ ageStr = `${Math.floor(ageSec / 60)}m ago`;
1287
+ else
1288
+ ageStr = `${Math.floor(ageSec / 3600)}h ago`;
1289
+ }
1290
+ let statusDesc;
1291
+ if (state === "running" && tool) {
1292
+ statusDesc = `typing... (turn ${turns})`;
1293
+ }
1294
+ else if (state === "running") {
1295
+ statusDesc = `running (turn ${turns}${ageStr ? `, ${ageStr}` : ""})`;
1296
+ }
1297
+ else {
1298
+ statusDesc = `idle (turn ${turns}${ageStr ? `, ${ageStr}` : ""})`;
1299
+ }
1300
+ lines.push(`${namespace} — ${statusDesc}`);
1301
+ }
1302
+ catch {
1303
+ lines.push(`${namespace} — status unknown`);
1304
+ }
1305
+ }
1306
+ await this.replyToChat(chatId, lines.join("\n"), threadId);
1307
+ }
1308
+ catch (err) {
1309
+ await this.replyToChat(chatId, `Failed to get agents status: ${err.message}`, threadId);
1310
+ }
1311
+ }
1239
1312
  callCcAgentTool(toolName, args = {}) {
1240
1313
  // For spawn tools, pass through the configured driver and model
1241
1314
  const spawnTools = new Set(["spawn_agent", "spawn_from_profile"]);
@@ -21,9 +21,10 @@ export interface ChatMessage {
21
21
  }
22
22
  /**
23
23
  * Parse a notification payload and return the display text.
24
- * Appends a [model] or [driver] badge if the driver is non-claude.
24
+ * Appends a [driver] or [driver:model] badge whenever the driver field is present.
25
+ * Appends " cost: $X.XXX" if a numeric cost field is present.
25
26
  *
26
- * Payload format (JSON): { text: string, driver?: string, model?: string }
27
+ * Payload format (JSON): { text: string, driver?: string, model?: string, cost?: number }
27
28
  * Falls back to raw string if not valid JSON.
28
29
  */
29
30
  export declare function parseNotification(raw: string): string;
package/dist/notifier.js CHANGED
@@ -13,33 +13,56 @@ function log(level, ...args) {
13
13
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
14
14
  fn("[notifier]", ...args);
15
15
  }
16
+ /**
17
+ * Shorten a model name for display in a badge.
18
+ * - Strips driver prefix (e.g. "claude-sonnet-4-6" with driver "claude" → "sonnet-4-6")
19
+ * - Strips vendor/ prefix for openrouter-style names (e.g. "openai/gpt-4o" → "gpt-4o")
20
+ * - Returns empty string when model is absent.
21
+ */
22
+ function shortenModelName(model, driver) {
23
+ if (!model.trim())
24
+ return "";
25
+ const pfx = driver.toLowerCase() + "-";
26
+ if (model.toLowerCase().startsWith(pfx))
27
+ return model.slice(pfx.length);
28
+ const slashIdx = model.indexOf("/");
29
+ if (slashIdx >= 0)
30
+ return model.slice(slashIdx + 1);
31
+ return model;
32
+ }
16
33
  /**
17
34
  * Parse a notification payload and return the display text.
18
- * Appends a [model] or [driver] badge if the driver is non-claude.
35
+ * Appends a [driver] or [driver:model] badge whenever the driver field is present.
36
+ * Appends " cost: $X.XXX" if a numeric cost field is present.
19
37
  *
20
- * Payload format (JSON): { text: string, driver?: string, model?: string }
38
+ * Payload format (JSON): { text: string, driver?: string, model?: string, cost?: number }
21
39
  * Falls back to raw string if not valid JSON.
22
40
  */
23
41
  export function parseNotification(raw) {
24
42
  let text = raw;
25
43
  let driver;
26
44
  let model;
45
+ let cost;
27
46
  try {
28
47
  const parsed = JSON.parse(raw);
29
48
  if (parsed.text)
30
49
  text = parsed.text;
31
50
  driver = parsed.driver;
32
51
  model = parsed.model;
52
+ if (typeof parsed.cost === "number")
53
+ cost = parsed.cost;
33
54
  }
34
55
  catch {
35
56
  // not JSON — use raw string as-is, no badge
36
57
  return text;
37
58
  }
38
- // Only show badge if driver is present and not 'claude'
39
- if (!driver || driver === "claude")
59
+ // Show badge whenever driver field is present
60
+ if (!driver)
40
61
  return text;
41
- const badge = model && model.trim() ? model.trim() : driver;
42
- return `${text} [${badge}]`;
62
+ const shortModel = shortenModelName(model ?? "", driver);
63
+ const badge = shortModel ? `${driver}:${shortModel}` : driver;
64
+ const costStr = cost != null ? ` cost: $${cost.toFixed(3)}` : "";
65
+ return `${text}\n[${badge}]${costStr}`;
43
66
  }
44
67
  /**
45
68
  * Write a message to the chat log in Redis.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.22",
3
+ "version": "0.9.23",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {