@gonzih/cc-tg 0.9.37 → 0.9.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.37",
3
+ "version": "0.9.38",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {