@gonzih/cc-discord 0.2.4 → 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 +2 -1
- package/dist/bot.js +9 -10
- package/dist/index.js +1 -1
- package/dist/meta-agent-manager.d.ts +1 -5
- package/dist/meta-agent-manager.js +20 -68
- package/dist/notifier.d.ts +16 -12
- package/dist/notifier.js +33 -58
- package/package.json +1 -1
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,
|
|
42
|
+
startPolling: (wire: Wire, getNamespaces: () => string[]) => void;
|
|
47
43
|
stop: () => void;
|
|
48
44
|
}
|
|
49
45
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 4. spawnSession: claude --continue -p "{message}" pipes stdout → wire.discord.publishOutgoing
|
|
9
9
|
*/
|
|
10
10
|
import { spawn, execSync } from "child_process";
|
|
11
|
-
import { existsSync, mkdirSync,
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { homedir } from "os";
|
|
14
14
|
import { CC_DISCORD_WORKSPACE_ROOT, TIMING, discordMetaInputKey, } from "@gonzih/cc-wire";
|
|
@@ -59,55 +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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
// Merge extra MCP servers from ~/.config/cc-discord-mcp.json (credentials file, not in source)
|
|
78
|
-
const extraMcpPath = join(homedir(), ".config", "cc-discord-mcp.json");
|
|
79
|
-
if (existsSync(extraMcpPath)) {
|
|
80
|
-
try {
|
|
81
|
-
const extra = JSON.parse(readFileSync(extraMcpPath, "utf8"));
|
|
82
|
-
Object.assign(servers, extra);
|
|
83
|
-
console.log(`[meta-agent-manager] merged extra MCP servers from ${extraMcpPath}: ${Object.keys(extra).join(", ")}`);
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
-
console.warn(`[meta-agent-manager] failed to read extra MCP config: ${msg}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
// Also check env vars as fallback for gmail and github
|
|
91
|
-
const gmailEmail = process.env.GMAIL_EMAIL;
|
|
92
|
-
const gmailPass = process.env.GMAIL_APP_PASSWORD;
|
|
93
|
-
if (gmailEmail && gmailPass && !servers["gmail-personal"]) {
|
|
94
|
-
servers["gmail-personal"] = {
|
|
95
|
-
command: "/opt/homebrew/bin/npx",
|
|
96
|
-
args: ["-y", "gmail-mcp-imap"],
|
|
97
|
-
env: { GMAIL_EMAIL: gmailEmail, GMAIL_APP_PASSWORD: gmailPass, npm_config_cache: npmCache },
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const ghToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
101
|
-
if (ghToken && !servers["github"]) {
|
|
102
|
-
servers["github"] = {
|
|
103
|
-
command: "docker",
|
|
104
|
-
args: ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
|
|
105
|
-
env: { GITHUB_PERSONAL_ACCESS_TOKEN: ghToken },
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const config = { mcpServers: servers };
|
|
109
79
|
writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
110
|
-
console.log(`[meta-agent-manager] injected MCP config for namespace=${ns}
|
|
80
|
+
console.log(`[meta-agent-manager] injected MCP config for namespace=${ns}`);
|
|
111
81
|
}
|
|
112
82
|
/**
|
|
113
83
|
* Resolve claude binary — same logic as claude.ts resolveClaude.
|
|
@@ -213,14 +183,14 @@ export function createMetaAgentManager() {
|
|
|
213
183
|
const wsPath = workspacePath(ns);
|
|
214
184
|
injectMcp(ns, wsPath, token);
|
|
215
185
|
},
|
|
216
|
-
startPolling(wire,
|
|
186
|
+
startPolling(wire, getNamespaces) {
|
|
217
187
|
if (pollInterval)
|
|
218
188
|
return; // already running
|
|
219
189
|
pollInterval = setInterval(() => {
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
190
|
+
const namespaces = getNamespaces();
|
|
191
|
+
if (namespaces.length === 0)
|
|
222
192
|
return;
|
|
223
|
-
for (const
|
|
193
|
+
for (const ns of namespaces) {
|
|
224
194
|
if (activeNamespaces.has(ns))
|
|
225
195
|
continue;
|
|
226
196
|
wire.discord.dequeue(ns)
|
|
@@ -258,24 +228,6 @@ export function createMetaAgentManager() {
|
|
|
258
228
|
});
|
|
259
229
|
return;
|
|
260
230
|
}
|
|
261
|
-
// Ensure workspace exists before spawning — clone happens once, no-op on repeat.
|
|
262
|
-
try {
|
|
263
|
-
await ensureWorkspace(ns, repoUrl);
|
|
264
|
-
injectMcp(ns, workspacePath(ns), token);
|
|
265
|
-
}
|
|
266
|
-
catch (wsErr) {
|
|
267
|
-
const msg = wsErr instanceof Error ? wsErr.message : String(wsErr);
|
|
268
|
-
console.error(`[meta-agent-manager] workspace setup failed (ns=${ns}):`, msg);
|
|
269
|
-
activeNamespaces.delete(ns);
|
|
270
|
-
await wire.discord.setStatus(ns, {
|
|
271
|
-
namespace: ns,
|
|
272
|
-
status: "idle",
|
|
273
|
-
isTyping: false,
|
|
274
|
-
turnCount: 0,
|
|
275
|
-
updatedAt: new Date().toISOString(),
|
|
276
|
-
}).catch(() => { });
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
231
|
spawnSession(ns, content, token, wire)
|
|
280
232
|
.catch((err) => {
|
|
281
233
|
console.error(`[meta-agent-manager] session error (ns=${ns}):`, err.message);
|
package/dist/notifier.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
44
|
-
*
|
|
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
|
|
61
|
-
* @param notifyChannelId
|
|
62
|
-
* @param namespace
|
|
63
|
-
* @param redis
|
|
64
|
-
* @param handleUserMessage
|
|
65
|
-
* @param forwardNotification
|
|
66
|
-
* @param getActiveChannelId
|
|
67
|
-
* @param reverseSnowflakeLookup
|
|
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
|
-
*
|
|
108
|
-
*
|
|
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
|
|
122
|
-
* @param notifyChannelId
|
|
123
|
-
* @param namespace
|
|
124
|
-
* @param redis
|
|
125
|
-
* @param handleUserMessage
|
|
126
|
-
* @param forwardNotification
|
|
127
|
-
* @param getActiveChannelId
|
|
128
|
-
* @param reverseSnowflakeLookup
|
|
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);
|
|
156
|
-
const legacyNotifyCh = notifyChannel(ns);
|
|
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}
|
|
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
|
|
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
|