@gonzih/cc-tg 0.9.45 → 0.9.47
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 +13 -12
- package/dist/index.js +2 -1
- package/dist/notifier.js +25 -24
- package/dist/router.js +4 -3
- package/package.json +2 -1
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(
|
|
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(
|
|
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(
|
|
584
|
-
.then(() => this.redis.expire(
|
|
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(
|
|
611
|
-
this.redis.lrange(
|
|
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(
|
|
644
|
+
await this.redis.lrem(VOICE_PENDING_KEY, 0, matchPending).catch(() => { });
|
|
644
645
|
if (matchFailed)
|
|
645
|
-
await this.redis.lrem(
|
|
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(
|
|
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(
|
|
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", "
|
|
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.
|
|
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(
|
|
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 =
|
|
80
|
-
const outKey =
|
|
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
|
-
//
|
|
123
|
-
sub.subscribe(
|
|
123
|
+
// notifyChannel(namespace) — forward job completion notifications to Telegram
|
|
124
|
+
sub.subscribe(notifyChannel(namespace), (err) => {
|
|
124
125
|
if (err) {
|
|
125
|
-
log("error", `subscribe
|
|
126
|
+
log("error", `subscribe ${notifyChannel(namespace)} failed:`, err.message);
|
|
126
127
|
}
|
|
127
128
|
else {
|
|
128
|
-
log("info", `subscribed to
|
|
129
|
+
log("info", `subscribed to ${notifyChannel(namespace)}`);
|
|
129
130
|
}
|
|
130
131
|
});
|
|
131
|
-
//
|
|
132
|
-
sub.subscribe(
|
|
132
|
+
// chatIncomingChannel(namespace) — messages from UI
|
|
133
|
+
sub.subscribe(chatIncomingChannel(namespace), (err) => {
|
|
133
134
|
if (err) {
|
|
134
|
-
log("error", `subscribe
|
|
135
|
+
log("error", `subscribe ${chatIncomingChannel(namespace)} failed:`, err.message);
|
|
135
136
|
}
|
|
136
137
|
else {
|
|
137
|
-
log("info", `subscribed to
|
|
138
|
+
log("info", `subscribed to ${chatIncomingChannel(namespace)}`);
|
|
138
139
|
}
|
|
139
140
|
});
|
|
140
|
-
//
|
|
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("
|
|
143
|
+
sub.psubscribe(chatOutgoingChannel("*"), (err) => {
|
|
143
144
|
if (err) {
|
|
144
|
-
log("error",
|
|
145
|
+
log("error", `psubscribe ${chatOutgoingChannel("*")} failed:`, err.message);
|
|
145
146
|
}
|
|
146
147
|
else {
|
|
147
|
-
log("info",
|
|
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 = `← [${ns}] ` + 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.
|
|
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
|
|
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 =
|
|
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
|
|
255
|
-
const
|
|
256
|
-
if (channel ===
|
|
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 ===
|
|
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(
|
|
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(
|
|
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 =
|
|
73
|
+
const statusKey = metaAgentStatusKey(namespace);
|
|
73
74
|
// State key written by startMetaAgent() directly — the source of truth for workspace existence.
|
|
74
|
-
const stateKey =
|
|
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(
|
|
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.
|
|
3
|
+
"version": "0.9.47",
|
|
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": {
|