@gonzih/cc-tg 0.9.23 → 0.9.24

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.
@@ -4,6 +4,7 @@
4
4
  * Channels:
5
5
  * cca:notify:{namespace} — job completion notifications from cc-agent → forward to Telegram
6
6
  * cca:chat:incoming:{namespace} — messages from the web UI → echo to Telegram + feed into Claude session
7
+ * cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
7
8
  *
8
9
  * All messages (Telegram incoming, Claude responses) are also written to:
9
10
  * cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
package/dist/notifier.js CHANGED
@@ -4,11 +4,13 @@
4
4
  * Channels:
5
5
  * cca:notify:{namespace} — job completion notifications from cc-agent → forward to Telegram
6
6
  * cca:chat:incoming:{namespace} — messages from the web UI → echo to Telegram + feed into Claude session
7
+ * cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
7
8
  *
8
9
  * All messages (Telegram incoming, Claude responses) are also written to:
9
10
  * cca:chat:log:{namespace} — LPUSH + LTRIM 0 499 (last 500 messages)
10
11
  * cca:chat:outgoing:{namespace} — PUBLISH for web UI to consume
11
12
  */
13
+ import { splitLongMessage } from "./formatter.js";
12
14
  function log(level, ...args) {
13
15
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
14
16
  fn("[notifier]", ...args);
@@ -30,6 +32,11 @@ function shortenModelName(model, driver) {
30
32
  return model.slice(slashIdx + 1);
31
33
  return model;
32
34
  }
35
+ /** Strip ANSI escape sequences from a string before sending to Telegram. */
36
+ function stripAnsi(text) {
37
+ // eslint-disable-next-line no-control-regex
38
+ return text.replace(/\x1B\[[0-9;]*[mGKHF]/g, "");
39
+ }
33
40
  /**
34
41
  * Parse a notification payload and return the display text.
35
42
  * Appends a [driver] or [driver:model] badge whenever the driver field is present.
@@ -124,6 +131,65 @@ export function startNotifier(bot, chatId, namespace, redis, handleUserMessage,
124
131
  log("info", `subscribed to cca:chat:incoming:${namespace}`);
125
132
  }
126
133
  });
134
+ // cca:chat:outgoing:* — meta-agent stdout lines (source=claude) → buffer+debounce → Telegram
135
+ // Using psubscribe so we catch all namespaces (money-brain, isoc-nevada, etc.)
136
+ sub.psubscribe("cca:chat:outgoing:*", (err) => {
137
+ if (err) {
138
+ log("error", "psubscribe cca:chat:outgoing:* failed:", err.message);
139
+ }
140
+ else {
141
+ log("info", "psubscribed to cca:chat:outgoing:*");
142
+ }
143
+ });
144
+ // Per-namespace debounce buffer: accumulate streaming lines, flush after 1.5s silence
145
+ const metaAgentBuffers = new Map();
146
+ function flushMetaAgentBuffer(ns, targetChatId) {
147
+ const buf = metaAgentBuffers.get(ns);
148
+ if (!buf || !buf.text.trim())
149
+ return;
150
+ const text = stripAnsi(buf.text.trim());
151
+ buf.text = "";
152
+ buf.timer = null;
153
+ const chunks = splitLongMessage(text);
154
+ for (const chunk of chunks) {
155
+ bot.sendMessage(targetChatId, chunk).catch((err) => {
156
+ log("warn", `meta-agent flush sendMessage failed (ns=${ns}):`, err.message);
157
+ });
158
+ }
159
+ }
160
+ sub.on("pmessage", (pattern, channel, message) => {
161
+ void pattern; // used only as a type guard
162
+ const ns = channel.replace("cca:chat:outgoing:", "");
163
+ let parsed = null;
164
+ try {
165
+ parsed = JSON.parse(message);
166
+ }
167
+ catch {
168
+ return; // non-JSON line — skip
169
+ }
170
+ // Only forward messages from the meta-agent (source=claude).
171
+ // cc-tg itself publishes to this channel with source "cc-tg"/"telegram"/"ui" — skip those.
172
+ if (parsed.source !== "claude")
173
+ return;
174
+ const content = parsed.content;
175
+ if (!content)
176
+ return;
177
+ const targetChatId = chatId ?? getActiveChatId?.();
178
+ if (targetChatId == null) {
179
+ log("warn", `meta-agent output: no chatId for namespace=${ns}, dropping line`);
180
+ return;
181
+ }
182
+ // Accumulate into per-namespace buffer and (re-)arm debounce timer
183
+ let buf = metaAgentBuffers.get(ns);
184
+ if (!buf) {
185
+ buf = { text: "", timer: null };
186
+ metaAgentBuffers.set(ns, buf);
187
+ }
188
+ buf.text += (buf.text ? "\n" : "") + content;
189
+ if (buf.timer)
190
+ clearTimeout(buf.timer);
191
+ buf.timer = setTimeout(() => flushMetaAgentBuffer(ns, targetChatId), 1500);
192
+ });
127
193
  // Poll the cca:notify:{namespace} LIST every 5 seconds.
128
194
  // Jobs push to this list via RPUSH; pub/sub alone won't deliver those messages.
129
195
  const notifyListKey = `cca:notify:${namespace}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.9.23",
3
+ "version": "0.9.24",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {