@gonzih/cc-tg 0.9.45 → 0.9.46

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.js CHANGED
@@ -17,6 +17,7 @@ import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./to
17
17
  import { writeChatLog } from "./notifier.js";
18
18
  import { CronManager } from "./cron.js";
19
19
  import { parseRoutingTag, ensureMetaAgent, routeToMetaAgent } from "./router.js";
20
+ import { VOICE_PENDING_KEY, VOICE_FAILED_KEY, TTL, metaAgentStatusKey } from "@gonzih/cc-wire";
20
21
  const BOT_COMMANDS = [
21
22
  { command: "start", description: "Reset session and start fresh" },
22
23
  { command: "reset", description: "Reset Claude session" },
@@ -539,7 +540,7 @@ export class CcTgBot {
539
540
  timestamp: Date.now(),
540
541
  });
541
542
  if (this.redis) {
542
- await this.redis.rpush("voice:pending", pendingEntry).catch((err) => console.warn("[voice] redis rpush voice:pending failed:", err.message));
543
+ await this.redis.rpush(VOICE_PENDING_KEY, pendingEntry).catch((err) => console.warn("[voice] redis rpush voice:pending failed:", err.message));
543
544
  }
544
545
  try {
545
546
  const fileLink = await this.bot.getFileLink(fileId);
@@ -547,7 +548,7 @@ export class CcTgBot {
547
548
  console.log(`[voice:${chatId}] transcribed: ${transcript}`);
548
549
  // Remove from pending on success
549
550
  if (this.redis) {
550
- await this.redis.lrem("voice:pending", 0, pendingEntry).catch((err) => console.warn("[voice] redis lrem voice:pending failed:", err.message));
551
+ await this.redis.lrem(VOICE_PENDING_KEY, 0, pendingEntry).catch((err) => console.warn("[voice] redis lrem voice:pending failed:", err.message));
551
552
  }
552
553
  if (!transcript || transcript === "[empty transcription]") {
553
554
  await this.replyToChat(chatId, "Could not transcribe voice message.", threadId);
@@ -580,8 +581,8 @@ export class CcTgBot {
580
581
  error: errMsg,
581
582
  failed_at: Date.now(),
582
583
  });
583
- this.redis.rpush("voice:failed", failedEntry)
584
- .then(() => this.redis.expire("voice:failed", 48 * 60 * 60))
584
+ this.redis.rpush(VOICE_FAILED_KEY, failedEntry)
585
+ .then(() => this.redis.expire(VOICE_FAILED_KEY, TTL.VOICE_FAILED_SECONDS))
585
586
  .catch((redisErr) => console.warn("[voice] redis write voice:failed failed:", redisErr.message));
586
587
  }
587
588
  // User-friendly error messages
@@ -607,8 +608,8 @@ export class CcTgBot {
607
608
  return;
608
609
  }
609
610
  const [pendingRaw, failedRaw] = await Promise.all([
610
- this.redis.lrange("voice:pending", 0, -1).catch(() => []),
611
- this.redis.lrange("voice:failed", 0, -1).catch(() => []),
611
+ this.redis.lrange(VOICE_PENDING_KEY, 0, -1).catch(() => []),
612
+ this.redis.lrange(VOICE_FAILED_KEY, 0, -1).catch(() => []),
612
613
  ]);
613
614
  // Deduplicate by file_id across both lists
614
615
  const allEntries = new Map();
@@ -640,9 +641,9 @@ export class CcTgBot {
640
641
  const matchPending = pendingRaw.find((r) => r.includes(`"${fileId}"`));
641
642
  const matchFailed = failedRaw.find((r) => r.includes(`"${fileId}"`));
642
643
  if (matchPending)
643
- await this.redis.lrem("voice:pending", 0, matchPending).catch(() => { });
644
+ await this.redis.lrem(VOICE_PENDING_KEY, 0, matchPending).catch(() => { });
644
645
  if (matchFailed)
645
- await this.redis.lrem("voice:failed", 0, matchFailed).catch(() => { });
646
+ await this.redis.lrem(VOICE_FAILED_KEY, 0, matchFailed).catch(() => { });
646
647
  succeeded++;
647
648
  }
648
649
  else {
@@ -658,7 +659,7 @@ export class CcTgBot {
658
659
  if (errMsg.includes("Bad Request") || errMsg.includes("file_id")) {
659
660
  const matchPending = pendingRaw.find((r) => r.includes(`"${fileId}"`));
660
661
  if (matchPending)
661
- await this.redis.lrem("voice:pending", 0, matchPending).catch(() => { });
662
+ await this.redis.lrem(VOICE_PENDING_KEY, 0, matchPending).catch(() => { });
662
663
  }
663
664
  }
664
665
  }
@@ -669,7 +670,7 @@ export class CcTgBot {
669
670
  try {
670
671
  const entry = JSON.parse(raw);
671
672
  if (entry.timestamp && Date.now() - entry.timestamp > staleThreshold) {
672
- await this.redis.lrem("voice:pending", 0, raw).catch(() => { });
673
+ await this.redis.lrem(VOICE_PENDING_KEY, 0, raw).catch(() => { });
673
674
  purged++;
674
675
  }
675
676
  }
@@ -1320,7 +1321,7 @@ export class CcTgBot {
1320
1321
  const keys = [];
1321
1322
  let cursor = "0";
1322
1323
  do {
1323
- const [nextCursor, found] = await this.redis.scan(cursor, "MATCH", "cca:meta-agent:status:*", "COUNT", 100);
1324
+ const [nextCursor, found] = await this.redis.scan(cursor, "MATCH", metaAgentStatusKey("*"), "COUNT", 100);
1324
1325
  cursor = nextCursor;
1325
1326
  keys.push(...found);
1326
1327
  } while (cursor !== "0");
@@ -1331,7 +1332,7 @@ export class CcTgBot {
1331
1332
  const statuses = await Promise.all(keys.sort().map(async (key) => ({ key, raw: await this.redis.get(key) })));
1332
1333
  const lines = ["🤖 Active Agents", ""];
1333
1334
  for (const { key, raw } of statuses) {
1334
- const namespace = key.replace("cca:meta-agent:status:", "");
1335
+ const namespace = key.slice(metaAgentStatusKey("").length);
1335
1336
  if (!raw) {
1336
1337
  lines.push(`${namespace} — status unknown`);
1337
1338
  continue;
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { Registry, startControlServer } from "@gonzih/agent-ops";
27
27
  import { Redis } from "ioredis";
28
28
  import { startNotifier } from "./notifier.js";
29
29
  import { seedClaudeMd } from "./seed.js";
30
+ import { CC_TG_VERSION_KEY } from "@gonzih/cc-wire";
30
31
  const __filename = fileURLToPath(import.meta.url);
31
32
  const __dirname = dirname(__filename);
32
33
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
@@ -125,7 +126,7 @@ sharedRedis.on("error", (err) => {
125
126
  console.warn("[redis] connection error:", err.message);
126
127
  });
127
128
  sharedRedis.once("ready", () => {
128
- sharedRedis.set("cca:meta:cc-tg:version", pkg.version).catch((err) => {
129
+ sharedRedis.set(CC_TG_VERSION_KEY, pkg.version).catch((err) => {
129
130
  console.warn("[redis] failed to write version:", err.message);
130
131
  });
131
132
  console.log(`[cc-tg] version:reported ${pkg.version}`);
package/dist/notifier.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
11
11
  * cca:chat:outgoing:{namespace} — PUBLISH for web UI to consume
12
12
  */
13
+ import { chatLogKey, chatOutgoingChannel, chatIncomingChannel, notifyChannel, metaAgentStatusKey, metaInputKey, } from "@gonzih/cc-wire";
13
14
  import { splitLongMessage } from "./formatter.js";
14
15
  function log(level, ...args) {
15
16
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
@@ -76,8 +77,8 @@ export function parseNotification(raw) {
76
77
  * Fire-and-forget — errors are logged but not thrown.
77
78
  */
78
79
  export function writeChatLog(redis, namespace, msg) {
79
- const logKey = `cca:chat:log:${namespace}`;
80
- const outKey = `cca:chat:outgoing:${namespace}`;
80
+ const logKey = chatLogKey(namespace);
81
+ const outKey = chatOutgoingChannel(namespace);
81
82
  const payload = JSON.stringify(msg);
82
83
  // LIFO — newest first. Consumers must LRANGE 0 N then reverse for chronological order.
83
84
  redis.lpush(logKey, payload).catch((err) => {
@@ -119,32 +120,32 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
119
120
  sub.on("close", () => {
120
121
  log("info", "subscriber disconnected, will reconnect with backoff");
121
122
  });
122
- // cca:notify:{namespace} — forward job completion notifications to Telegram
123
- sub.subscribe(`cca:notify:${namespace}`, (err) => {
123
+ // notifyChannel(namespace) — forward job completion notifications to Telegram
124
+ sub.subscribe(notifyChannel(namespace), (err) => {
124
125
  if (err) {
125
- log("error", `subscribe cca:notify:${namespace} failed:`, err.message);
126
+ log("error", `subscribe ${notifyChannel(namespace)} failed:`, err.message);
126
127
  }
127
128
  else {
128
- log("info", `subscribed to cca:notify:${namespace}`);
129
+ log("info", `subscribed to ${notifyChannel(namespace)}`);
129
130
  }
130
131
  });
131
- // cca:chat:incoming:{namespace} — messages from UI
132
- sub.subscribe(`cca:chat:incoming:${namespace}`, (err) => {
132
+ // chatIncomingChannel(namespace) — messages from UI
133
+ sub.subscribe(chatIncomingChannel(namespace), (err) => {
133
134
  if (err) {
134
- log("error", `subscribe cca:chat:incoming:${namespace} failed:`, err.message);
135
+ log("error", `subscribe ${chatIncomingChannel(namespace)} failed:`, err.message);
135
136
  }
136
137
  else {
137
- log("info", `subscribed to cca:chat:incoming:${namespace}`);
138
+ log("info", `subscribed to ${chatIncomingChannel(namespace)}`);
138
139
  }
139
140
  });
140
- // cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
141
+ // chatOutgoingChannel("*") — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
141
142
  // Using psubscribe so we catch all namespaces (money-brain, isoc-nevada, etc.)
142
- sub.psubscribe("cca:chat:outgoing:*", (err) => {
143
+ sub.psubscribe(chatOutgoingChannel("*"), (err) => {
143
144
  if (err) {
144
- log("error", "psubscribe cca:chat:outgoing:* failed:", err.message);
145
+ log("error", `psubscribe ${chatOutgoingChannel("*")} failed:`, err.message);
145
146
  }
146
147
  else {
147
- log("info", "psubscribed to cca:chat:outgoing:*");
148
+ log("info", `psubscribed to ${chatOutgoingChannel("*")}`);
148
149
  }
149
150
  });
150
151
  // 1.5s silence buffer. Combined with cc-agent's 3s poll = up to 4.5s meta-agent response latency.
@@ -155,7 +156,7 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
155
156
  const buf = metaAgentBuffers.get(ns);
156
157
  if (!buf || !buf.text.trim())
157
158
  return;
158
- const text = stripAnsi(buf.text.trim());
159
+ const text = "← " + stripAnsi(buf.text.trim());
159
160
  buf.text = "";
160
161
  buf.timer = null;
161
162
  const chunks = splitLongMessage(text);
@@ -167,7 +168,7 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
167
168
  }
168
169
  sub.on("pmessage", (pattern, channel, message) => {
169
170
  void pattern; // used only as a type guard
170
- const ns = channel.replace("cca:chat:outgoing:", "");
171
+ const ns = channel.slice(chatOutgoingChannel("").length);
171
172
  let parsed = null;
172
173
  try {
173
174
  parsed = JSON.parse(message);
@@ -200,9 +201,9 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
200
201
  clearTimeout(buf.timer);
201
202
  buf.timer = setTimeout(() => flushMetaAgentBuffer(ns, targetChatId), META_AGENT_FLUSH_DELAY_MS);
202
203
  });
203
- // Poll the cca:notify:{namespace} LIST every 5 seconds.
204
+ // Poll the notifyChannel(namespace) LIST every 5 seconds.
204
205
  // Jobs push to this list via RPUSH; pub/sub alone won't deliver those messages.
205
- const notifyListKey = `cca:notify:${namespace}`;
206
+ const notifyListKey = notifyChannel(namespace);
206
207
  const MAX_PER_CYCLE = 20;
207
208
  const pollNotifyList = async () => {
208
209
  const targetId = chatId ?? getActiveChatId?.();
@@ -251,9 +252,9 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
251
252
  void pollNotifyList();
252
253
  }, 5_000);
253
254
  sub.on("message", (channel, message) => {
254
- const notifyChannel = `cca:notify:${namespace}`;
255
- const incomingChannel = `cca:chat:incoming:${namespace}`;
256
- if (channel === notifyChannel) {
255
+ const notifyCh = notifyChannel(namespace);
256
+ const incomingCh = chatIncomingChannel(namespace);
257
+ if (channel === notifyCh) {
257
258
  const targetId = chatId ?? getActiveChatId?.();
258
259
  if (targetId != null) {
259
260
  const text = parseNotification(message);
@@ -269,7 +270,7 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
269
270
  }
270
271
  return;
271
272
  }
272
- if (channel === incomingChannel) {
273
+ if (channel === incomingCh) {
273
274
  let content = message;
274
275
  let originalTimestamp;
275
276
  try {
@@ -304,7 +305,7 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
304
305
  void (async () => {
305
306
  let routedToMetaAgent = false;
306
307
  try {
307
- const statusRaw = await redis.get(`cca:meta-agent:status:${namespace}`);
308
+ const statusRaw = await redis.get(metaAgentStatusKey(namespace));
308
309
  if (statusRaw) {
309
310
  const status = JSON.parse(statusRaw);
310
311
  if (status.status === "running") {
@@ -314,7 +315,7 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
314
315
  timestamp: new Date().toISOString(),
315
316
  });
316
317
  // Polled by cc-agent every 3s — up to 3s delivery latency
317
- await redis.rpush(`cca:meta:${namespace}:input`, entry);
318
+ await redis.rpush(metaInputKey(namespace), entry);
318
319
  log("info", `cca:chat:incoming: routed to meta-agent for namespace ${namespace}`);
319
320
  routedToMetaAgent = true;
320
321
  }
package/dist/router.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * #org/repo → namespace=repo, repo=https://github.com/org/repo
10
10
  */
11
11
  import { execSync } from "child_process";
12
+ import { metaAgentStatusKey, metaKey, metaInputKey } from "@gonzih/cc-wire";
12
13
  /**
13
14
  * Parse the first #tag or #org/repo token from a message.
14
15
  * Returns null when no routing tag is present, or when the short #repo format is used
@@ -69,9 +70,9 @@ export function parseRoutingTag(text) {
69
70
  */
70
71
  export async function ensureMetaAgent(namespace, repoUrl, callTool, redis) {
71
72
  const timeoutMs = parseInt(process.env.META_AGENT_TIMEOUT_MS ?? "10000", 10);
72
- const statusKey = `cca:meta-agent:status:${namespace}`;
73
+ const statusKey = metaAgentStatusKey(namespace);
73
74
  // State key written by startMetaAgent() directly — the source of truth for workspace existence.
74
- const stateKey = `cca:meta:${namespace}`;
75
+ const stateKey = metaKey(namespace);
75
76
  console.log(`[router] ensureMetaAgent namespace=${namespace}`);
76
77
  // Fast path: check live-status key (written by messageMetaAgent after first message)
77
78
  const statusRaw = await redis.get(statusKey);
@@ -187,6 +188,6 @@ export async function routeToMetaAgent(namespace, strippedMessage, redis) {
187
188
  timestamp: new Date().toISOString(),
188
189
  });
189
190
  // FIFO — cc-agent reads via LPOP
190
- await redis.rpush(`cca:meta:${namespace}:input`, entry);
191
+ await redis.rpush(metaInputKey(namespace), entry);
191
192
  console.log(`[router] routed message to meta-agent namespace=${namespace}`);
192
193
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.45",
3
+ "version": "0.9.46",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@gonzih/agent-ops": "^0.1.0",
22
+ "@gonzih/cc-wire": "^0.1.0",
22
23
  "node-telegram-bot-api": "^0.66.0"
23
24
  },
24
25
  "devDependencies": {