@gonzih/cc-tg 0.9.0 → 0.9.1

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
@@ -23,10 +23,13 @@ export declare class CcTgBot {
23
23
  private botId;
24
24
  private redis?;
25
25
  private namespace;
26
+ private lastActiveChatId?;
26
27
  constructor(opts: BotOptions);
27
28
  private registerBotCommands;
28
29
  /** Write a message to the Redis chat log. Fire-and-forget — no-op if Redis is not configured. */
29
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;
30
33
  /** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
31
34
  private sessionKey;
32
35
  /**
package/dist/bot.js CHANGED
@@ -156,6 +156,7 @@ export class CcTgBot {
156
156
  botId = 0;
157
157
  redis;
158
158
  namespace;
159
+ lastActiveChatId;
159
160
  constructor(opts) {
160
161
  this.opts = opts;
161
162
  this.redis = opts.redis;
@@ -192,6 +193,10 @@ export class CcTgBot {
192
193
  };
193
194
  writeChatLog(this.redis, this.namespace, msg);
194
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
+ }
195
200
  /** Session key: "chatId:threadId" for topics, "chatId:main" for DMs/non-topic groups */
196
201
  sessionKey(chatId, threadId) {
197
202
  return `${chatId}:${threadId ?? 'main'}`;
@@ -243,6 +248,8 @@ export class CcTgBot {
243
248
  await this.replyToChat(chatId, "Not authorized.", threadId);
244
249
  return;
245
250
  }
251
+ // Track the last chat that sent us a message for the chat bridge
252
+ this.lastActiveChatId = chatId;
246
253
  // Group chat handling
247
254
  const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
248
255
  if (isGroup) {
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();
@@ -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.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {