@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 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
- // Fast path: already running
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
- if (s.status === "running")
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.37",
3
+ "version": "0.9.39",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {