@gonzih/cc-tg 0.9.37 → 0.9.39
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 +20 -0
- package/dist/bot.js +80 -0
- package/dist/router.js +11 -4
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class CcTgBot {
|
|
|
27
27
|
private namespace;
|
|
28
28
|
private lastActiveChatId?;
|
|
29
29
|
private cron;
|
|
30
|
+
/** In-memory cache of forum topic names: `${chatId}:${threadId}` → topic name */
|
|
31
|
+
private topicNameCache;
|
|
30
32
|
constructor(opts: BotOptions);
|
|
31
33
|
private registerBotCommands;
|
|
32
34
|
/** Write a message to the Redis chat log. Fire-and-forget — no-op if Redis is not configured. */
|
|
@@ -43,6 +45,13 @@ export declare class CcTgBot {
|
|
|
43
45
|
private replyToChat;
|
|
44
46
|
/** Parse THREAD_CWD_MAP env var — maps thread name or thread_id to a CWD path */
|
|
45
47
|
private getThreadCwdMap;
|
|
48
|
+
/**
|
|
49
|
+
* Parse FORUM_META_AGENT_ROUTING env var.
|
|
50
|
+
* "auto" (default) → route all forum topics to meta-agents
|
|
51
|
+
* "off" → disable forum routing entirely
|
|
52
|
+
* "topic-a,topic-b" → only route these named topics
|
|
53
|
+
*/
|
|
54
|
+
private getForumRoutingConfig;
|
|
46
55
|
private isAllowed;
|
|
47
56
|
private handleTelegram;
|
|
48
57
|
/**
|
|
@@ -85,4 +94,15 @@ export declare class CcTgBot {
|
|
|
85
94
|
export declare function enrichPromptWithUrls(text: string): Promise<string>;
|
|
86
95
|
/** List available skills from ~/.claude/skills/ */
|
|
87
96
|
export declare function listSkills(): string;
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a Telegram forum topic name into a meta-agent namespace.
|
|
99
|
+
* Rules: strip leading #, lowercase, spaces → hyphens, non-alphanumeric → hyphens,
|
|
100
|
+
* collapse and trim hyphens.
|
|
101
|
+
*
|
|
102
|
+
* Examples:
|
|
103
|
+
* "CC Suite" → "cc-suite"
|
|
104
|
+
* "#research" → "research"
|
|
105
|
+
* "of-stack" → "of-stack"
|
|
106
|
+
*/
|
|
107
|
+
export declare function normalizeTopicNamespace(name: string): string;
|
|
88
108
|
export declare function splitMessage(text: string, maxLen?: number): string[];
|
package/dist/bot.js
CHANGED
|
@@ -176,6 +176,8 @@ export class CcTgBot {
|
|
|
176
176
|
namespace;
|
|
177
177
|
lastActiveChatId;
|
|
178
178
|
cron;
|
|
179
|
+
/** In-memory cache of forum topic names: `${chatId}:${threadId}` → topic name */
|
|
180
|
+
topicNameCache = new Map();
|
|
179
181
|
constructor(opts) {
|
|
180
182
|
this.opts = opts;
|
|
181
183
|
this.redis = opts.redis;
|
|
@@ -250,6 +252,20 @@ export class CcTgBot {
|
|
|
250
252
|
return {};
|
|
251
253
|
}
|
|
252
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Parse FORUM_META_AGENT_ROUTING env var.
|
|
257
|
+
* "auto" (default) → route all forum topics to meta-agents
|
|
258
|
+
* "off" → disable forum routing entirely
|
|
259
|
+
* "topic-a,topic-b" → only route these named topics
|
|
260
|
+
*/
|
|
261
|
+
getForumRoutingConfig() {
|
|
262
|
+
const raw = process.env.FORUM_META_AGENT_ROUTING;
|
|
263
|
+
if (!raw || raw === "auto")
|
|
264
|
+
return "auto";
|
|
265
|
+
if (raw === "off")
|
|
266
|
+
return "off";
|
|
267
|
+
return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
268
|
+
}
|
|
253
269
|
isAllowed(userId) {
|
|
254
270
|
if (!this.opts.allowedUserIds?.length)
|
|
255
271
|
return true;
|
|
@@ -266,6 +282,26 @@ export class CcTgBot {
|
|
|
266
282
|
const threadName = rawMsg.forum_topic_created
|
|
267
283
|
? rawMsg.forum_topic_created.name
|
|
268
284
|
: undefined;
|
|
285
|
+
// Cache forum topic names from service messages so routing can look them up later
|
|
286
|
+
if (threadId !== undefined) {
|
|
287
|
+
if (threadName) {
|
|
288
|
+
this.topicNameCache.set(`${chatId}:${threadId}`, threadName);
|
|
289
|
+
}
|
|
290
|
+
// forum_topic_edited carries name only when the name was changed
|
|
291
|
+
const editedTopicName = rawMsg.forum_topic_edited?.name;
|
|
292
|
+
if (editedTopicName) {
|
|
293
|
+
this.topicNameCache.set(`${chatId}:${threadId}`, editedTopicName);
|
|
294
|
+
}
|
|
295
|
+
// Best-effort: first message in a topic often has reply_to_message pointing to the creation event
|
|
296
|
+
if (!this.topicNameCache.has(`${chatId}:${threadId}`)) {
|
|
297
|
+
const replyRaw = msg.reply_to_message;
|
|
298
|
+
const replyCreated = replyRaw?.forum_topic_created;
|
|
299
|
+
const replyName = replyCreated?.name;
|
|
300
|
+
if (replyName) {
|
|
301
|
+
this.topicNameCache.set(`${chatId}:${threadId}`, replyName);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
269
305
|
if (!this.isAllowed(userId)) {
|
|
270
306
|
await this.replyToChat(chatId, "Not authorized.", threadId);
|
|
271
307
|
return;
|
|
@@ -428,6 +464,31 @@ export class CcTgBot {
|
|
|
428
464
|
return;
|
|
429
465
|
}
|
|
430
466
|
}
|
|
467
|
+
// Forum topic → meta-agent routing (runs after hashtag routing so explicit #tag wins)
|
|
468
|
+
if (this.redis && threadId !== undefined) {
|
|
469
|
+
const topicKey = `${chatId}:${threadId}`;
|
|
470
|
+
const topicName = this.topicNameCache.get(topicKey);
|
|
471
|
+
if (topicName) {
|
|
472
|
+
const namespace = normalizeTopicNamespace(topicName);
|
|
473
|
+
const routingConfig = this.getForumRoutingConfig();
|
|
474
|
+
const shouldRoute = routingConfig === "auto" ||
|
|
475
|
+
(routingConfig instanceof Set && (routingConfig.has(topicName) || routingConfig.has(namespace)));
|
|
476
|
+
if (shouldRoute) {
|
|
477
|
+
const defaultOrg = process.env.DEFAULT_GITHUB_ORG ?? "gonzih";
|
|
478
|
+
const repoUrl = `https://github.com/${defaultOrg}/${namespace}`;
|
|
479
|
+
await this.replyToChat(chatId, `→ #${namespace} (meta-agent)`, threadId);
|
|
480
|
+
this.writeChatMessage("user", "telegram", text, chatId);
|
|
481
|
+
try {
|
|
482
|
+
await ensureMetaAgent(namespace, repoUrl, (toolName, args) => this.callCcAgentTool(toolName, args ?? {}), this.redis);
|
|
483
|
+
await routeToMetaAgent(namespace, text, this.redis);
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
await this.replyToChat(chatId, `Failed to route to #${namespace}: ${err.message}`, threadId);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
431
492
|
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
432
493
|
try {
|
|
433
494
|
const enriched = await enrichPromptWithUrls(text);
|
|
@@ -1523,6 +1584,25 @@ export function listSkills() {
|
|
|
1523
1584
|
}
|
|
1524
1585
|
return lines.join("\n");
|
|
1525
1586
|
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Normalize a Telegram forum topic name into a meta-agent namespace.
|
|
1589
|
+
* Rules: strip leading #, lowercase, spaces → hyphens, non-alphanumeric → hyphens,
|
|
1590
|
+
* collapse and trim hyphens.
|
|
1591
|
+
*
|
|
1592
|
+
* Examples:
|
|
1593
|
+
* "CC Suite" → "cc-suite"
|
|
1594
|
+
* "#research" → "research"
|
|
1595
|
+
* "of-stack" → "of-stack"
|
|
1596
|
+
*/
|
|
1597
|
+
export function normalizeTopicNamespace(name) {
|
|
1598
|
+
return name
|
|
1599
|
+
.replace(/^#+/, "") // strip leading # prefix
|
|
1600
|
+
.toLowerCase()
|
|
1601
|
+
.replace(/\s+/g, "-") // spaces → hyphens
|
|
1602
|
+
.replace(/[^a-z0-9._-]/g, "-") // non-alphanumeric/non-safe → hyphens
|
|
1603
|
+
.replace(/-+/g, "-") // collapse consecutive hyphens
|
|
1604
|
+
.replace(/^-|-$/g, ""); // trim leading/trailing hyphens
|
|
1605
|
+
}
|
|
1526
1606
|
export function splitMessage(text, maxLen = 4096) {
|
|
1527
1607
|
if (text.length <= maxLen)
|
|
1528
1608
|
return [text];
|
package/dist/router.js
CHANGED
|
@@ -58,13 +58,16 @@ export function parseRoutingTag(text) {
|
|
|
58
58
|
export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
|
|
59
59
|
const timeoutMs = parseInt(process.env.META_AGENT_TIMEOUT_MS ?? "10000", 10);
|
|
60
60
|
const statusKey = `cca:meta-agent:status:${namespace}`;
|
|
61
|
-
|
|
61
|
+
console.log(`[router] ensureMetaAgent namespace=${namespace} checking ${statusKey}`);
|
|
62
|
+
// Fast path: already running or idle (idle = ready to receive messages)
|
|
62
63
|
const statusRaw = await redis.get(statusKey);
|
|
63
64
|
if (statusRaw) {
|
|
64
65
|
try {
|
|
65
66
|
const status = JSON.parse(statusRaw);
|
|
66
|
-
if (status.status === "running")
|
|
67
|
+
if (status.status === "running" || status.status === "idle") {
|
|
68
|
+
console.log(`[router] meta-agent ${namespace} is already ready (status=${status.status})`);
|
|
67
69
|
return;
|
|
70
|
+
}
|
|
68
71
|
}
|
|
69
72
|
catch {
|
|
70
73
|
// Corrupt status value — fall through and restart
|
|
@@ -91,7 +94,7 @@ export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
|
|
|
91
94
|
if (result === null) {
|
|
92
95
|
throw new Error(`start_meta_agent returned null — tool may not be available in cc-agent`);
|
|
93
96
|
}
|
|
94
|
-
// Poll until the meta-agent reports "running"
|
|
97
|
+
// Poll until the meta-agent reports "running" or "idle" (both mean ready)
|
|
95
98
|
const deadline = Date.now() + timeoutMs;
|
|
96
99
|
while (Date.now() < deadline) {
|
|
97
100
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -99,13 +102,17 @@ export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
|
|
|
99
102
|
if (raw) {
|
|
100
103
|
try {
|
|
101
104
|
const s = JSON.parse(raw);
|
|
102
|
-
|
|
105
|
+
console.log(`[router] waiting for meta-agent ${namespace} — current status: ${s.status}`);
|
|
106
|
+
if (s.status === "running" || s.status === "idle")
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
105
109
|
catch {
|
|
106
110
|
// ignore parse errors, keep polling
|
|
107
111
|
}
|
|
108
112
|
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(`[router] waiting for meta-agent ${namespace} — no status key yet`);
|
|
115
|
+
}
|
|
109
116
|
}
|
|
110
117
|
throw new Error(`Meta-agent for ${namespace} did not become ready within ${timeoutMs}ms`);
|
|
111
118
|
}
|