@gonzih/cc-discord 0.2.47 → 0.2.49
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 +20 -11
- package/dist/bot.js +328 -74
- package/dist/index.d.ts +9 -5
- package/dist/index.js +17 -10
- package/dist/meta-agent-manager.d.ts +2 -2
- package/dist/meta-agent-manager.js +567 -81
- package/dist/notifier.d.ts +2 -2
- package/dist/notifier.js +80 -44
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +1 -1
- package/package.json +8 -5
|
@@ -18,6 +18,11 @@ import { join } from "path";
|
|
|
18
18
|
import { homedir } from "os";
|
|
19
19
|
import { CC_DISCORD_WORKSPACE_ROOT, TIMING, DISCORD_INSTANCE_KEY, discordMetaInputKey, discordChatOutgoing, metaStreamChannel as ccWireMetaStreamChannel, metaLogKey as ccWireMetaLogKey, } from "@gonzih/cc-wire";
|
|
20
20
|
const WORKSPACE_ROOT = join(homedir(), CC_DISCORD_WORKSPACE_ROOT);
|
|
21
|
+
const CHAT_SOURCE = "claude";
|
|
22
|
+
function agentDriver() {
|
|
23
|
+
const raw = (process.env.CC_DISCORD_AGENT_DRIVER ?? process.env.AGENT_DRIVER ?? "claude").toLowerCase();
|
|
24
|
+
return raw === "codex" ? "codex" : "claude";
|
|
25
|
+
}
|
|
21
26
|
/**
|
|
22
27
|
* Returns the path to the workspace for the given namespace.
|
|
23
28
|
* Creates the parent root directory if needed.
|
|
@@ -52,13 +57,18 @@ export async function ensureWorkspace(ns, repoUrl) {
|
|
|
52
57
|
*
|
|
53
58
|
* Template priority:
|
|
54
59
|
* 1. CC_DISCORD_MCP_JSON env var (full JSON string) — operator-supplied override
|
|
55
|
-
* 2. Built-in template:
|
|
60
|
+
* 2. Built-in template: gitkb MCP plus ATC shell environment
|
|
56
61
|
*
|
|
57
62
|
* Variables substituted in the template: {namespace}, {workspacePath}, {token},
|
|
58
63
|
* {npmCache}, {trustedOwners}, {path}.
|
|
59
64
|
*/
|
|
60
65
|
export function injectMcp(ns, wsPath, token) {
|
|
61
66
|
const mcpPath = join(wsPath, ".mcp.json");
|
|
67
|
+
const codexDir = join(wsPath, ".codex");
|
|
68
|
+
const codexConfigPath = join(codexDir, "config.toml");
|
|
69
|
+
const agentsPath = join(wsPath, "AGENTS.md");
|
|
70
|
+
const atcDir = join(wsPath, ".atc");
|
|
71
|
+
const atcConfigPath = join(atcDir, "config.toml");
|
|
62
72
|
if (process.env.CC_DISCORD_MCP_JSON) {
|
|
63
73
|
const rendered = process.env.CC_DISCORD_MCP_JSON
|
|
64
74
|
.replace(/\{namespace\}/g, ns)
|
|
@@ -66,34 +76,75 @@ export function injectMcp(ns, wsPath, token) {
|
|
|
66
76
|
.replace(/\{token\}/g, token);
|
|
67
77
|
writeFileSync(mcpPath, rendered, "utf8");
|
|
68
78
|
console.log(`[meta-agent-manager] injected MCP config (from CC_DISCORD_MCP_JSON) for ${ns}`);
|
|
69
|
-
return;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
CLAUDE_TOKENS: token,
|
|
88
|
-
CC_AGENT_TRUSTED_OWNERS: trustedOwners,
|
|
89
|
-
PATH: systemPath,
|
|
90
|
-
npm_config_cache: npmCache,
|
|
80
|
+
else {
|
|
81
|
+
const npmCache = process.env.npm_config_cache ?? `${homedir()}/.npm`;
|
|
82
|
+
const systemPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
83
|
+
const config = {
|
|
84
|
+
mcpServers: {
|
|
85
|
+
"gitkb": {
|
|
86
|
+
command: "/opt/homebrew/bin/git-kb",
|
|
87
|
+
args: ["mcp"],
|
|
88
|
+
env: {
|
|
89
|
+
GITKB_ROOT: wsPath,
|
|
90
|
+
ATC_ROOT: process.env.ATC_ROOT ?? `${homedir()}/.local/share/atc`,
|
|
91
|
+
DISPATCH_KB_ROOT: wsPath,
|
|
92
|
+
DISPATCH_META_ROOT: process.env.DISPATCH_META_ROOT ?? wsPath,
|
|
93
|
+
PATH: systemPath,
|
|
94
|
+
npm_config_cache: npmCache,
|
|
95
|
+
},
|
|
91
96
|
},
|
|
92
97
|
},
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
};
|
|
99
|
+
writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf8");
|
|
100
|
+
console.log(`[meta-agent-manager] injected MCP config for namespace=${ns}`);
|
|
101
|
+
}
|
|
102
|
+
const npmCache = process.env.npm_config_cache ?? `${homedir()}/.npm`;
|
|
103
|
+
const systemPath = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
104
|
+
mkdirSync(codexDir, { recursive: true });
|
|
105
|
+
mkdirSync(atcDir, { recursive: true });
|
|
106
|
+
writeFileSync(codexConfigPath, [
|
|
107
|
+
"[mcp_servers.gitkb]",
|
|
108
|
+
'command = "/opt/homebrew/bin/git-kb"',
|
|
109
|
+
'args = ["mcp"]',
|
|
110
|
+
"",
|
|
111
|
+
"[mcp_servers.gitkb.env]",
|
|
112
|
+
`GITKB_ROOT = ${JSON.stringify(wsPath)}`,
|
|
113
|
+
`ATC_ROOT = ${JSON.stringify(process.env.ATC_ROOT ?? `${homedir()}/.local/share/atc`)}`,
|
|
114
|
+
`DISPATCH_KB_ROOT = ${JSON.stringify(wsPath)}`,
|
|
115
|
+
`DISPATCH_META_ROOT = ${JSON.stringify(process.env.DISPATCH_META_ROOT ?? wsPath)}`,
|
|
116
|
+
`PATH = ${JSON.stringify(systemPath)}`,
|
|
117
|
+
`npm_config_cache = ${JSON.stringify(npmCache)}`,
|
|
118
|
+
"",
|
|
119
|
+
].join("\n"), "utf8");
|
|
120
|
+
writeFileSync(atcConfigPath, [
|
|
121
|
+
"[dispatch]",
|
|
122
|
+
"sandbox = false",
|
|
123
|
+
"project_env = true",
|
|
124
|
+
`max_turns = ${Number(process.env.ATC_MAX_TURNS ?? 10000)}`,
|
|
125
|
+
`max_budget_usd = ${Number(process.env.ATC_MAX_BUDGET_USD ?? 250)}`,
|
|
126
|
+
`max_retries = ${Number(process.env.ATC_MAX_RETRIES ?? 1)}`,
|
|
127
|
+
"",
|
|
128
|
+
].join("\n"), "utf8");
|
|
129
|
+
if (!existsSync(agentsPath)) {
|
|
130
|
+
writeFileSync(agentsPath, [
|
|
131
|
+
`# ${ns} Agent Workspace`,
|
|
132
|
+
"",
|
|
133
|
+
"- This workspace is managed by cc-discord.",
|
|
134
|
+
"- Use gitkb for project memory.",
|
|
135
|
+
"- Use ATC for spawned or delegated agent work; ATC is a private local/cloud system and must not be vendored, published, or documented as public API.",
|
|
136
|
+
"- ATC command patterns:",
|
|
137
|
+
" - Implement a task: `GITKB_ROOT=$PWD atc run task <slug>`",
|
|
138
|
+
" - Research a task: `GITKB_ROOT=$PWD atc run research task <slug>`",
|
|
139
|
+
" - Review a PR: `GITKB_ROOT=$PWD atc run pr-review --param pr=<url>`",
|
|
140
|
+
" - Monitor work: `atc status --flat`, `atc logs <slug-or-id>`, `atc watch --id <dispatch-id>`",
|
|
141
|
+
"- Dry-run before dispatch: run `atc run <args> --dry-run`, inspect resolver/directive/worktree/repo, adjust the command, and dry-run again until the output matches intent.",
|
|
142
|
+
"- Never dispatch when the dry-run resolves as raw `prompt` unless raw-prompt dispatch is explicitly intended.",
|
|
143
|
+
"- Keep changes scoped to the registered repository for this Discord namespace.",
|
|
144
|
+
"",
|
|
145
|
+
].join("\n"), "utf8");
|
|
146
|
+
}
|
|
147
|
+
console.log(`[meta-agent-manager] injected Codex config for namespace=${ns}`);
|
|
97
148
|
}
|
|
98
149
|
/**
|
|
99
150
|
* Resolve claude binary — same logic as claude.ts resolveClaude.
|
|
@@ -121,6 +172,27 @@ function resolveClaude() {
|
|
|
121
172
|
}
|
|
122
173
|
return "claude";
|
|
123
174
|
}
|
|
175
|
+
function resolveCodex() {
|
|
176
|
+
if (process.env.CODEX_BIN)
|
|
177
|
+
return process.env.CODEX_BIN;
|
|
178
|
+
const dirs = (process.env.PATH ?? "").split(":");
|
|
179
|
+
for (const dir of dirs) {
|
|
180
|
+
const c = `${dir}/codex`;
|
|
181
|
+
if (existsSync(c))
|
|
182
|
+
return c;
|
|
183
|
+
}
|
|
184
|
+
const fallbacks = [
|
|
185
|
+
`${homedir()}/.npm-global/bin/codex`,
|
|
186
|
+
"/opt/homebrew/bin/codex",
|
|
187
|
+
"/usr/local/bin/codex",
|
|
188
|
+
"/usr/bin/codex",
|
|
189
|
+
];
|
|
190
|
+
for (const p of fallbacks) {
|
|
191
|
+
if (existsSync(p))
|
|
192
|
+
return p;
|
|
193
|
+
}
|
|
194
|
+
return "codex";
|
|
195
|
+
}
|
|
124
196
|
/**
|
|
125
197
|
* Redis keys for meta-agent stdout streaming.
|
|
126
198
|
* cca:meta:{ns}:stream — pub/sub channel (live streaming)
|
|
@@ -134,6 +206,8 @@ export const metaLogKey = ccWireMetaLogKey;
|
|
|
134
206
|
*/
|
|
135
207
|
function buildEnv(token) {
|
|
136
208
|
const env = { ...process.env };
|
|
209
|
+
if (!token)
|
|
210
|
+
return env;
|
|
137
211
|
if (token.startsWith("sk-ant-api")) {
|
|
138
212
|
env.ANTHROPIC_API_KEY = token;
|
|
139
213
|
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
@@ -144,6 +218,276 @@ function buildEnv(token) {
|
|
|
144
218
|
}
|
|
145
219
|
return env;
|
|
146
220
|
}
|
|
221
|
+
function codexArgs(message, sessionId) {
|
|
222
|
+
const extra = (process.env.CC_DISCORD_CODEX_ARGS ?? "--dangerously-bypass-approvals-and-sandbox")
|
|
223
|
+
.split(/\s+/)
|
|
224
|
+
.map((s) => s.trim())
|
|
225
|
+
.filter(Boolean);
|
|
226
|
+
if (sessionId) {
|
|
227
|
+
return ["exec", "resume", "--json", "--skip-git-repo-check", ...extra, sessionId, message];
|
|
228
|
+
}
|
|
229
|
+
return ["exec", "--json", "--skip-git-repo-check", ...extra, message];
|
|
230
|
+
}
|
|
231
|
+
function codexAppServerArgs() {
|
|
232
|
+
const extra = (process.env.CC_DISCORD_CODEX_SERVER_ARGS ?? "")
|
|
233
|
+
.split(/\s+/)
|
|
234
|
+
.map((s) => s.trim())
|
|
235
|
+
.filter(Boolean);
|
|
236
|
+
return ["app-server", "--stdio", ...extra];
|
|
237
|
+
}
|
|
238
|
+
function codexThreadParams(wsPath) {
|
|
239
|
+
return {
|
|
240
|
+
cwd: wsPath,
|
|
241
|
+
approvalPolicy: process.env.CC_DISCORD_CODEX_APPROVAL_POLICY ?? "never",
|
|
242
|
+
sandbox: process.env.CC_DISCORD_CODEX_SANDBOX ?? "danger-full-access",
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function codexTurnParams(threadId, message, wsPath) {
|
|
246
|
+
return {
|
|
247
|
+
threadId,
|
|
248
|
+
cwd: wsPath,
|
|
249
|
+
approvalPolicy: process.env.CC_DISCORD_CODEX_APPROVAL_POLICY ?? "never",
|
|
250
|
+
sandboxPolicy: { type: "dangerFullAccess" },
|
|
251
|
+
input: [{ type: "text", text: message, text_elements: [] }],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function publishDiscordEvent(wire, ns, driver, event, content = "") {
|
|
255
|
+
wire._redis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
256
|
+
id: crypto.randomUUID(),
|
|
257
|
+
source: driver,
|
|
258
|
+
role: "assistant",
|
|
259
|
+
content,
|
|
260
|
+
event,
|
|
261
|
+
timestamp: new Date().toISOString(),
|
|
262
|
+
chatId: 0,
|
|
263
|
+
})).catch(() => { });
|
|
264
|
+
}
|
|
265
|
+
function publishAssistantText(wire, ns, driver, content, logLabel = "publishOutgoing") {
|
|
266
|
+
const msg = {
|
|
267
|
+
id: crypto.randomUUID(),
|
|
268
|
+
source: driver,
|
|
269
|
+
role: "assistant",
|
|
270
|
+
content,
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
chatId: 0,
|
|
273
|
+
};
|
|
274
|
+
if (driver === "codex") {
|
|
275
|
+
wire._redis.publish(discordChatOutgoing(ns), JSON.stringify(msg)).catch((err) => {
|
|
276
|
+
console.warn(`[meta-agent-manager] ${logLabel} failed (ns=${ns}):`, err.message);
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
wire.discord.publishOutgoing(ns, { ...msg, source: CHAT_SOURCE }).catch((err) => {
|
|
281
|
+
console.warn(`[meta-agent-manager] ${logLabel} failed (ns=${ns}):`, err.message);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function extractCodexItemText(item) {
|
|
285
|
+
if (!item)
|
|
286
|
+
return "";
|
|
287
|
+
if (typeof item.text === "string")
|
|
288
|
+
return item.text;
|
|
289
|
+
const content = item.content;
|
|
290
|
+
if (!Array.isArray(content))
|
|
291
|
+
return "";
|
|
292
|
+
return content.map((part) => {
|
|
293
|
+
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
294
|
+
return part.text;
|
|
295
|
+
}
|
|
296
|
+
return "";
|
|
297
|
+
}).join("");
|
|
298
|
+
}
|
|
299
|
+
const codexDeltaItemIds = new Set();
|
|
300
|
+
function forwardCodexAppServerMessage(ns, wire, msg) {
|
|
301
|
+
const eventJson = JSON.stringify(msg);
|
|
302
|
+
const rawRedis = wire._redis;
|
|
303
|
+
const streamCh = metaStreamChannel(ns);
|
|
304
|
+
const logKey = metaLogKey(ns);
|
|
305
|
+
rawRedis.publish(streamCh, eventJson).catch((err) => {
|
|
306
|
+
console.warn(`[meta-agent-manager] codex stream publish failed (ns=${ns}):`, err.message);
|
|
307
|
+
});
|
|
308
|
+
rawRedis.lpush(logKey, eventJson).then(() => {
|
|
309
|
+
rawRedis.ltrim(logKey, 0, 1999).catch(() => { });
|
|
310
|
+
}).catch((err) => {
|
|
311
|
+
console.warn(`[meta-agent-manager] codex log lpush failed (ns=${ns}):`, err.message);
|
|
312
|
+
});
|
|
313
|
+
const method = typeof msg.method === "string" ? msg.method : "";
|
|
314
|
+
const params = (msg.params && typeof msg.params === "object") ? msg.params : {};
|
|
315
|
+
if (method === "item/agentMessage/delta" && typeof params.delta === "string" && params.delta) {
|
|
316
|
+
if (typeof params.itemId === "string")
|
|
317
|
+
codexDeltaItemIds.add(`${ns}:${params.itemId}`);
|
|
318
|
+
publishAssistantText(wire, ns, "codex", params.delta, "codex delta publishOutgoing");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (method === "item/started") {
|
|
322
|
+
const item = params.item;
|
|
323
|
+
const itemType = typeof item?.type === "string" ? item.type : "";
|
|
324
|
+
if (itemType === "commandExecution" || itemType === "mcpToolCall" || itemType === "dynamicToolCall") {
|
|
325
|
+
const label = typeof item?.command === "string"
|
|
326
|
+
? item.command
|
|
327
|
+
: typeof item?.tool === "string"
|
|
328
|
+
? item.tool
|
|
329
|
+
: itemType;
|
|
330
|
+
publishDiscordEvent(wire, ns, "codex", "tool_start", label);
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (method === "item/completed") {
|
|
335
|
+
const item = params.item;
|
|
336
|
+
const itemType = typeof item?.type === "string" ? item.type : "";
|
|
337
|
+
if (itemType === "agentMessage") {
|
|
338
|
+
const itemId = typeof item?.id === "string" ? item.id : "";
|
|
339
|
+
if (itemId && codexDeltaItemIds.has(`${ns}:${itemId}`)) {
|
|
340
|
+
codexDeltaItemIds.delete(`${ns}:${itemId}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const text = extractCodexItemText(item);
|
|
344
|
+
if (text) {
|
|
345
|
+
publishAssistantText(wire, ns, "codex", text, "codex item publishOutgoing");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (itemType === "commandExecution" || itemType === "mcpToolCall" || itemType === "dynamicToolCall") {
|
|
349
|
+
publishDiscordEvent(wire, ns, "codex", "tool_end");
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (method === "turn/completed") {
|
|
354
|
+
publishDiscordEvent(wire, ns, "codex", "done");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (method === "error" || msg.error) {
|
|
358
|
+
const err = (msg.error && typeof msg.error === "object") ? msg.error : params;
|
|
359
|
+
const message = typeof err.message === "string" ? err.message : JSON.stringify(err);
|
|
360
|
+
publishAssistantText(wire, ns, "codex", `⚠️ Error: ${message}`, "codex error publishOutgoing");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function spawnCodexAppServerSession(ns, token, wire, onExit) {
|
|
364
|
+
const wsPath = workspacePath(ns);
|
|
365
|
+
const proc = spawn(resolveCodex(), codexAppServerArgs(), {
|
|
366
|
+
cwd: wsPath,
|
|
367
|
+
env: buildEnv(token),
|
|
368
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
369
|
+
});
|
|
370
|
+
proc.stdin.setDefaultEncoding("utf8");
|
|
371
|
+
const pending = new Map();
|
|
372
|
+
let nextId = 1;
|
|
373
|
+
let lineBuffer = "";
|
|
374
|
+
const session = {
|
|
375
|
+
proc,
|
|
376
|
+
ns,
|
|
377
|
+
wsPath,
|
|
378
|
+
running: false,
|
|
379
|
+
queue: [],
|
|
380
|
+
ready: Promise.resolve(),
|
|
381
|
+
sendRequest(method, params) {
|
|
382
|
+
const id = nextId++;
|
|
383
|
+
const message = { method, id, params };
|
|
384
|
+
return new Promise((resolve, reject) => {
|
|
385
|
+
pending.set(id, { resolve, reject });
|
|
386
|
+
proc.stdin.write(`${JSON.stringify(message)}\n`, (err) => {
|
|
387
|
+
if (err) {
|
|
388
|
+
pending.delete(id);
|
|
389
|
+
reject(err);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
sendNotification(method, params) {
|
|
395
|
+
proc.stdin.write(`${JSON.stringify({ method, params })}\n`);
|
|
396
|
+
},
|
|
397
|
+
stop() {
|
|
398
|
+
for (const [, waiter] of pending)
|
|
399
|
+
waiter.reject(new Error("Codex app-server stopped"));
|
|
400
|
+
pending.clear();
|
|
401
|
+
try {
|
|
402
|
+
proc.stdin.end();
|
|
403
|
+
proc.kill("SIGTERM");
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// ignore shutdown errors
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
function handleLine(line) {
|
|
411
|
+
const trimmed = line.trim();
|
|
412
|
+
if (!trimmed)
|
|
413
|
+
return;
|
|
414
|
+
let msg;
|
|
415
|
+
try {
|
|
416
|
+
msg = JSON.parse(trimmed);
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
msg = { method: "text", params: { text: trimmed } };
|
|
420
|
+
}
|
|
421
|
+
if (typeof msg.id === "number") {
|
|
422
|
+
const waiter = pending.get(msg.id);
|
|
423
|
+
if (waiter) {
|
|
424
|
+
pending.delete(msg.id);
|
|
425
|
+
if (msg.error) {
|
|
426
|
+
const err = msg.error;
|
|
427
|
+
waiter.reject(new Error(err.message ?? JSON.stringify(msg.error)));
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
waiter.resolve(msg);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const method = typeof msg.method === "string" ? msg.method : "";
|
|
435
|
+
if (method === "turn/started") {
|
|
436
|
+
const params = (msg.params && typeof msg.params === "object") ? msg.params : {};
|
|
437
|
+
const turn = (params.turn && typeof params.turn === "object") ? params.turn : {};
|
|
438
|
+
if (typeof turn.id === "string")
|
|
439
|
+
session.activeTurnId = turn.id;
|
|
440
|
+
}
|
|
441
|
+
else if (method === "turn/completed") {
|
|
442
|
+
session.activeTurnId = undefined;
|
|
443
|
+
}
|
|
444
|
+
forwardCodexAppServerMessage(ns, wire, msg);
|
|
445
|
+
}
|
|
446
|
+
proc.stdout.on("data", (chunk) => {
|
|
447
|
+
lineBuffer += chunk.toString();
|
|
448
|
+
const lines = lineBuffer.split("\n");
|
|
449
|
+
lineBuffer = lines.pop() ?? "";
|
|
450
|
+
for (const line of lines)
|
|
451
|
+
handleLine(line);
|
|
452
|
+
});
|
|
453
|
+
proc.stderr.on("data", (chunk) => {
|
|
454
|
+
const text = chunk.toString().trim();
|
|
455
|
+
if (text)
|
|
456
|
+
console.log(`[meta-agent-manager:${ns}:codex-stderr] ${text}`);
|
|
457
|
+
});
|
|
458
|
+
proc.on("exit", (code) => {
|
|
459
|
+
if (lineBuffer.trim())
|
|
460
|
+
handleLine(lineBuffer);
|
|
461
|
+
for (const [, waiter] of pending)
|
|
462
|
+
waiter.reject(new Error(`Codex app-server exited with code ${code}`));
|
|
463
|
+
pending.clear();
|
|
464
|
+
console.log(`[meta-agent-manager] codex app-server exited (ns=${ns}, code=${code})`);
|
|
465
|
+
onExit();
|
|
466
|
+
});
|
|
467
|
+
proc.on("error", (err) => {
|
|
468
|
+
for (const [, waiter] of pending)
|
|
469
|
+
waiter.reject(err);
|
|
470
|
+
pending.clear();
|
|
471
|
+
console.error(`[meta-agent-manager] codex app-server spawn error (ns=${ns}):`, err.message);
|
|
472
|
+
onExit();
|
|
473
|
+
});
|
|
474
|
+
session.ready = (async () => {
|
|
475
|
+
await session.sendRequest("initialize", {
|
|
476
|
+
clientInfo: { name: "cc-discord", title: "cc-discord", version: "0.1.0" },
|
|
477
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
478
|
+
});
|
|
479
|
+
session.sendNotification("initialized", {});
|
|
480
|
+
const threadResponse = await session.sendRequest("thread/start", codexThreadParams(wsPath));
|
|
481
|
+
const result = (threadResponse.result && typeof threadResponse.result === "object")
|
|
482
|
+
? threadResponse.result
|
|
483
|
+
: {};
|
|
484
|
+
const thread = (result.thread && typeof result.thread === "object") ? result.thread : {};
|
|
485
|
+
if (typeof thread.id !== "string")
|
|
486
|
+
throw new Error("Codex app-server did not return a thread id");
|
|
487
|
+
session.threadId = thread.id;
|
|
488
|
+
})();
|
|
489
|
+
return session;
|
|
490
|
+
}
|
|
147
491
|
/**
|
|
148
492
|
* Wire stdout from a claude subprocess into Redis.
|
|
149
493
|
* Parses JSONL lines from stream-json format, publishes to:
|
|
@@ -151,12 +495,10 @@ function buildEnv(token) {
|
|
|
151
495
|
* LPUSH cca:meta:{ns}:log (capped at 2000)
|
|
152
496
|
* Also dispatches assistant/result text to wire.discord.publishOutgoing.
|
|
153
497
|
*/
|
|
154
|
-
function wireStdoutToRedis(proc, ns, wire) {
|
|
498
|
+
function wireStdoutToRedis(proc, ns, wire, driver = "claude", onParsedEvent) {
|
|
155
499
|
const rawRedis = wire._redis;
|
|
156
500
|
const streamCh = metaStreamChannel(ns);
|
|
157
501
|
const logKey = metaLogKey(ns);
|
|
158
|
-
// Deduplicate rate_limit_event: Claude emits multiple per turn; only notify once per session.
|
|
159
|
-
let rateLimitNotified = false;
|
|
160
502
|
const forwardEventToRedis = (eventJson) => {
|
|
161
503
|
rawRedis.publish(streamCh, eventJson).catch((err) => {
|
|
162
504
|
console.warn(`[meta-agent-manager] stream publish failed (ns=${ns}):`, err.message);
|
|
@@ -174,6 +516,7 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
174
516
|
let structuredEvent;
|
|
175
517
|
try {
|
|
176
518
|
const parsed = JSON.parse(trimmed);
|
|
519
|
+
onParsedEvent?.(parsed);
|
|
177
520
|
const type = parsed.type;
|
|
178
521
|
if (type === "assistant") {
|
|
179
522
|
const content = parsed.message;
|
|
@@ -181,17 +524,7 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
181
524
|
const text = textBlock?.text ?? "";
|
|
182
525
|
structuredEvent = { type: "assistant", text };
|
|
183
526
|
if (text) {
|
|
184
|
-
|
|
185
|
-
id: crypto.randomUUID(),
|
|
186
|
-
source: "claude",
|
|
187
|
-
role: "assistant",
|
|
188
|
-
content: text,
|
|
189
|
-
timestamp: new Date().toISOString(),
|
|
190
|
-
chatId: 0,
|
|
191
|
-
};
|
|
192
|
-
wire.discord.publishOutgoing(ns, msg).catch((err) => {
|
|
193
|
-
console.warn(`[meta-agent-manager] publishOutgoing failed (ns=${ns}):`, err.message);
|
|
194
|
-
});
|
|
527
|
+
publishAssistantText(wire, ns, driver, text);
|
|
195
528
|
}
|
|
196
529
|
}
|
|
197
530
|
else if (type === "tool_use") {
|
|
@@ -203,7 +536,7 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
203
536
|
};
|
|
204
537
|
// Ephemeral signal: notifier shows tool activity overlay in the live message
|
|
205
538
|
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
206
|
-
id: crypto.randomUUID(), source:
|
|
539
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
207
540
|
content: toolName, event: "tool_start", timestamp: new Date().toISOString(), chatId: 0,
|
|
208
541
|
})).catch(() => { });
|
|
209
542
|
}
|
|
@@ -214,7 +547,7 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
214
547
|
};
|
|
215
548
|
// Ephemeral signal: tool finished, notifier can restart finalize timer
|
|
216
549
|
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
217
|
-
id: crypto.randomUUID(), source:
|
|
550
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
218
551
|
content: "", event: "tool_end", timestamp: new Date().toISOString(), chatId: 0,
|
|
219
552
|
})).catch(() => { });
|
|
220
553
|
}
|
|
@@ -227,55 +560,54 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
227
560
|
};
|
|
228
561
|
if (parsed.is_error && resultText) {
|
|
229
562
|
// Error case: assistant event may not carry the error message — publish it directly
|
|
230
|
-
|
|
231
|
-
id: crypto.randomUUID(),
|
|
232
|
-
source: "claude",
|
|
233
|
-
role: "assistant",
|
|
234
|
-
content: `⚠️ Error: ${resultText}`,
|
|
235
|
-
timestamp: new Date().toISOString(),
|
|
236
|
-
chatId: 0,
|
|
237
|
-
};
|
|
238
|
-
wire.discord.publishOutgoing(ns, errMsg).catch(() => { });
|
|
563
|
+
publishAssistantText(wire, ns, driver, `⚠️ Error: ${resultText}`);
|
|
239
564
|
}
|
|
240
565
|
// Signal turn completion — text was already published via the assistant event above.
|
|
241
566
|
// Do NOT re-publish resultText here: that would double-deliver the same content.
|
|
242
567
|
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
243
|
-
id: crypto.randomUUID(), source:
|
|
568
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
244
569
|
content: "", event: "done", timestamp: new Date().toISOString(), chatId: 0,
|
|
245
570
|
})).catch(() => { });
|
|
246
571
|
}
|
|
247
|
-
else if (type === "
|
|
248
|
-
const
|
|
572
|
+
else if (driver === "codex" && type === "item.completed") {
|
|
573
|
+
const item = parsed.item;
|
|
574
|
+
const itemType = item?.type;
|
|
575
|
+
const text = typeof item?.text === "string" ? item.text : "";
|
|
249
576
|
structuredEvent = parsed;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const resetsAt = rl.resetsAt ? new Date(rl.resetsAt * 1000).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZoneName: "short" }) : "soon";
|
|
255
|
-
const alertMsg = {
|
|
256
|
-
id: crypto.randomUUID(),
|
|
257
|
-
source: "claude",
|
|
258
|
-
role: "assistant",
|
|
259
|
-
content: `⏳ Rate limited — five-hour window exhausted. Resets at **${resetsAt}**. Session paused; send any message after reset to resume.`,
|
|
260
|
-
timestamp: new Date().toISOString(),
|
|
261
|
-
chatId: 0,
|
|
262
|
-
};
|
|
263
|
-
wire.discord.publishOutgoing(ns, alertMsg).catch(() => { });
|
|
264
|
-
// Fire done so notifier finalizes the live message
|
|
577
|
+
if (itemType === "agent_message" && text) {
|
|
578
|
+
publishAssistantText(wire, ns, "codex", text);
|
|
579
|
+
}
|
|
580
|
+
else if (itemType === "command_execution" || itemType === "mcp_tool_call") {
|
|
265
581
|
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
266
|
-
id: crypto.randomUUID(), source:
|
|
267
|
-
content: "", event: "
|
|
582
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
583
|
+
content: "", event: "tool_end", timestamp: new Date().toISOString(), chatId: 0,
|
|
584
|
+
})).catch(() => { });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else if (driver === "codex" && type === "item.started") {
|
|
588
|
+
const item = parsed.item;
|
|
589
|
+
const itemType = item?.type;
|
|
590
|
+
structuredEvent = parsed;
|
|
591
|
+
if (itemType === "command_execution" || itemType === "mcp_tool_call") {
|
|
592
|
+
const label = typeof item?.command === "string" ? item.command : (itemType ?? "tool");
|
|
593
|
+
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
594
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
595
|
+
content: label, event: "tool_start", timestamp: new Date().toISOString(), chatId: 0,
|
|
268
596
|
})).catch(() => { });
|
|
269
|
-
// Kill the process — API calls will hang until reset anyway
|
|
270
|
-
setTimeout(() => {
|
|
271
|
-
try {
|
|
272
|
-
proc.stdin.end();
|
|
273
|
-
proc.kill("SIGTERM");
|
|
274
|
-
}
|
|
275
|
-
catch { /* ignore */ }
|
|
276
|
-
}, 500);
|
|
277
597
|
}
|
|
278
598
|
}
|
|
599
|
+
else if (driver === "codex" && (type === "turn.completed" || type === "turn.failed")) {
|
|
600
|
+
structuredEvent = parsed;
|
|
601
|
+
rawRedis.publish(discordChatOutgoing(ns), JSON.stringify({
|
|
602
|
+
id: crypto.randomUUID(), source: driver, role: "assistant",
|
|
603
|
+
content: "", event: "done", timestamp: new Date().toISOString(), chatId: 0,
|
|
604
|
+
})).catch(() => { });
|
|
605
|
+
}
|
|
606
|
+
else if (driver === "codex" && type === "error") {
|
|
607
|
+
const message = typeof parsed.message === "string" ? parsed.message : JSON.stringify(parsed);
|
|
608
|
+
structuredEvent = parsed;
|
|
609
|
+
publishAssistantText(wire, ns, "codex", `⚠️ Error: ${message}`);
|
|
610
|
+
}
|
|
279
611
|
else {
|
|
280
612
|
structuredEvent = parsed;
|
|
281
613
|
}
|
|
@@ -317,8 +649,26 @@ function wireStdoutToRedis(proc, ns, wire) {
|
|
|
317
649
|
export function spawnSession(ns, message, token, wire) {
|
|
318
650
|
return new Promise((resolve, reject) => {
|
|
319
651
|
const wsPath = workspacePath(ns);
|
|
320
|
-
const claudeBin = resolveClaude();
|
|
321
652
|
const env = buildEnv(token);
|
|
653
|
+
const driver = agentDriver();
|
|
654
|
+
if (driver === "codex") {
|
|
655
|
+
const proc = spawn(resolveCodex(), codexArgs(message), {
|
|
656
|
+
cwd: wsPath,
|
|
657
|
+
env,
|
|
658
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
659
|
+
});
|
|
660
|
+
wireStdoutToRedis(proc, ns, wire, "codex");
|
|
661
|
+
proc.on("exit", (code) => {
|
|
662
|
+
console.log(`[meta-agent-manager] codex session exited (ns=${ns}, code=${code})`);
|
|
663
|
+
resolve();
|
|
664
|
+
});
|
|
665
|
+
proc.on("error", (err) => {
|
|
666
|
+
console.error(`[meta-agent-manager] codex spawn error (ns=${ns}):`, err.message);
|
|
667
|
+
reject(err);
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const claudeBin = resolveClaude();
|
|
322
672
|
const proc = spawn(claudeBin, [
|
|
323
673
|
"--continue",
|
|
324
674
|
"-p", message,
|
|
@@ -397,6 +747,8 @@ export function createMetaAgentManager() {
|
|
|
397
747
|
let watchdogInterval = null;
|
|
398
748
|
/** One persistent ChildProcess per namespace. */
|
|
399
749
|
const sessions = new Map();
|
|
750
|
+
/** One persistent Codex app-server process per namespace. */
|
|
751
|
+
const codexSessions = new Map();
|
|
400
752
|
/** Namespaces currently being set up (workspace clone / first spawn). */
|
|
401
753
|
const startingUp = new Set();
|
|
402
754
|
/** Wire reference needed for watchdog notifications. Set in startPolling. */
|
|
@@ -488,6 +840,91 @@ export function createMetaAgentManager() {
|
|
|
488
840
|
writeToStdin(ns, content);
|
|
489
841
|
}
|
|
490
842
|
}
|
|
843
|
+
async function popQueueMessage(ns, wire) {
|
|
844
|
+
const inputKey = discordMetaInputKey(ns);
|
|
845
|
+
let raw;
|
|
846
|
+
try {
|
|
847
|
+
raw = await wire._redis.lpop(inputKey);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
if (!raw)
|
|
853
|
+
return null;
|
|
854
|
+
try {
|
|
855
|
+
return JSON.parse(raw).content ?? raw;
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return raw;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function ensureCodexSession(ns, repoUrl, token, wire) {
|
|
862
|
+
const existing = codexSessions.get(ns);
|
|
863
|
+
if (existing) {
|
|
864
|
+
await existing.ready;
|
|
865
|
+
return existing;
|
|
866
|
+
}
|
|
867
|
+
const wsPath = workspacePath(ns);
|
|
868
|
+
await ensureWorkspace(ns, repoUrl);
|
|
869
|
+
injectMcp(ns, wsPath, token);
|
|
870
|
+
const session = spawnCodexAppServerSession(ns, token, wire, () => {
|
|
871
|
+
codexSessions.delete(ns);
|
|
872
|
+
console.log(`[meta-agent-manager] codex session removed from map (ns=${ns})`);
|
|
873
|
+
});
|
|
874
|
+
codexSessions.set(ns, session);
|
|
875
|
+
await session.ready;
|
|
876
|
+
return session;
|
|
877
|
+
}
|
|
878
|
+
async function runCodexTurn(ns, repoUrl, token, wire, message) {
|
|
879
|
+
const session = await ensureCodexSession(ns, repoUrl, token, wire);
|
|
880
|
+
if (message.trim() === "/compact") {
|
|
881
|
+
await session.sendRequest("thread/compact/start", { threadId: session.threadId });
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (!session.threadId)
|
|
885
|
+
throw new Error("Codex app-server session is missing thread id");
|
|
886
|
+
const response = await session.sendRequest("turn/start", codexTurnParams(session.threadId, message, session.wsPath));
|
|
887
|
+
const result = (response.result && typeof response.result === "object") ? response.result : {};
|
|
888
|
+
const turn = (result.turn && typeof result.turn === "object") ? result.turn : {};
|
|
889
|
+
if (typeof turn.id === "string")
|
|
890
|
+
session.activeTurnId = turn.id;
|
|
891
|
+
}
|
|
892
|
+
async function drainCodexQueue(ns, repoUrl, token, wire) {
|
|
893
|
+
const session = await ensureCodexSession(ns, repoUrl, token, wire);
|
|
894
|
+
if (session.running)
|
|
895
|
+
return;
|
|
896
|
+
session.running = true;
|
|
897
|
+
try {
|
|
898
|
+
for (;;) {
|
|
899
|
+
const message = await popQueueMessage(ns, wire);
|
|
900
|
+
if (message == null)
|
|
901
|
+
break;
|
|
902
|
+
if (session.activeTurnId && session.threadId) {
|
|
903
|
+
await session.sendRequest("turn/steer", {
|
|
904
|
+
threadId: session.threadId,
|
|
905
|
+
expectedTurnId: session.activeTurnId,
|
|
906
|
+
input: [{ type: "text", text: message, text_elements: [] }],
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
await runCodexTurn(ns, repoUrl, token, wire, message);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
await wire.discord.setStatus(ns, {
|
|
914
|
+
namespace: ns,
|
|
915
|
+
status: "running",
|
|
916
|
+
isTyping: false,
|
|
917
|
+
turnCount: 0,
|
|
918
|
+
updatedAt: new Date().toISOString(),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
catch (err) {
|
|
922
|
+
console.error(`[meta-agent-manager] drainCodexQueue failed (ns=${ns}):`, err.message);
|
|
923
|
+
}
|
|
924
|
+
finally {
|
|
925
|
+
session.running = false;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
491
928
|
/**
|
|
492
929
|
* Ensure a persistent session exists for ns.
|
|
493
930
|
* If one already exists, returns immediately.
|
|
@@ -577,6 +1014,28 @@ export function createMetaAgentManager() {
|
|
|
577
1014
|
}).catch(() => { });
|
|
578
1015
|
}
|
|
579
1016
|
// If session exists, drain any queued messages
|
|
1017
|
+
if (agentDriver() === "codex") {
|
|
1018
|
+
const inputKey = discordMetaInputKey(ns);
|
|
1019
|
+
wire._redis.llen(inputKey).then(async (queueLen) => {
|
|
1020
|
+
if (queueLen === 0)
|
|
1021
|
+
return;
|
|
1022
|
+
let token = "";
|
|
1023
|
+
try {
|
|
1024
|
+
token = await wire.token.getMaster();
|
|
1025
|
+
}
|
|
1026
|
+
catch {
|
|
1027
|
+
token = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
1028
|
+
?? process.env.CLAUDE_CODE_TOKEN
|
|
1029
|
+
?? process.env.ANTHROPIC_API_KEY
|
|
1030
|
+
?? "";
|
|
1031
|
+
}
|
|
1032
|
+
await drainCodexQueue(ns, repoUrl, token, wire);
|
|
1033
|
+
}).catch((err) => {
|
|
1034
|
+
console.warn(`[meta-agent-manager] codex llen error (ns=${ns}):`, err.message);
|
|
1035
|
+
});
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
// If session exists, drain any queued messages
|
|
580
1039
|
if (sessions.has(ns)) {
|
|
581
1040
|
drainQueue(ns, wire).catch((err) => {
|
|
582
1041
|
console.warn(`[meta-agent-manager] drainQueue error (ns=${ns}):`, err.message);
|
|
@@ -634,8 +1093,22 @@ export function createMetaAgentManager() {
|
|
|
634
1093
|
sessions.delete(ns);
|
|
635
1094
|
console.log(`[meta-agent-manager] killed session on stop (ns=${ns})`);
|
|
636
1095
|
}
|
|
1096
|
+
for (const [ns, session] of codexSessions) {
|
|
1097
|
+
session.stop();
|
|
1098
|
+
codexSessions.delete(ns);
|
|
1099
|
+
console.log(`[meta-agent-manager] killed codex session on stop (ns=${ns})`);
|
|
1100
|
+
}
|
|
1101
|
+
codexSessions.clear();
|
|
637
1102
|
},
|
|
638
1103
|
killSession(ns) {
|
|
1104
|
+
if (agentDriver() === "codex") {
|
|
1105
|
+
const session = codexSessions.get(ns);
|
|
1106
|
+
if (session)
|
|
1107
|
+
session.stop();
|
|
1108
|
+
codexSessions.delete(ns);
|
|
1109
|
+
console.log(`[meta-agent-manager] cleared codex session state (ns=${ns})`);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
639
1112
|
const session = sessions.get(ns);
|
|
640
1113
|
if (!session)
|
|
641
1114
|
return;
|
|
@@ -650,12 +1123,25 @@ export function createMetaAgentManager() {
|
|
|
650
1123
|
console.log(`[meta-agent-manager] killed session (ns=${ns})`);
|
|
651
1124
|
},
|
|
652
1125
|
sendToSession(ns, line) {
|
|
1126
|
+
if (agentDriver() === "codex") {
|
|
1127
|
+
if (!wireRef)
|
|
1128
|
+
return;
|
|
1129
|
+
wireRef._redis.rpush(discordMetaInputKey(ns), JSON.stringify({
|
|
1130
|
+
id: crypto.randomUUID(),
|
|
1131
|
+
content: line,
|
|
1132
|
+
timestamp: new Date().toISOString(),
|
|
1133
|
+
source: "cc-discord",
|
|
1134
|
+
})).catch((err) => {
|
|
1135
|
+
console.warn(`[meta-agent-manager] codex sendToSession enqueue failed (ns=${ns}):`, err.message);
|
|
1136
|
+
});
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
653
1139
|
writeToStdin(ns, line);
|
|
654
1140
|
},
|
|
655
1141
|
};
|
|
656
1142
|
}
|
|
657
1143
|
/**
|
|
658
|
-
* Migrate the old
|
|
1144
|
+
* Migrate the old meta input key to the new cc-discord key.
|
|
659
1145
|
* Old: cca:meta:{ns}:input → New: cca:discord:meta:{ns}:input
|
|
660
1146
|
*
|
|
661
1147
|
* Called once on startup.
|