@gonzih/cc-discord 0.2.3 → 0.2.5

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
@@ -64,7 +64,6 @@ export declare class CcDiscordBot {
64
64
  private sendToChannel;
65
65
  /** Send to a channel by ID — used by notifier callbacks. */
66
66
  sendToChannelById(channelId: string, text: string): Promise<void>;
67
- sendAttachmentToChannelById(channelId: string, filePath: string): Promise<void>;
68
67
  private isAllowed;
69
68
  private handleMessage;
70
69
  private handleVoice;
@@ -97,6 +96,8 @@ export declare class CcDiscordBot {
97
96
  private writeChatMessage;
98
97
  /** Returns the last channelId that sent a message. */
99
98
  getLastActiveChannelId(): string | undefined;
99
+ /** Reverse lookup: find the Discord channelId registered for a given namespace. */
100
+ getChannelIdForNamespace(ns: string): string | undefined;
100
101
  /**
101
102
  * Feed a text message into the active Claude session for the given channel.
102
103
  * Called by the notifier when a UI message arrives via Redis pub/sub.
package/dist/bot.js CHANGED
@@ -294,15 +294,6 @@ export class CcDiscordBot {
294
294
  }
295
295
  await this.sendToChannel(channel, text);
296
296
  }
297
- async sendAttachmentToChannelById(channelId, filePath) {
298
- const channel = await this.getChannel(channelId);
299
- if (!channel) {
300
- console.warn(`[bot] sendAttachmentToChannelById: channel ${channelId} not found`);
301
- return;
302
- }
303
- const attachment = new AttachmentBuilder(filePath, { name: basename(filePath) });
304
- await channel.send({ files: [attachment] });
305
- }
306
297
  isAllowed(userId) {
307
298
  if (!this.opts.allowedUserIds?.length)
308
299
  return true;
@@ -1009,6 +1000,14 @@ export class CcDiscordBot {
1009
1000
  getLastActiveChannelId() {
1010
1001
  return this.lastActiveChannelId;
1011
1002
  }
1003
+ /** Reverse lookup: find the Discord channelId registered for a given namespace. */
1004
+ getChannelIdForNamespace(ns) {
1005
+ for (const [channelId, mapping] of this.channelNamespaceMap) {
1006
+ if (mapping.namespace === ns)
1007
+ return channelId;
1008
+ }
1009
+ return undefined;
1010
+ }
1012
1011
  /**
1013
1012
  * Feed a text message into the active Claude session for the given channel.
1014
1013
  * Called by the notifier when a UI message arrives via Redis pub/sub.
@@ -1072,7 +1071,7 @@ export class CcDiscordBot {
1072
1071
  startMetaAgentPolling() {
1073
1072
  if (!this.wire)
1074
1073
  return;
1075
- this.metaAgentManager.startPolling(this.wire, () => Array.from(this.channelNamespaceMap.values()));
1074
+ this.metaAgentManager.startPolling(this.wire, () => Array.from(this.channelNamespaceMap.values()).map((v) => v.namespace));
1076
1075
  }
1077
1076
  stop() {
1078
1077
  for (const [key, session] of this.sessions) {
package/dist/index.js CHANGED
@@ -142,7 +142,7 @@ const bot = new CcDiscordBot({
142
142
  namespace,
143
143
  registerRoutedChannelId: (ns, channelId) => notifier.registerRoutedChannelId(ns, channelId),
144
144
  });
145
- const notifier = startNotifier(bot, notifyChannelId, namespace, sharedRedis, (channelId, text) => handleUserMessageFn?.(channelId, text), (channelId, text) => forwardNotificationFn?.(channelId, text), () => getLastActiveChannelIdFn(), (n) => bot.reverseSnowflakeLookup(n));
145
+ const notifier = startNotifier(bot, notifyChannelId, namespace, sharedRedis, (channelId, text) => handleUserMessageFn?.(channelId, text), (channelId, text) => forwardNotificationFn?.(channelId, text), () => getLastActiveChannelIdFn(), (n) => bot.reverseSnowflakeLookup(n), (ns) => bot.getChannelIdForNamespace(ns));
146
146
  console.log(`[notifier] started for namespace=${namespace} notifyChannelId=${notifyChannelId ?? "dynamic"}`);
147
147
  // Restore persisted channel→namespace mappings so routing survives restarts
148
148
  bot.loadChannelMappings().catch((err) => {
@@ -36,14 +36,10 @@ export declare function injectMcp(ns: string, wsPath: string, token: string): vo
36
36
  * Returns a Promise that resolves when the process exits.
37
37
  */
38
38
  export declare function spawnSession(ns: string, message: string, token: string, wire: Wire): Promise<void>;
39
- export interface NamespaceEntry {
40
- namespace: string;
41
- repoUrl: string;
42
- }
43
39
  export interface MetaAgentManager {
44
40
  ensureWorkspace: (ns: string, repoUrl: string) => Promise<void>;
45
41
  injectMcp: (ns: string, token: string) => void;
46
- startPolling: (wire: Wire, getNamespaceEntries: () => NamespaceEntry[]) => void;
42
+ startPolling: (wire: Wire, getNamespaces: () => string[]) => void;
47
43
  stop: () => void;
48
44
  }
49
45
  /**
@@ -59,43 +59,25 @@ export function injectMcp(ns, wsPath, token) {
59
59
  const npmCache = process.env.npm_config_cache ?? `${homedir()}/.npm`;
60
60
  const trustedOwners = process.env.CC_AGENT_TRUSTED_OWNERS ?? "";
61
61
  const systemPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
62
- const servers = {
63
- "cc-agent": {
64
- command: "/opt/homebrew/bin/npx",
65
- args: ["-y", "--prefer-online", "@gonzih/cc-agent"],
66
- env: {
67
- CC_AGENT_NAMESPACE: ns,
68
- CWD: wsPath,
69
- CLAUDE_CODE_OAUTH_TOKEN: token,
70
- CLAUDE_TOKENS: token,
71
- CC_AGENT_TRUSTED_OWNERS: trustedOwners,
72
- PATH: systemPath,
73
- npm_config_cache: npmCache,
62
+ const config = {
63
+ mcpServers: {
64
+ "cc-agent": {
65
+ command: "/opt/homebrew/bin/npx",
66
+ args: ["-y", "--prefer-online", "@gonzih/cc-agent"],
67
+ env: {
68
+ CC_AGENT_NAMESPACE: ns,
69
+ CWD: wsPath,
70
+ CLAUDE_CODE_OAUTH_TOKEN: token,
71
+ CLAUDE_TOKENS: token,
72
+ CC_AGENT_TRUSTED_OWNERS: trustedOwners,
73
+ PATH: systemPath,
74
+ npm_config_cache: npmCache,
75
+ },
74
76
  },
75
77
  },
76
78
  };
77
- // gmail MCP — enabled when GMAIL_EMAIL + GMAIL_APP_PASSWORD are in env
78
- const gmailEmail = process.env.GMAIL_EMAIL;
79
- const gmailPass = process.env.GMAIL_APP_PASSWORD;
80
- if (gmailEmail && gmailPass) {
81
- servers["gmail-personal"] = {
82
- command: "/opt/homebrew/bin/npx",
83
- args: ["-y", "gmail-mcp-imap"],
84
- env: { GMAIL_EMAIL: gmailEmail, GMAIL_APP_PASSWORD: gmailPass, npm_config_cache: npmCache },
85
- };
86
- }
87
- // github MCP — enabled when GITHUB_PERSONAL_ACCESS_TOKEN is in env
88
- const ghToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
89
- if (ghToken) {
90
- servers["github"] = {
91
- command: "docker",
92
- args: ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
93
- env: { GITHUB_PERSONAL_ACCESS_TOKEN: ghToken },
94
- };
95
- }
96
- const config = { mcpServers: servers };
97
79
  writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf8");
98
- console.log(`[meta-agent-manager] injected MCP config for namespace=${ns} (servers: ${Object.keys(servers).join(", ")})`);
80
+ console.log(`[meta-agent-manager] injected MCP config for namespace=${ns}`);
99
81
  }
100
82
  /**
101
83
  * Resolve claude binary — same logic as claude.ts resolveClaude.
@@ -201,14 +183,14 @@ export function createMetaAgentManager() {
201
183
  const wsPath = workspacePath(ns);
202
184
  injectMcp(ns, wsPath, token);
203
185
  },
204
- startPolling(wire, getNamespaceEntries) {
186
+ startPolling(wire, getNamespaces) {
205
187
  if (pollInterval)
206
188
  return; // already running
207
189
  pollInterval = setInterval(() => {
208
- const entries = getNamespaceEntries();
209
- if (entries.length === 0)
190
+ const namespaces = getNamespaces();
191
+ if (namespaces.length === 0)
210
192
  return;
211
- for (const { namespace: ns, repoUrl } of entries) {
193
+ for (const ns of namespaces) {
212
194
  if (activeNamespaces.has(ns))
213
195
  continue;
214
196
  wire.discord.dequeue(ns)
@@ -246,24 +228,6 @@ export function createMetaAgentManager() {
246
228
  });
247
229
  return;
248
230
  }
249
- // Ensure workspace exists before spawning — clone happens once, no-op on repeat.
250
- try {
251
- await ensureWorkspace(ns, repoUrl);
252
- injectMcp(ns, workspacePath(ns), token);
253
- }
254
- catch (wsErr) {
255
- const msg = wsErr instanceof Error ? wsErr.message : String(wsErr);
256
- console.error(`[meta-agent-manager] workspace setup failed (ns=${ns}):`, msg);
257
- activeNamespaces.delete(ns);
258
- await wire.discord.setStatus(ns, {
259
- namespace: ns,
260
- status: "idle",
261
- isTyping: false,
262
- turnCount: 0,
263
- updatedAt: new Date().toISOString(),
264
- }).catch(() => { });
265
- return;
266
- }
267
231
  spawnSession(ns, content, token, wire)
268
232
  .catch((err) => {
269
233
  console.error(`[meta-agent-manager] session error (ns=${ns}):`, err.message);
@@ -40,10 +40,13 @@ export declare function parseNotification(raw: string): ParsedNotification | nul
40
40
  export declare function writeChatLog(redis: Redis, namespace: string, msg: ChatMessage): void;
41
41
  /**
42
42
  * Resolve the target Discord channelId for a notification.
43
- * When chatId is set and a reverse-lookup function is available, prefer the originating channel.
44
- * Falls back to notifyChannelId, then getActiveChannelId.
43
+ * Priority:
44
+ * 1. chatId reverseSnowflakeLookup (originating channel from the notification payload)
45
+ * 2. ns → getChannelIdForNamespace (registered Discord channel for this namespace)
46
+ * 3. notifyChannelId (static env var — may be stale/dead)
47
+ * 4. getActiveChannelId (last channel that sent a message)
45
48
  */
46
- export declare function resolveNotifyChannel(chatId: number | undefined, notifyChannelId: string | null, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined): string | undefined;
49
+ export declare function resolveNotifyChannel(chatId: number | undefined, notifyChannelId: string | null, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined, ns?: string, getChannelIdForNamespace?: (ns: string) => string | undefined): string | undefined;
47
50
  export interface NotifierHandle {
48
51
  /**
49
52
  * Register the originating Discord channel ID for a routed namespace.
@@ -57,13 +60,14 @@ export interface NotifierHandle {
57
60
  /**
58
61
  * Start the Discord notifier.
59
62
  *
60
- * @param bot - CcDiscordBot instance (for sending messages)
61
- * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
62
- * @param namespace - primary namespace (used to build Redis channel names)
63
- * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
64
- * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
65
- * @param forwardNotification - Optional callback to forward job notifications
66
- * @param getActiveChannelId - Optional callback to resolve channelId dynamically
67
- * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
63
+ * @param bot - CcDiscordBot instance (for sending messages)
64
+ * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
65
+ * @param namespace - primary namespace (used to build Redis channel names)
66
+ * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
67
+ * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
68
+ * @param forwardNotification - Optional callback to forward job notifications
69
+ * @param getActiveChannelId - Optional callback to resolve channelId dynamically
70
+ * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
71
+ * @param getChannelIdForNamespace - Optional callback to resolve a namespace to its registered Discord channelId
68
72
  */
69
- export declare function startNotifier(bot: CcDiscordBot, notifyChannelId: string | null, namespace: string, redis: Redis, handleUserMessage?: (channelId: string, text: string) => void, forwardNotification?: (channelId: string, text: string) => void, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined): NotifierHandle;
73
+ export declare function startNotifier(bot: CcDiscordBot, notifyChannelId: string | null, namespace: string, redis: Redis, handleUserMessage?: (channelId: string, text: string) => void, forwardNotification?: (channelId: string, text: string) => void, getActiveChannelId?: () => string | undefined, reverseSnowflakeLookup?: (n: number) => string | undefined, getChannelIdForNamespace?: (ns: string) => string | undefined): NotifierHandle;
package/dist/notifier.js CHANGED
@@ -11,22 +11,7 @@
11
11
  * cca:discord:chat:outgoing:{ns} — PUBLISH for web UI to consume
12
12
  */
13
13
  import { discordChatLog, discordChatOutgoing, discordNotify, notifyChannel, chatIncomingChannel, createCcWire, TIMING, } from "@gonzih/cc-wire";
14
- import { existsSync, statSync } from "fs";
15
14
  import { splitLongMessage, stripAnsi } from "./formatter.js";
16
- const SAFE_DIRS = ["/tmp/", "/var/folders/"];
17
- const MAX_DISCORD_BYTES = 25 * 1024 * 1024; // 25 MB Discord file limit
18
- /** Extract /tmp/ or /var/folders/ file paths mentioned in text that actually exist on disk. */
19
- function extractAttachablePaths(text) {
20
- const pattern = /(?:^|[\s`'"(])(\/(?:tmp|var\/folders)\/[\w.\-/]+\.[\w]{1,10})(?:[\s`'")\n]|$)/gm;
21
- const quoted = /"(\/(?:tmp|var\/folders)\/[^"]+\.[a-zA-Z0-9]{1,10})"|'(\/(?:tmp|var\/folders)\/[^']+\.[a-zA-Z0-9]{1,10})'/g;
22
- const candidates = new Set();
23
- let m;
24
- while ((m = pattern.exec(text)) !== null)
25
- candidates.add(m[1]);
26
- while ((m = quoted.exec(text)) !== null)
27
- candidates.add(m[1] ?? m[2]);
28
- return [...candidates].filter(p => SAFE_DIRS.some(d => p.startsWith(d)) && existsSync(p));
29
- }
30
15
  function log(level, ...args) {
31
16
  const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
32
17
  fn("[notifier]", ...args);
@@ -104,30 +89,39 @@ export function writeChatLog(redis, namespace, msg) {
104
89
  }
105
90
  /**
106
91
  * Resolve the target Discord channelId for a notification.
107
- * When chatId is set and a reverse-lookup function is available, prefer the originating channel.
108
- * Falls back to notifyChannelId, then getActiveChannelId.
92
+ * Priority:
93
+ * 1. chatId reverseSnowflakeLookup (originating channel from the notification payload)
94
+ * 2. ns → getChannelIdForNamespace (registered Discord channel for this namespace)
95
+ * 3. notifyChannelId (static env var — may be stale/dead)
96
+ * 4. getActiveChannelId (last channel that sent a message)
109
97
  */
110
- export function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) {
98
+ export function resolveNotifyChannel(chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup, ns, getChannelIdForNamespace) {
111
99
  if (chatId != null && reverseSnowflakeLookup) {
112
100
  const resolved = reverseSnowflakeLookup(chatId);
113
101
  if (resolved)
114
102
  return resolved;
115
103
  }
104
+ if (ns && getChannelIdForNamespace) {
105
+ const resolved = getChannelIdForNamespace(ns);
106
+ if (resolved)
107
+ return resolved;
108
+ }
116
109
  return notifyChannelId ?? getActiveChannelId?.();
117
110
  }
118
111
  /**
119
112
  * Start the Discord notifier.
120
113
  *
121
- * @param bot - CcDiscordBot instance (for sending messages)
122
- * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
123
- * @param namespace - primary namespace (used to build Redis channel names)
124
- * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
125
- * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
126
- * @param forwardNotification - Optional callback to forward job notifications
127
- * @param getActiveChannelId - Optional callback to resolve channelId dynamically
128
- * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
114
+ * @param bot - CcDiscordBot instance (for sending messages)
115
+ * @param notifyChannelId - Discord channel ID to forward notifications to. Pass null to use getActiveChannelId.
116
+ * @param namespace - primary namespace (used to build Redis channel names)
117
+ * @param redis - ioredis client in normal mode (will be duplicated for pub/sub)
118
+ * @param handleUserMessage - Optional callback to feed UI messages into the active Claude session
119
+ * @param forwardNotification - Optional callback to forward job notifications
120
+ * @param getActiveChannelId - Optional callback to resolve channelId dynamically
121
+ * @param reverseSnowflakeLookup - Optional callback to resolve a chatId integer to a Discord channelId
122
+ * @param getChannelIdForNamespace - Optional callback to resolve a namespace to its registered Discord channelId
129
123
  */
130
- export function startNotifier(bot, notifyChannelId, namespace, redis, handleUserMessage, forwardNotification, getActiveChannelId, reverseSnowflakeLookup) {
124
+ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUserMessage, forwardNotification, getActiveChannelId, reverseSnowflakeLookup, getChannelIdForNamespace) {
131
125
  const wire = createCcWire(redis);
132
126
  // Per-namespace channelId registry — maps routed namespace → Discord channelId
133
127
  const routedChannelIds = new Map();
@@ -152,8 +146,8 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
152
146
  if (subscribedNamespaces.has(ns))
153
147
  return;
154
148
  subscribedNamespaces.add(ns);
155
- const notifyCh = discordNotify(ns); // cca:discord:notify:{ns} — new cc-wire path
156
- const legacyNotifyCh = notifyChannel(ns); // cca:notify:{ns} — cc-agent still publishes here
149
+ const notifyCh = discordNotify(ns);
150
+ const legacyNotifyCh = notifyChannel(ns);
157
151
  const incomingCh = chatIncomingChannel(ns);
158
152
  channelToNamespace.set(notifyCh, ns);
159
153
  channelToNamespace.set(legacyNotifyCh, ns);
@@ -166,13 +160,12 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
166
160
  log("info", `subscribed to ${notifyCh}`);
167
161
  }
168
162
  });
169
- // Also subscribe to legacy cc-agent notification channel
170
163
  sub.subscribe(legacyNotifyCh, (err) => {
171
164
  if (err) {
172
165
  log("error", `subscribe ${legacyNotifyCh} failed:`, err.message);
173
166
  }
174
167
  else {
175
- log("info", `subscribed to ${legacyNotifyCh} (legacy cc-agent)`);
168
+ log("info", `subscribed to ${legacyNotifyCh}`);
176
169
  }
177
170
  });
178
171
  sub.subscribe(incomingCh, (err) => {
@@ -208,8 +201,7 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
208
201
  const buf = metaAgentBuffers.get(ns);
209
202
  if (!buf || !buf.text.trim())
210
203
  return;
211
- const rawText = stripAnsi(buf.text.trim());
212
- const text = `← [${ns}] ` + rawText;
204
+ const text = `← [${ns}] ` + stripAnsi(buf.text.trim());
213
205
  buf.text = "";
214
206
  buf.timer = null;
215
207
  const chunks = splitLongMessage(text);
@@ -218,25 +210,6 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
218
210
  log("warn", `meta-agent flush sendToChannelById failed (ns=${ns}):`, err.message);
219
211
  });
220
212
  }
221
- // Attach any /tmp/ files mentioned in the response
222
- const paths = extractAttachablePaths(rawText);
223
- for (const filePath of paths) {
224
- let size;
225
- try {
226
- size = statSync(filePath).size;
227
- }
228
- catch {
229
- continue;
230
- }
231
- if (size > MAX_DISCORD_BYTES) {
232
- bot.sendToChannelById(targetChannelId, `File too large for Discord (${(size / 1024 / 1024).toFixed(1)} MB): ${filePath}`).catch(() => { });
233
- continue;
234
- }
235
- log("info", `attaching file to Discord (ns=${ns}): ${filePath}`);
236
- bot.sendAttachmentToChannelById(targetChannelId, filePath).catch((err) => {
237
- log("warn", `attachment send failed (ns=${ns}, path=${filePath}):`, err.message);
238
- });
239
- }
240
213
  }
241
214
  sub.on("pmessage", (pattern, channel, message) => {
242
215
  void pattern;
@@ -303,10 +276,11 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
303
276
  const notification = parseNotification(raw);
304
277
  if (notification === null)
305
278
  continue; // routing excludes discord
306
- // Primary namespace: honour chatId-based per-channel routing via reverseSnowflakeLookup.
279
+ // Primary namespace: honour chatId-based per-channel routing via reverseSnowflakeLookup,
280
+ // then namespace → channelId lookup, then notifyChannelId / active channel.
307
281
  // Routed namespaces: always deliver to the registered Discord channelId — no leakage.
308
282
  const destChannelId = ns === namespace
309
- ? (resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup) ?? targetChannelId)
283
+ ? (resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup, ns, getChannelIdForNamespace) ?? targetChannelId)
310
284
  : targetChannelId;
311
285
  bot.sendToChannelById(destChannelId, notification.text).catch((err) => {
312
286
  log("warn", `notify list send failed (ns=${ns}):`, err.message);
@@ -322,8 +296,8 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
322
296
  }
323
297
  };
324
298
  const pollNotifyList = async () => {
325
- // Primary namespace
326
- const primaryTargetId = notifyChannelId ?? getActiveChannelId?.();
299
+ // Primary namespace: prefer registered channel for this namespace, then env var, then active channel
300
+ const primaryTargetId = getChannelIdForNamespace?.(namespace) ?? notifyChannelId ?? getActiveChannelId?.();
327
301
  if (primaryTargetId != null) {
328
302
  await pollOneNamespace(namespace, primaryTargetId);
329
303
  }
@@ -344,14 +318,15 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
344
318
  return;
345
319
  const isPrimary = ns === namespace;
346
320
  const notifyCh = discordNotify(ns);
321
+ const legacyNotifyCh = notifyChannel(ns);
347
322
  const incomingCh = chatIncomingChannel(ns);
348
- if (channel === notifyCh) {
323
+ if (channel === notifyCh || channel === legacyNotifyCh) {
349
324
  const notification = parseNotification(message);
350
325
  if (notification === null)
351
326
  return; // routing excludes discord
352
327
  let targetId;
353
328
  if (isPrimary) {
354
- targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup);
329
+ targetId = resolveNotifyChannel(notification.chatId, notifyChannelId, getActiveChannelId, reverseSnowflakeLookup, ns, getChannelIdForNamespace);
355
330
  }
356
331
  else {
357
332
  // For routed namespaces, only use the registered channelId — no fallback to primary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-discord",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Claude Code Discord bot — chat with Claude Code via Discord",
5
5
  "type": "module",
6
6
  "bin": {