@gonzih/cc-tg 0.8.1 → 0.8.2
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 +10 -0
- package/dist/bot.js +43 -0
- package/dist/index.js +17 -13
- package/dist/notifier.d.ts +5 -4
- package/dist/notifier.js +32 -22
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* One ClaudeProcess per chat_id — sessions are isolated per user.
|
|
4
4
|
*/
|
|
5
5
|
import TelegramBot from "node-telegram-bot-api";
|
|
6
|
+
import { Redis } from "ioredis";
|
|
6
7
|
export interface BotOptions {
|
|
7
8
|
telegramToken: string;
|
|
8
9
|
claudeToken?: string;
|
|
9
10
|
cwd?: string;
|
|
10
11
|
allowedUserIds?: number[];
|
|
11
12
|
groupChatIds?: number[];
|
|
13
|
+
redis?: Redis;
|
|
14
|
+
namespace?: string;
|
|
12
15
|
}
|
|
13
16
|
export declare class CcTgBot {
|
|
14
17
|
private bot;
|
|
@@ -18,8 +21,15 @@ export declare class CcTgBot {
|
|
|
18
21
|
private costStore;
|
|
19
22
|
private botUsername;
|
|
20
23
|
private botId;
|
|
24
|
+
private redis?;
|
|
25
|
+
private namespace;
|
|
26
|
+
private lastActiveChatId?;
|
|
21
27
|
constructor(opts: BotOptions);
|
|
22
28
|
private registerBotCommands;
|
|
29
|
+
/** Write a message to the Redis chat log. Fire-and-forget — no-op if Redis is not configured. */
|
|
30
|
+
private writeChatMessage;
|
|
31
|
+
/** Returns the last chatId that sent a message — used by the chat bridge when no fixed chatId is configured. */
|
|
32
|
+
getLastActiveChatId(): number | undefined;
|
|
23
33
|
/** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
|
|
24
34
|
private sessionKey;
|
|
25
35
|
/**
|
package/dist/bot.js
CHANGED
|
@@ -14,6 +14,7 @@ import { transcribeVoice, isVoiceAvailable } from "./voice.js";
|
|
|
14
14
|
import { formatForTelegram, splitLongMessage } from "./formatter.js";
|
|
15
15
|
import { detectUsageLimit } from "./usage-limit.js";
|
|
16
16
|
import { getCurrentToken, rotateToken, getTokenIndex, getTokenCount } from "./tokens.js";
|
|
17
|
+
import { writeChatLog } from "./notifier.js";
|
|
17
18
|
const BOT_COMMANDS = [
|
|
18
19
|
{ command: "start", description: "Reset session and start fresh" },
|
|
19
20
|
{ command: "reset", description: "Reset Claude session" },
|
|
@@ -153,8 +154,13 @@ export class CcTgBot {
|
|
|
153
154
|
costStore;
|
|
154
155
|
botUsername = "";
|
|
155
156
|
botId = 0;
|
|
157
|
+
redis;
|
|
158
|
+
namespace;
|
|
159
|
+
lastActiveChatId;
|
|
156
160
|
constructor(opts) {
|
|
157
161
|
this.opts = opts;
|
|
162
|
+
this.redis = opts.redis;
|
|
163
|
+
this.namespace = opts.namespace ?? "default";
|
|
158
164
|
this.bot = new TelegramBot(opts.telegramToken, { polling: true });
|
|
159
165
|
this.bot.on("message", (msg) => this.handleTelegram(msg));
|
|
160
166
|
this.bot.on("polling_error", (err) => console.error("[tg]", err.message));
|
|
@@ -173,6 +179,24 @@ export class CcTgBot {
|
|
|
173
179
|
.then(() => console.log("[tg] bot commands registered"))
|
|
174
180
|
.catch((err) => console.error("[tg] setMyCommands failed:", err.message));
|
|
175
181
|
}
|
|
182
|
+
/** Write a message to the Redis chat log. Fire-and-forget — no-op if Redis is not configured. */
|
|
183
|
+
writeChatMessage(role, source, content, chatId) {
|
|
184
|
+
if (!this.redis)
|
|
185
|
+
return;
|
|
186
|
+
const msg = {
|
|
187
|
+
id: `${source}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
188
|
+
source,
|
|
189
|
+
role,
|
|
190
|
+
content,
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
chatId,
|
|
193
|
+
};
|
|
194
|
+
writeChatLog(this.redis, this.namespace, msg);
|
|
195
|
+
}
|
|
196
|
+
/** Returns the last chatId that sent a message — used by the chat bridge when no fixed chatId is configured. */
|
|
197
|
+
getLastActiveChatId() {
|
|
198
|
+
return this.lastActiveChatId;
|
|
199
|
+
}
|
|
176
200
|
/** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
|
|
177
201
|
sessionKey(chatId, threadId) {
|
|
178
202
|
return `${chatId}:${threadId ?? 'main'}`;
|
|
@@ -224,6 +248,8 @@ export class CcTgBot {
|
|
|
224
248
|
await this.replyToChat(chatId, "Not authorized.", threadId);
|
|
225
249
|
return;
|
|
226
250
|
}
|
|
251
|
+
// Track the last chat that sent us a message for the chat bridge
|
|
252
|
+
this.lastActiveChatId = chatId;
|
|
227
253
|
// Group chat handling
|
|
228
254
|
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
|
|
229
255
|
if (isGroup) {
|
|
@@ -350,6 +376,7 @@ export class CcTgBot {
|
|
|
350
376
|
session.currentPrompt = prompt;
|
|
351
377
|
session.claude.sendPrompt(prompt);
|
|
352
378
|
this.startTyping(chatId, session);
|
|
379
|
+
this.writeChatMessage("user", "telegram", text, chatId);
|
|
353
380
|
}
|
|
354
381
|
catch (err) {
|
|
355
382
|
await this.replyToChat(chatId, `Error sending to Claude: ${err.message}`, threadId);
|
|
@@ -367,6 +394,7 @@ export class CcTgBot {
|
|
|
367
394
|
session.currentPrompt = enriched;
|
|
368
395
|
session.claude.sendPrompt(enriched);
|
|
369
396
|
this.startTyping(chatId, session);
|
|
397
|
+
this.writeChatMessage("user", "ui", text, chatId);
|
|
370
398
|
}
|
|
371
399
|
catch (err) {
|
|
372
400
|
await this.replyToChat(chatId, `Error sending to Claude: ${err.message}`);
|
|
@@ -495,6 +523,20 @@ export class CcTgBot {
|
|
|
495
523
|
console.log(logParts.join(" "));
|
|
496
524
|
// Track files written by Write/Edit tool calls
|
|
497
525
|
this.trackWrittenFiles(msg, session, sessionCwd);
|
|
526
|
+
// Publish tool call events to the chat log
|
|
527
|
+
if (msg.type === "assistant") {
|
|
528
|
+
const message = msg.payload.message;
|
|
529
|
+
const content = message?.content;
|
|
530
|
+
if (Array.isArray(content)) {
|
|
531
|
+
for (const block of content) {
|
|
532
|
+
if (block.type !== "tool_use")
|
|
533
|
+
continue;
|
|
534
|
+
const name = block.name;
|
|
535
|
+
const input = block.input;
|
|
536
|
+
this.writeChatMessage("tool", "cc-tg", `[tool] ${name}: ${JSON.stringify(input ?? {}).slice(0, 120)}`, chatId);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
498
540
|
this.handleClaudeMessage(chatId, session, msg);
|
|
499
541
|
});
|
|
500
542
|
claude.on("stderr", (data) => {
|
|
@@ -611,6 +653,7 @@ export class CcTgBot {
|
|
|
611
653
|
session.flushTimer = null;
|
|
612
654
|
if (!raw)
|
|
613
655
|
return;
|
|
656
|
+
this.writeChatMessage("assistant", "cc-tg", raw, chatId);
|
|
614
657
|
const text = session.isRetry ? `✅ Claude is back!\n\n${raw}` : raw;
|
|
615
658
|
session.isRetry = false;
|
|
616
659
|
// Format for Telegram HTML and split if needed (max 4096 chars)
|
package/dist/index.js
CHANGED
|
@@ -113,20 +113,26 @@ const groupChatIds = process.env.GROUP_CHAT_IDS
|
|
|
113
113
|
? process.env.GROUP_CHAT_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
114
114
|
: [];
|
|
115
115
|
const cwd = process.env.CWD ?? process.cwd();
|
|
116
|
+
// agent-ops / chat bridge — Redis is always initialized so the chat bridge works
|
|
117
|
+
// regardless of whether CC_AGENT_OPS_PORT or CC_AGENT_NOTIFY_CHAT_ID are set.
|
|
118
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
119
|
+
const namespace = process.env.CC_AGENT_NAMESPACE || "default";
|
|
120
|
+
const sharedRedis = new Redis(redisUrl);
|
|
121
|
+
sharedRedis.on("error", (err) => {
|
|
122
|
+
// Non-fatal — Redis features (chat bridge, ops) degrade gracefully
|
|
123
|
+
console.warn("[redis] connection error:", err.message);
|
|
124
|
+
});
|
|
116
125
|
const bot = new CcTgBot({
|
|
117
126
|
telegramToken,
|
|
118
127
|
claudeToken,
|
|
119
128
|
cwd,
|
|
120
129
|
allowedUserIds,
|
|
121
130
|
groupChatIds,
|
|
131
|
+
redis: sharedRedis,
|
|
132
|
+
namespace,
|
|
122
133
|
});
|
|
123
|
-
// agent-ops: optional self-registration + HTTP control endpoint
|
|
124
|
-
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
125
|
-
const namespace = process.env.CC_AGENT_NAMESPACE || "default";
|
|
126
|
-
let sharedRedis = null;
|
|
127
134
|
if (process.env.CC_AGENT_OPS_PORT) {
|
|
128
135
|
const botInfo = await bot.getMe();
|
|
129
|
-
sharedRedis = new Redis(redisUrl);
|
|
130
136
|
const registry = new Registry(sharedRedis);
|
|
131
137
|
await registry.register({
|
|
132
138
|
namespace,
|
|
@@ -147,17 +153,15 @@ if (process.env.CC_AGENT_OPS_PORT) {
|
|
|
147
153
|
});
|
|
148
154
|
console.log(`[ops] control server on port ${process.env.CC_AGENT_OPS_PORT}`);
|
|
149
155
|
}
|
|
150
|
-
// Notifier — subscribe to cca:notify
|
|
156
|
+
// Notifier — always subscribe to cca:notify and cca:chat:incoming channels.
|
|
157
|
+
// CC_AGENT_NOTIFY_CHAT_ID pins a fixed Telegram chatId; without it the last
|
|
158
|
+
// active chatId is used dynamically for the chat bridge.
|
|
151
159
|
const notifyChatId = process.env.CC_AGENT_NOTIFY_CHAT_ID
|
|
152
160
|
? Number(process.env.CC_AGENT_NOTIFY_CHAT_ID)
|
|
153
161
|
: null;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const notifierBot = new TelegramBot(telegramToken, { polling: false });
|
|
158
|
-
startNotifier(notifierBot, notifyChatId, namespace, sharedRedis, (cid, text) => bot.handleUserMessage(cid, text));
|
|
159
|
-
console.log(`[notifier] started for namespace=${namespace} chatId=${notifyChatId}`);
|
|
160
|
-
}
|
|
162
|
+
const notifierBot = new TelegramBot(telegramToken, { polling: false });
|
|
163
|
+
startNotifier(notifierBot, notifyChatId, namespace, sharedRedis, (cid, text) => bot.handleUserMessage(cid, text), () => bot.getLastActiveChatId());
|
|
164
|
+
console.log(`[notifier] started for namespace=${namespace} chatId=${notifyChatId ?? "dynamic"}`);
|
|
161
165
|
process.on("SIGINT", () => {
|
|
162
166
|
console.log("\nShutting down...");
|
|
163
167
|
bot.stop();
|
package/dist/notifier.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { Redis } from "ioredis";
|
|
|
13
13
|
import TelegramBot from "node-telegram-bot-api";
|
|
14
14
|
export interface ChatMessage {
|
|
15
15
|
id: string;
|
|
16
|
-
source: "telegram" | "ui" | "claude";
|
|
17
|
-
role: "user" | "assistant";
|
|
16
|
+
source: "telegram" | "ui" | "claude" | "cc-tg";
|
|
17
|
+
role: "user" | "assistant" | "tool";
|
|
18
18
|
content: string;
|
|
19
19
|
timestamp: string;
|
|
20
20
|
chatId: number;
|
|
@@ -28,9 +28,10 @@ export declare function writeChatLog(redis: Redis, namespace: string, msg: ChatM
|
|
|
28
28
|
* Start the notifier.
|
|
29
29
|
*
|
|
30
30
|
* @param bot - Telegram bot instance (for sending messages)
|
|
31
|
-
* @param chatId - Telegram chat ID to forward notifications to
|
|
31
|
+
* @param chatId - Telegram chat ID to forward notifications to. Pass null to use getActiveChatId.
|
|
32
32
|
* @param namespace - cc-agent namespace (used to build Redis channel names)
|
|
33
33
|
* @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
|
|
34
34
|
* @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
|
|
35
|
+
* @param getActiveChatId - Optional callback to resolve chatId dynamically (used when chatId is null)
|
|
35
36
|
*/
|
|
36
|
-
export declare function startNotifier(bot: TelegramBot, chatId: number, namespace: string, redis: Redis, handleUserMessage?: (chatId: number, text: string) => void): void;
|
|
37
|
+
export declare function startNotifier(bot: TelegramBot, chatId: number | null, namespace: string, redis: Redis, handleUserMessage?: (chatId: number, text: string) => void, getActiveChatId?: () => number | undefined): void;
|
package/dist/notifier.js
CHANGED
|
@@ -35,12 +35,13 @@ export function writeChatLog(redis, namespace, msg) {
|
|
|
35
35
|
* Start the notifier.
|
|
36
36
|
*
|
|
37
37
|
* @param bot - Telegram bot instance (for sending messages)
|
|
38
|
-
* @param chatId - Telegram chat ID to forward notifications to
|
|
38
|
+
* @param chatId - Telegram chat ID to forward notifications to. Pass null to use getActiveChatId.
|
|
39
39
|
* @param namespace - cc-agent namespace (used to build Redis channel names)
|
|
40
40
|
* @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
|
|
41
41
|
* @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
|
|
42
|
+
* @param getActiveChatId - Optional callback to resolve chatId dynamically (used when chatId is null)
|
|
42
43
|
*/
|
|
43
|
-
export function startNotifier(bot, chatId, namespace, redis, handleUserMessage) {
|
|
44
|
+
export function startNotifier(bot, chatId, namespace, redis, handleUserMessage, getActiveChatId) {
|
|
44
45
|
const sub = redis.duplicate({
|
|
45
46
|
retryStrategy: (times) => {
|
|
46
47
|
const delay = Math.min(1000 * Math.pow(2, times - 1), 30_000);
|
|
@@ -76,9 +77,11 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage)
|
|
|
76
77
|
const notifyChannel = `cca:notify:${namespace}`;
|
|
77
78
|
const incomingChannel = `cca:chat:incoming:${namespace}`;
|
|
78
79
|
if (channel === notifyChannel) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if (chatId !== null) {
|
|
81
|
+
bot.sendMessage(chatId, message).catch((err) => {
|
|
82
|
+
log("warn", "sendMessage failed:", err.message);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
82
85
|
return;
|
|
83
86
|
}
|
|
84
87
|
if (channel === incomingChannel) {
|
|
@@ -91,23 +94,30 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage)
|
|
|
91
94
|
catch {
|
|
92
95
|
// raw string message — use as-is
|
|
93
96
|
}
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
97
|
+
// Resolve the target chatId: prefer the fixed chatId, fall back to last active
|
|
98
|
+
const targetChatId = chatId ?? getActiveChatId?.();
|
|
99
|
+
if (targetChatId !== undefined) {
|
|
100
|
+
// Echo to Telegram so the user sees UI messages in the chat
|
|
101
|
+
bot.sendMessage(targetChatId, `📱 [from UI]: ${content}`).catch((err) => {
|
|
102
|
+
log("warn", "sendMessage (UI echo) failed:", err.message);
|
|
103
|
+
});
|
|
104
|
+
// Log the incoming message
|
|
105
|
+
const inMsg = {
|
|
106
|
+
id: `ui-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
107
|
+
source: "ui",
|
|
108
|
+
role: "user",
|
|
109
|
+
content,
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
chatId: targetChatId,
|
|
112
|
+
};
|
|
113
|
+
writeChatLog(redis, namespace, inMsg);
|
|
114
|
+
// Feed into active Claude session as if user typed it
|
|
115
|
+
if (handleUserMessage) {
|
|
116
|
+
handleUserMessage(targetChatId, content);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
log("warn", "cca:chat:incoming: no active chatId to route message to");
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
123
|
});
|