@gonzih/cc-tg 0.9.50 → 0.9.51

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
@@ -32,6 +32,8 @@ export declare class CcTgBot {
32
32
  private cron;
33
33
  /** In-memory cache of forum topic names: `${chatId}:${threadId}` → topic name */
34
34
  private topicNameCache;
35
+ /** Pending /wiki update state: chatId → {repoSlug, pageName, threadId} — awaiting user's next message as content */
36
+ private pendingWikiUpdates;
35
37
  constructor(opts: BotOptions);
36
38
  private registerBotCommands;
37
39
  /** Write a message to the Redis chat log. Fire-and-forget — no-op if Redis is not configured. */
@@ -95,6 +97,14 @@ export declare class CcTgBot {
95
97
  private handleGetFile;
96
98
  private handleDrivers;
97
99
  private handleAgents;
100
+ private wikiKey;
101
+ private wikiUpdatedKey;
102
+ private handleWiki;
103
+ private handleWikiList;
104
+ private handleWikiShow;
105
+ private handleWikiUpdateContent;
106
+ private handleWikiDelete;
107
+ private handleWikiSync;
98
108
  private callCcAgentTool;
99
109
  private killSession;
100
110
  getMe(): Promise<TelegramBot.User>;
package/dist/bot.js CHANGED
@@ -36,6 +36,7 @@ const BOT_COMMANDS = [
36
36
  { command: "voice_retry", description: "Retry failed voice message transcriptions" },
37
37
  { command: "drivers", description: "List available agent drivers" },
38
38
  { command: "agents", description: "Show running meta-agents and their live status" },
39
+ { command: "wiki", description: "Manage wiki pages — list/show/update/delete/sync" },
39
40
  ];
40
41
  // Debounces streaming chunks. Resets on each chunk. Fires 800ms after last chunk.
41
42
  const FLUSH_DELAY_MS = 800;
@@ -179,6 +180,8 @@ export class CcTgBot {
179
180
  cron;
180
181
  /** In-memory cache of forum topic names: `${chatId}:${threadId}` → topic name */
181
182
  topicNameCache = new Map();
183
+ /** Pending /wiki update state: chatId → {repoSlug, pageName, threadId} — awaiting user's next message as content */
184
+ pendingWikiUpdates = new Map();
182
185
  constructor(opts) {
183
186
  this.opts = opts;
184
187
  this.redis = opts.redis;
@@ -348,6 +351,13 @@ export class CcTgBot {
348
351
  text = text.replace(new RegExp(`@${this.botUsername}\\s*`, "g"), "").trim();
349
352
  }
350
353
  const sessionKey = this.sessionKey(chatId, threadId);
354
+ // Pending /wiki update — next message is the new page content
355
+ const pendingWiki = this.pendingWikiUpdates.get(chatId);
356
+ if (pendingWiki && !text.startsWith("/")) {
357
+ this.pendingWikiUpdates.delete(chatId);
358
+ await this.handleWikiUpdateContent(chatId, pendingWiki.repoSlug, pendingWiki.pageName, text, pendingWiki.threadId);
359
+ return;
360
+ }
351
361
  // /start or /reset — kill existing session and ack
352
362
  if (text === "/start" || text === "/reset") {
353
363
  this.killSession(chatId, true, threadId);
@@ -448,6 +458,11 @@ export class CcTgBot {
448
458
  await this.handleAgents(chatId, threadId);
449
459
  return;
450
460
  }
461
+ // /wiki <subcommand> — manage wiki pages in Redis
462
+ if (text.startsWith("/wiki")) {
463
+ await this.handleWiki(chatId, text, threadId);
464
+ return;
465
+ }
451
466
  // #tag / #org/repo routing — delegate to meta-agent instead of local Claude session
452
467
  if (this.redis) {
453
468
  const routing = parseRoutingTag(text);
@@ -1394,6 +1409,181 @@ export class CcTgBot {
1394
1409
  await this.replyToChat(chatId, `Failed to get agents status: ${err.message}`, threadId);
1395
1410
  }
1396
1411
  }
1412
+ // ─── Wiki helpers ───────────────────────────────────────────────────────────
1413
+ wikiKey(repoSlug) {
1414
+ return `cca:wiki:${repoSlug}`;
1415
+ }
1416
+ wikiUpdatedKey(repoSlug) {
1417
+ return `cca:wiki:${repoSlug}:updated`;
1418
+ }
1419
+ async handleWiki(chatId, text, threadId) {
1420
+ const args = text.slice("/wiki".length).trim();
1421
+ const parts = args.split(/\s+/);
1422
+ const subCmd = parts[0] ?? "";
1423
+ if (subCmd === "list" || subCmd === "") {
1424
+ await this.handleWikiList(chatId, parts[1], threadId);
1425
+ return;
1426
+ }
1427
+ if (subCmd === "show") {
1428
+ const repoSlug = parts[1];
1429
+ const pageName = parts.slice(2).join(" ");
1430
+ if (!repoSlug || !pageName) {
1431
+ await this.replyToChat(chatId, "Usage: /wiki show <repo_slug> <page_name>", threadId);
1432
+ return;
1433
+ }
1434
+ await this.handleWikiShow(chatId, repoSlug, pageName, threadId);
1435
+ return;
1436
+ }
1437
+ if (subCmd === "update") {
1438
+ const repoSlug = parts[1];
1439
+ const pageName = parts.slice(2).join(" ");
1440
+ if (!repoSlug || !pageName) {
1441
+ await this.replyToChat(chatId, "Usage: /wiki update <repo_slug> <page_name>", threadId);
1442
+ return;
1443
+ }
1444
+ this.pendingWikiUpdates.set(chatId, { repoSlug, pageName, threadId });
1445
+ await this.replyToChat(chatId, `Send the new content for page "${pageName}" in repo "${repoSlug}":`, threadId);
1446
+ return;
1447
+ }
1448
+ if (subCmd === "delete") {
1449
+ const repoSlug = parts[1];
1450
+ const pageName = parts.slice(2).join(" ");
1451
+ if (!repoSlug || !pageName) {
1452
+ await this.replyToChat(chatId, "Usage: /wiki delete <repo_slug> <page_name>", threadId);
1453
+ return;
1454
+ }
1455
+ await this.handleWikiDelete(chatId, repoSlug, pageName, threadId);
1456
+ return;
1457
+ }
1458
+ if (subCmd === "sync") {
1459
+ await this.handleWikiSync(chatId, threadId);
1460
+ return;
1461
+ }
1462
+ await this.replyToChat(chatId, "Usage:\n/wiki list [repo_slug]\n/wiki show <repo_slug> <page_name>\n/wiki update <repo_slug> <page_name>\n/wiki delete <repo_slug> <page_name>\n/wiki sync", threadId);
1463
+ }
1464
+ async handleWikiList(chatId, repoSlug, threadId) {
1465
+ if (!this.redis) {
1466
+ await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1467
+ return;
1468
+ }
1469
+ if (repoSlug) {
1470
+ const pages = await this.redis.hkeys(this.wikiKey(repoSlug));
1471
+ if (!pages.length) {
1472
+ await this.replyToChat(chatId, `No wiki pages for "${repoSlug}".`, threadId);
1473
+ return;
1474
+ }
1475
+ const lines = pages.sort().map((p, i) => `${i + 1}. ${p}`);
1476
+ await this.replyToChat(chatId, `Wiki pages for ${repoSlug} (${pages.length}):\n${lines.join("\n")}`, threadId);
1477
+ return;
1478
+ }
1479
+ // List all repos — scan for cca:wiki:* keys, exclude :updated keys
1480
+ const keys = [];
1481
+ let cursor = "0";
1482
+ do {
1483
+ const [nextCursor, found] = await this.redis.scan(cursor, "MATCH", "cca:wiki:*", "COUNT", 100);
1484
+ cursor = nextCursor;
1485
+ keys.push(...found.filter((k) => !k.endsWith(":updated")));
1486
+ } while (cursor !== "0");
1487
+ if (!keys.length) {
1488
+ await this.replyToChat(chatId, "No wiki repos found.", threadId);
1489
+ return;
1490
+ }
1491
+ const counts = await Promise.all(keys.sort().map(async (key) => {
1492
+ const slug = key.slice("cca:wiki:".length);
1493
+ const count = await this.redis.hlen(key);
1494
+ return `${slug} (${count} pages)`;
1495
+ }));
1496
+ await this.replyToChat(chatId, `Wiki repos:\n${counts.join("\n")}`, threadId);
1497
+ }
1498
+ async handleWikiShow(chatId, repoSlug, pageName, threadId) {
1499
+ if (!this.redis) {
1500
+ await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1501
+ return;
1502
+ }
1503
+ const content = await this.redis.hget(this.wikiKey(repoSlug), pageName);
1504
+ if (!content) {
1505
+ await this.replyToChat(chatId, `Page "${pageName}" not found in "${repoSlug}".`, threadId);
1506
+ return;
1507
+ }
1508
+ const TG_LIMIT = 4000;
1509
+ const header = `📄 ${repoSlug}/${pageName}\n\n`;
1510
+ const fullText = header + content;
1511
+ if (fullText.length <= TG_LIMIT) {
1512
+ await this.replyToChat(chatId, fullText, threadId);
1513
+ return;
1514
+ }
1515
+ const truncated = fullText.slice(0, TG_LIMIT - 20) + "\n...(truncated)";
1516
+ await this.replyToChat(chatId, truncated, threadId);
1517
+ }
1518
+ async handleWikiUpdateContent(chatId, repoSlug, pageName, content, threadId) {
1519
+ if (!this.redis) {
1520
+ await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1521
+ return;
1522
+ }
1523
+ await this.redis.hset(this.wikiKey(repoSlug), pageName, content);
1524
+ await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1525
+ await this.replyToChat(chatId, `Updated "${pageName}" in "${repoSlug}".`, threadId);
1526
+ }
1527
+ async handleWikiDelete(chatId, repoSlug, pageName, threadId) {
1528
+ if (!this.redis) {
1529
+ await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1530
+ return;
1531
+ }
1532
+ const deleted = await this.redis.hdel(this.wikiKey(repoSlug), pageName);
1533
+ if (deleted) {
1534
+ await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1535
+ await this.replyToChat(chatId, `Deleted "${pageName}" from "${repoSlug}".`, threadId);
1536
+ }
1537
+ else {
1538
+ await this.replyToChat(chatId, `Page "${pageName}" not found in "${repoSlug}".`, threadId);
1539
+ }
1540
+ }
1541
+ async handleWikiSync(chatId, threadId) {
1542
+ if (!this.redis) {
1543
+ await this.replyToChat(chatId, "Redis not configured — wiki unavailable.", threadId);
1544
+ return;
1545
+ }
1546
+ const wikiDir = "/Users/feral/money-brain/wiki";
1547
+ if (!existsSync(wikiDir)) {
1548
+ await this.replyToChat(chatId, `Wiki directory not found: ${wikiDir}`, threadId);
1549
+ return;
1550
+ }
1551
+ let files;
1552
+ try {
1553
+ files = readdirSync(wikiDir).filter((f) => f.endsWith(".md"));
1554
+ }
1555
+ catch (err) {
1556
+ await this.replyToChat(chatId, `Failed to read wiki directory: ${err.message}`, threadId);
1557
+ return;
1558
+ }
1559
+ if (!files.length) {
1560
+ await this.replyToChat(chatId, "No .md files found in wiki directory.", threadId);
1561
+ return;
1562
+ }
1563
+ const repoSlug = "gonzih-money-brain";
1564
+ let synced = 0;
1565
+ const errors = [];
1566
+ for (const file of files) {
1567
+ const pageName = file.replace(/\.md$/, "");
1568
+ try {
1569
+ const content = readFileSync(join(wikiDir, file), "utf8");
1570
+ await this.redis.hset(this.wikiKey(repoSlug), pageName, content);
1571
+ synced++;
1572
+ }
1573
+ catch (err) {
1574
+ errors.push(`${file}: ${err.message}`);
1575
+ }
1576
+ }
1577
+ if (synced > 0) {
1578
+ await this.redis.set(this.wikiUpdatedKey(repoSlug), new Date().toISOString());
1579
+ }
1580
+ let reply = `Synced ${synced}/${files.length} pages to cca:wiki:${repoSlug}`;
1581
+ if (errors.length) {
1582
+ reply += `\nErrors:\n${errors.join("\n")}`;
1583
+ }
1584
+ await this.replyToChat(chatId, reply, threadId);
1585
+ }
1586
+ // ────────────────────────────────────────────────────────────────────────────
1397
1587
  callCcAgentTool(toolName, args = {}) {
1398
1588
  // For spawn tools, pass through the configured driver and model
1399
1589
  const spawnTools = new Set(["spawn_agent", "spawn_from_profile"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.50",
3
+ "version": "0.9.51",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {