@gonzih/cc-tg 0.9.22 → 0.9.24
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 +1 -0
- package/dist/bot.js +81 -8
- package/dist/notifier.d.ts +4 -2
- package/dist/notifier.js +95 -6
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
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
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
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
|
|
86
|
-
lines.push(
|
|
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
|
|
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"]);
|
package/dist/notifier.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Channels:
|
|
5
5
|
* cca:notify:{namespace} — job completion notifications from cc-agent → forward to Telegram
|
|
6
6
|
* cca:chat:incoming:{namespace} — messages from the web UI → echo to Telegram + feed into Claude session
|
|
7
|
+
* cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
|
|
7
8
|
*
|
|
8
9
|
* All messages (Telegram incoming, Claude responses) are also written to:
|
|
9
10
|
* cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
|
|
@@ -21,9 +22,10 @@ export interface ChatMessage {
|
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
23
24
|
* Parse a notification payload and return the display text.
|
|
24
|
-
* Appends a [
|
|
25
|
+
* Appends a [driver] or [driver:model] badge whenever the driver field is present.
|
|
26
|
+
* Appends " cost: $X.XXX" if a numeric cost field is present.
|
|
25
27
|
*
|
|
26
|
-
* Payload format (JSON): { text: string, driver?: string, model?: string }
|
|
28
|
+
* Payload format (JSON): { text: string, driver?: string, model?: string, cost?: number }
|
|
27
29
|
* Falls back to raw string if not valid JSON.
|
|
28
30
|
*/
|
|
29
31
|
export declare function parseNotification(raw: string): string;
|
package/dist/notifier.js
CHANGED
|
@@ -4,42 +4,72 @@
|
|
|
4
4
|
* Channels:
|
|
5
5
|
* cca:notify:{namespace} — job completion notifications from cc-agent → forward to Telegram
|
|
6
6
|
* cca:chat:incoming:{namespace} — messages from the web UI → echo to Telegram + feed into Claude session
|
|
7
|
+
* cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
|
|
7
8
|
*
|
|
8
9
|
* All messages (Telegram incoming, Claude responses) are also written to:
|
|
9
10
|
* cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
|
|
10
11
|
* cca:chat:outgoing:{namespace} — PUBLISH for web UI to consume
|
|
11
12
|
*/
|
|
13
|
+
import { splitLongMessage } from "./formatter.js";
|
|
12
14
|
function log(level, ...args) {
|
|
13
15
|
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
14
16
|
fn("[notifier]", ...args);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Shorten a model name for display in a badge.
|
|
20
|
+
* - Strips driver prefix (e.g. "claude-sonnet-4-6" with driver "claude" → "sonnet-4-6")
|
|
21
|
+
* - Strips vendor/ prefix for openrouter-style names (e.g. "openai/gpt-4o" → "gpt-4o")
|
|
22
|
+
* - Returns empty string when model is absent.
|
|
23
|
+
*/
|
|
24
|
+
function shortenModelName(model, driver) {
|
|
25
|
+
if (!model.trim())
|
|
26
|
+
return "";
|
|
27
|
+
const pfx = driver.toLowerCase() + "-";
|
|
28
|
+
if (model.toLowerCase().startsWith(pfx))
|
|
29
|
+
return model.slice(pfx.length);
|
|
30
|
+
const slashIdx = model.indexOf("/");
|
|
31
|
+
if (slashIdx >= 0)
|
|
32
|
+
return model.slice(slashIdx + 1);
|
|
33
|
+
return model;
|
|
34
|
+
}
|
|
35
|
+
/** Strip ANSI escape sequences from a string before sending to Telegram. */
|
|
36
|
+
function stripAnsi(text) {
|
|
37
|
+
// eslint-disable-next-line no-control-regex
|
|
38
|
+
return text.replace(/\x1B\[[0-9;]*[mGKHF]/g, "");
|
|
39
|
+
}
|
|
16
40
|
/**
|
|
17
41
|
* Parse a notification payload and return the display text.
|
|
18
|
-
* Appends a [
|
|
42
|
+
* Appends a [driver] or [driver:model] badge whenever the driver field is present.
|
|
43
|
+
* Appends " cost: $X.XXX" if a numeric cost field is present.
|
|
19
44
|
*
|
|
20
|
-
* Payload format (JSON): { text: string, driver?: string, model?: string }
|
|
45
|
+
* Payload format (JSON): { text: string, driver?: string, model?: string, cost?: number }
|
|
21
46
|
* Falls back to raw string if not valid JSON.
|
|
22
47
|
*/
|
|
23
48
|
export function parseNotification(raw) {
|
|
24
49
|
let text = raw;
|
|
25
50
|
let driver;
|
|
26
51
|
let model;
|
|
52
|
+
let cost;
|
|
27
53
|
try {
|
|
28
54
|
const parsed = JSON.parse(raw);
|
|
29
55
|
if (parsed.text)
|
|
30
56
|
text = parsed.text;
|
|
31
57
|
driver = parsed.driver;
|
|
32
58
|
model = parsed.model;
|
|
59
|
+
if (typeof parsed.cost === "number")
|
|
60
|
+
cost = parsed.cost;
|
|
33
61
|
}
|
|
34
62
|
catch {
|
|
35
63
|
// not JSON — use raw string as-is, no badge
|
|
36
64
|
return text;
|
|
37
65
|
}
|
|
38
|
-
//
|
|
39
|
-
if (!driver
|
|
66
|
+
// Show badge whenever driver field is present
|
|
67
|
+
if (!driver)
|
|
40
68
|
return text;
|
|
41
|
-
const
|
|
42
|
-
|
|
69
|
+
const shortModel = shortenModelName(model ?? "", driver);
|
|
70
|
+
const badge = shortModel ? `${driver}:${shortModel}` : driver;
|
|
71
|
+
const costStr = cost != null ? ` cost: $${cost.toFixed(3)}` : "";
|
|
72
|
+
return `${text}\n[${badge}]${costStr}`;
|
|
43
73
|
}
|
|
44
74
|
/**
|
|
45
75
|
* Write a message to the chat log in Redis.
|
|
@@ -101,6 +131,65 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
|
|
|
101
131
|
log("info", `subscribed to cca:chat:incoming:${namespace}`);
|
|
102
132
|
}
|
|
103
133
|
});
|
|
134
|
+
// cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
|
|
135
|
+
// Using psubscribe so we catch all namespaces (money-brain, isoc-nevada, etc.)
|
|
136
|
+
sub.psubscribe("cca:chat:outgoing:*", (err) => {
|
|
137
|
+
if (err) {
|
|
138
|
+
log("error", "psubscribe cca:chat:outgoing:* failed:", err.message);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
log("info", "psubscribed to cca:chat:outgoing:*");
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// Per-namespace debounce buffer: accumulate streaming lines, flush after 1.5s silence
|
|
145
|
+
const metaAgentBuffers = new Map();
|
|
146
|
+
function flushMetaAgentBuffer(ns, targetChatId) {
|
|
147
|
+
const buf = metaAgentBuffers.get(ns);
|
|
148
|
+
if (!buf || !buf.text.trim())
|
|
149
|
+
return;
|
|
150
|
+
const text = stripAnsi(buf.text.trim());
|
|
151
|
+
buf.text = "";
|
|
152
|
+
buf.timer = null;
|
|
153
|
+
const chunks = splitLongMessage(text);
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
bot.sendMessage(targetChatId, chunk).catch((err) => {
|
|
156
|
+
log("warn", `meta-agent flush sendMessage failed (ns=${ns}):`, err.message);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
sub.on("pmessage", (pattern, channel, message) => {
|
|
161
|
+
void pattern; // used only as a type guard
|
|
162
|
+
const ns = channel.replace("cca:chat:outgoing:", "");
|
|
163
|
+
let parsed = null;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(message);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return; // non-JSON line — skip
|
|
169
|
+
}
|
|
170
|
+
// Only forward messages from the meta-agent (source=claude).
|
|
171
|
+
// cc-tg itself publishes to this channel with source "cc-tg"/"telegram"/"ui" — skip those.
|
|
172
|
+
if (parsed.source !== "claude")
|
|
173
|
+
return;
|
|
174
|
+
const content = parsed.content;
|
|
175
|
+
if (!content)
|
|
176
|
+
return;
|
|
177
|
+
const targetChatId = chatId ?? getActiveChatId?.();
|
|
178
|
+
if (targetChatId == null) {
|
|
179
|
+
log("warn", `meta-agent output: no chatId for namespace=${ns}, dropping line`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Accumulate into per-namespace buffer and (re-)arm debounce timer
|
|
183
|
+
let buf = metaAgentBuffers.get(ns);
|
|
184
|
+
if (!buf) {
|
|
185
|
+
buf = { text: "", timer: null };
|
|
186
|
+
metaAgentBuffers.set(ns, buf);
|
|
187
|
+
}
|
|
188
|
+
buf.text += (buf.text ? "\n" : "") + content;
|
|
189
|
+
if (buf.timer)
|
|
190
|
+
clearTimeout(buf.timer);
|
|
191
|
+
buf.timer = setTimeout(() => flushMetaAgentBuffer(ns, targetChatId), 1500);
|
|
192
|
+
});
|
|
104
193
|
// Poll the cca:notify:{namespace} LIST every 5 seconds.
|
|
105
194
|
// Jobs push to this list via RPUSH; pub/sub alone won't deliver those messages.
|
|
106
195
|
const notifyListKey = `cca:notify:${namespace}`;
|