@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 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:{namespace} and cca:chat:incoming:{namespace}
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
- if (notifyChatId) {
155
- if (!sharedRedis)
156
- sharedRedis = new Redis(redisUrl);
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();
@@ -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
- bot.sendMessage(chatId, message).catch((err) => {
80
- log("warn", "sendMessage failed:", err.message);
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
- // Echo to Telegram so the user sees UI messages in the chat
95
- bot.sendMessage(chatId, `📱 [from UI]: ${content}`).catch((err) => {
96
- log("warn", "sendMessage (UI echo) failed:", err.message);
97
- });
98
- // Log the incoming message
99
- const inMsg = {
100
- id: `ui-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
101
- source: "ui",
102
- role: "user",
103
- content,
104
- timestamp: new Date().toISOString(),
105
- chatId,
106
- };
107
- writeChatLog(redis, namespace, inMsg);
108
- // Feed into active Claude session as if user typed it
109
- if (handleUserMessage) {
110
- handleUserMessage(chatId, content);
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {