@leo000001/codex-mcp 2.1.1 → 2.1.4
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/README.md +61 -31
- package/dist/index.js +707 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -197,8 +197,8 @@ function tryResolveNodeScriptFromShim(shimPath, codexCommand, exists, readFile,
|
|
|
197
197
|
// src/utils/codex-executable.ts
|
|
198
198
|
import { accessSync, constants, existsSync as existsSync2, statSync } from "fs";
|
|
199
199
|
import path2 from "path";
|
|
200
|
-
var
|
|
201
|
-
var
|
|
200
|
+
var CODEX_MCP_COMMAND = "CODEX_MCP_COMMAND";
|
|
201
|
+
var CODEX_MCP_PATH = "CODEX_MCP_PATH";
|
|
202
202
|
var AUTO_CODEX_COMMANDS = ["codex", "codex-internal"];
|
|
203
203
|
var _resolved;
|
|
204
204
|
var WINDOWS_SUPPORTED_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
@@ -263,33 +263,33 @@ function looksLikePath(value) {
|
|
|
263
263
|
return value.includes("/") || value.includes("\\");
|
|
264
264
|
}
|
|
265
265
|
function resolveDefaultCodexExecutable(env = process.env) {
|
|
266
|
-
const envPathRaw = env[
|
|
267
|
-
const envCommandRaw = env[
|
|
266
|
+
const envPathRaw = env[CODEX_MCP_PATH]?.trim();
|
|
267
|
+
const envCommandRaw = env[CODEX_MCP_COMMAND]?.trim();
|
|
268
268
|
const envPath = envPathRaw ? normalizeMaybeQuotedToken(envPathRaw) : void 0;
|
|
269
269
|
const envCommand = envCommandRaw ? normalizeMaybeQuotedToken(envCommandRaw) : void 0;
|
|
270
270
|
if (envPath && envCommand) {
|
|
271
271
|
throw new Error(
|
|
272
|
-
`Cannot set both ${
|
|
272
|
+
`Cannot set both ${CODEX_MCP_PATH} and ${CODEX_MCP_COMMAND}. Use one or the other.`
|
|
273
273
|
);
|
|
274
274
|
}
|
|
275
275
|
if (envPath) {
|
|
276
276
|
const resolvedPath = path2.resolve(envPath);
|
|
277
277
|
if (!existsSync2(resolvedPath)) {
|
|
278
|
-
throw new Error(`${
|
|
278
|
+
throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 file does not exist.`);
|
|
279
279
|
}
|
|
280
280
|
if (!isExecutableFile(resolvedPath)) {
|
|
281
|
-
throw new Error(`${
|
|
281
|
+
throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 not an executable file.`);
|
|
282
282
|
}
|
|
283
283
|
return { command: resolvedPath, isPath: true, source: "env_path" };
|
|
284
284
|
}
|
|
285
285
|
if (envCommand) {
|
|
286
286
|
if (looksLikePath(envCommand)) {
|
|
287
287
|
throw new Error(
|
|
288
|
-
`${
|
|
288
|
+
`${CODEX_MCP_COMMAND}="${envCommand}" looks like a path. Use ${CODEX_MCP_PATH} for filesystem paths.`
|
|
289
289
|
);
|
|
290
290
|
}
|
|
291
291
|
if (!commandExistsOnPath(envCommand, env)) {
|
|
292
|
-
throw new Error(`${
|
|
292
|
+
throw new Error(`${CODEX_MCP_COMMAND}="${envCommand}" was not found in PATH.`);
|
|
293
293
|
}
|
|
294
294
|
return { command: envCommand, isPath: false, source: "env_command" };
|
|
295
295
|
}
|
|
@@ -311,17 +311,17 @@ function checkDefaultCodexExecutableAvailability() {
|
|
|
311
311
|
const label = info.isPath ? "path" : "command";
|
|
312
312
|
switch (info.source) {
|
|
313
313
|
case "env_path":
|
|
314
|
-
console.error(`[codex-executable] Using ${
|
|
314
|
+
console.error(`[codex-executable] Using ${CODEX_MCP_PATH}: ${info.command}`);
|
|
315
315
|
break;
|
|
316
316
|
case "env_command":
|
|
317
|
-
console.error(`[codex-executable] Using ${
|
|
317
|
+
console.error(`[codex-executable] Using ${CODEX_MCP_COMMAND}: ${info.command}`);
|
|
318
318
|
break;
|
|
319
319
|
case "auto_detect":
|
|
320
320
|
console.error(`[codex-executable] Auto-detected ${label}: ${info.command}`);
|
|
321
321
|
break;
|
|
322
322
|
case "default":
|
|
323
323
|
console.error(
|
|
324
|
-
`[codex-executable] No codex found on PATH; falling back to "${info.command}". Set ${
|
|
324
|
+
`[codex-executable] No codex found on PATH; falling back to "${info.command}". Set ${CODEX_MCP_COMMAND} or ${CODEX_MCP_PATH} to configure.`
|
|
325
325
|
);
|
|
326
326
|
break;
|
|
327
327
|
}
|
|
@@ -372,6 +372,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
372
372
|
ErrorCode2["THREAD_FORK_RESUME_FAILED"] = "THREAD_FORK_RESUME_FAILED";
|
|
373
373
|
ErrorCode2["PROTOCOL_PARSE_ERROR"] = "PROTOCOL_PARSE_ERROR";
|
|
374
374
|
ErrorCode2["WRITE_QUEUE_DROPPED"] = "WRITE_QUEUE_DROPPED";
|
|
375
|
+
ErrorCode2["EXEC_NOT_SUPPORTED"] = "EXEC_NOT_SUPPORTED";
|
|
375
376
|
ErrorCode2["INTERNAL"] = "INTERNAL";
|
|
376
377
|
return ErrorCode2;
|
|
377
378
|
})(ErrorCode || {});
|
|
@@ -391,7 +392,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
|
|
|
391
392
|
var CLEANUP_INTERVAL_MS = 6e4;
|
|
392
393
|
|
|
393
394
|
// src/app-server/client.ts
|
|
394
|
-
var CLIENT_VERSION = true ? "2.1.
|
|
395
|
+
var CLIENT_VERSION = true ? "2.1.4" : "0.0.0-dev";
|
|
395
396
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
396
397
|
var STARTUP_REQUEST_TIMEOUT = 9e4;
|
|
397
398
|
var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
|
|
@@ -413,6 +414,9 @@ var AppServerClient = class extends EventEmitter {
|
|
|
413
414
|
get destroyed() {
|
|
414
415
|
return this._destroyed;
|
|
415
416
|
}
|
|
417
|
+
get supportsTurnOverrides() {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
416
420
|
/**
|
|
417
421
|
* Spawn codex app-server and perform initialization handshake.
|
|
418
422
|
*/
|
|
@@ -810,7 +814,7 @@ function resolveAndValidateCwd(inputCwd, baseCwd) {
|
|
|
810
814
|
function redactPaths(message) {
|
|
811
815
|
const uncPath = /(^|[\s'"(])\\\\[^\s\\/:]+\\[^\s:]+(?:\\[^\s:]+)*/g;
|
|
812
816
|
const windowsPath = /\b[A-Za-z]:\\[^\s:]+/g;
|
|
813
|
-
const posixPath = /(^|[\s'"(])\/[^\s:'")]+/g;
|
|
817
|
+
const posixPath = /(^|[\s'"(])\/[^\s:'")]*\/[^\s:'")]+/g;
|
|
814
818
|
return message.replace(uncPath, (_m, prefix) => `${prefix}<path>`).replace(windowsPath, "<path>").replace(posixPath, (_m, prefix) => `${prefix}<path>`);
|
|
815
819
|
}
|
|
816
820
|
|
|
@@ -1001,12 +1005,13 @@ var SessionManager = class {
|
|
|
1001
1005
|
const turnStartResult = await client.turnStart(turnParams);
|
|
1002
1006
|
const startedTurnId = extractTurnId(turnStartResult);
|
|
1003
1007
|
if (startedTurnId) session.activeTurnId = startedTurnId;
|
|
1004
|
-
|
|
1008
|
+
const canOverride = client.supportsTurnOverrides;
|
|
1009
|
+
if (resolvedCwd && canOverride) session.cwd = resolvedCwd;
|
|
1005
1010
|
if (overrides?.model) session.model = overrides.model;
|
|
1006
1011
|
if (overrides?.approvalPolicy) {
|
|
1007
1012
|
session.approvalPolicy = overrides.approvalPolicy;
|
|
1008
1013
|
}
|
|
1009
|
-
if (overrides?.sandbox) {
|
|
1014
|
+
if (overrides?.sandbox && canOverride) {
|
|
1010
1015
|
session.sandbox = overrides.sandbox;
|
|
1011
1016
|
}
|
|
1012
1017
|
} catch (err) {
|
|
@@ -1459,10 +1464,10 @@ var SessionManager = class {
|
|
|
1459
1464
|
`Error [${"INTERNAL" /* INTERNAL */}]: Failed to build approval response for request '${requestId}'`
|
|
1460
1465
|
);
|
|
1461
1466
|
}
|
|
1462
|
-
sendPendingRequestResponseOrThrow(req, response, sessionId, requestId);
|
|
1463
1467
|
req.resolved = true;
|
|
1464
1468
|
req.decision = decision;
|
|
1465
1469
|
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1470
|
+
sendPendingRequestResponseOrThrow(req, response, sessionId, requestId);
|
|
1466
1471
|
pushEvent(
|
|
1467
1472
|
session.eventBuffer,
|
|
1468
1473
|
"approval_result",
|
|
@@ -1489,14 +1494,14 @@ var SessionManager = class {
|
|
|
1489
1494
|
`Error [${"REQUEST_NOT_FOUND" /* REQUEST_NOT_FOUND */}]: User input request '${requestId}' not found`
|
|
1490
1495
|
);
|
|
1491
1496
|
}
|
|
1497
|
+
req.resolved = true;
|
|
1498
|
+
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1492
1499
|
sendPendingRequestResponseOrThrow(
|
|
1493
1500
|
req,
|
|
1494
1501
|
{ answers },
|
|
1495
1502
|
sessionId,
|
|
1496
1503
|
requestId
|
|
1497
1504
|
);
|
|
1498
|
-
req.resolved = true;
|
|
1499
|
-
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1500
1505
|
pushEvent(
|
|
1501
1506
|
session.eventBuffer,
|
|
1502
1507
|
"approval_result",
|
|
@@ -1558,10 +1563,14 @@ var SessionManager = class {
|
|
|
1558
1563
|
switch (method) {
|
|
1559
1564
|
case Methods.THREAD_STARTED: {
|
|
1560
1565
|
const thread = isRecord(p.thread) ? p.thread : void 0;
|
|
1566
|
+
const notifiedThreadId = normalizeOptionalString(p.threadId) ?? normalizeOptionalString(thread?.id);
|
|
1567
|
+
if (notifiedThreadId && notifiedThreadId !== session.threadId) {
|
|
1568
|
+
session.threadId = notifiedThreadId;
|
|
1569
|
+
}
|
|
1561
1570
|
pushEvent(session.eventBuffer, "progress", {
|
|
1562
1571
|
method,
|
|
1563
1572
|
...p,
|
|
1564
|
-
threadId:
|
|
1573
|
+
threadId: notifiedThreadId,
|
|
1565
1574
|
status: normalizeOptionalString(thread?.status)
|
|
1566
1575
|
});
|
|
1567
1576
|
break;
|
|
@@ -2009,6 +2018,14 @@ function clearSessionPendingRequests(session) {
|
|
|
2009
2018
|
session.pendingRequests.clear();
|
|
2010
2019
|
for (const [, req] of entries) {
|
|
2011
2020
|
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
2021
|
+
if (!req.resolved && req.respond) {
|
|
2022
|
+
try {
|
|
2023
|
+
if (req.kind === "command") req.respond({ decision: "cancel" });
|
|
2024
|
+
else if (req.kind === "fileChange") req.respond({ decision: "cancel" });
|
|
2025
|
+
else if (req.kind === "user_input") req.respond({ answers: {} });
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2012
2029
|
req.resolved = true;
|
|
2013
2030
|
}
|
|
2014
2031
|
}
|
|
@@ -2290,15 +2307,27 @@ function tryCoalesceProgressDelta(buf, type, data, pinned) {
|
|
|
2290
2307
|
return true;
|
|
2291
2308
|
}
|
|
2292
2309
|
function evictEvents(buf) {
|
|
2293
|
-
|
|
2294
|
-
const
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2310
|
+
if (buf.events.length > buf.maxSize) {
|
|
2311
|
+
const overflow2 = buf.events.length - buf.maxSize;
|
|
2312
|
+
const unpinnedIdx = [];
|
|
2313
|
+
const approvalResultIdx2 = [];
|
|
2314
|
+
for (let i = 0; i < buf.events.length; i++) {
|
|
2315
|
+
const event = buf.events[i];
|
|
2316
|
+
if (!event.pinned) unpinnedIdx.push(i);
|
|
2317
|
+
else if (event.type === "approval_result") approvalResultIdx2.push(i);
|
|
2318
|
+
}
|
|
2319
|
+
const drop2 = /* @__PURE__ */ new Set();
|
|
2320
|
+
for (const idx of unpinnedIdx) {
|
|
2321
|
+
if (drop2.size >= overflow2) break;
|
|
2322
|
+
drop2.add(idx);
|
|
2323
|
+
}
|
|
2324
|
+
for (const idx of approvalResultIdx2) {
|
|
2325
|
+
if (drop2.size >= overflow2) break;
|
|
2326
|
+
drop2.add(idx);
|
|
2327
|
+
}
|
|
2328
|
+
if (drop2.size > 0) {
|
|
2329
|
+
buf.events = buf.events.filter((_, idx) => !drop2.has(idx));
|
|
2330
|
+
}
|
|
2302
2331
|
}
|
|
2303
2332
|
if (buf.events.length <= buf.hardMaxSize) return;
|
|
2304
2333
|
const overflow = buf.events.length - buf.hardMaxSize;
|
|
@@ -2808,6 +2837,7 @@ var ERROR_CODE_HINTS = {
|
|
|
2808
2837
|
["THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */]: "Forked thread could not resume in new process. Retry fork from current source session.",
|
|
2809
2838
|
["PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */]: "Non-JSON or malformed app-server line. Check shell/profile noise and transport health.",
|
|
2810
2839
|
["WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */]: "stdin backpressure overflow. Reduce burst size and re-run in smaller turns.",
|
|
2840
|
+
["EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */]: "Operation not supported in exec mode. Features like threadFork and threadResume require app-server mode.",
|
|
2811
2841
|
["INTERNAL" /* INTERNAL */]: "Unexpected server-side failure. Inspect logs and retry safely."
|
|
2812
2842
|
};
|
|
2813
2843
|
function asTextResource(uri, text, mimeType) {
|
|
@@ -2823,7 +2853,8 @@ function asTextResource(uri, text, mimeType) {
|
|
|
2823
2853
|
}
|
|
2824
2854
|
function detectCodexCliVersion(timeoutMs = 1500) {
|
|
2825
2855
|
try {
|
|
2826
|
-
const
|
|
2856
|
+
const executable = getDefaultCodexExecutable();
|
|
2857
|
+
const run = spawnSync(executable.command, ["--version"], {
|
|
2827
2858
|
encoding: "utf8",
|
|
2828
2859
|
timeout: timeoutMs,
|
|
2829
2860
|
windowsHide: true
|
|
@@ -2917,6 +2948,7 @@ function buildGotchasText() {
|
|
|
2917
2948
|
"- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
|
|
2918
2949
|
"- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
|
|
2919
2950
|
"- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
|
|
2951
|
+
"- **Poll frequency guidance**: Adapt poll interval to task complexity and previous poll results. For `running` sessions, start at 2 minutes and increase for long tasks. Only poll frequently (~1s) when `waiting_approval`. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.",
|
|
2920
2952
|
"",
|
|
2921
2953
|
"## Approval behavior",
|
|
2922
2954
|
"",
|
|
@@ -2950,6 +2982,13 @@ function buildGotchasText() {
|
|
|
2950
2982
|
"",
|
|
2951
2983
|
"- codex-mcp does not hard-code a strict concurrent-session cap.",
|
|
2952
2984
|
"- Practical limit depends on machine resources and child-process load.",
|
|
2985
|
+
"",
|
|
2986
|
+
"## Exec fallback mode",
|
|
2987
|
+
"",
|
|
2988
|
+
"- When the codex binary does not support `app-server`, codex-mcp falls back to `exec` mode (`codex exec --json`).",
|
|
2989
|
+
"- Check `codex-mcp:///server-info` `clientMode` field to detect which mode is active.",
|
|
2990
|
+
"- **Exec mode supports multi-turn**: first turn uses `codex exec`, subsequent turns use `codex exec resume <threadId>` for context continuity.",
|
|
2991
|
+
"- **Exec mode limitations**: no approval/user-input interactions, `threadFork`/`threadResume` throw `EXEC_NOT_SUPPORTED`. `sandbox`/`profile`/`cwd`/`outputSchema` overrides only apply on the first turn (exec resume does not support `-s`/`-p`/`-C`/`--output-schema`).",
|
|
2953
2992
|
""
|
|
2954
2993
|
].join("\n");
|
|
2955
2994
|
}
|
|
@@ -3130,6 +3169,7 @@ function registerResources(server, deps) {
|
|
|
3130
3169
|
name: "codex-mcp",
|
|
3131
3170
|
version: deps.version,
|
|
3132
3171
|
codexCliVersion: getCodexCliVersion(),
|
|
3172
|
+
clientMode: deps.clientMode ?? "app-server",
|
|
3133
3173
|
node: process.version,
|
|
3134
3174
|
platform: process.platform,
|
|
3135
3175
|
arch: process.arch,
|
|
@@ -3221,7 +3261,7 @@ function registerResources(server, deps) {
|
|
|
3221
3261
|
}
|
|
3222
3262
|
|
|
3223
3263
|
// src/server.ts
|
|
3224
|
-
var SERVER_VERSION = true ? "2.1.
|
|
3264
|
+
var SERVER_VERSION = true ? "2.1.4" : "0.0.0-dev";
|
|
3225
3265
|
function formatErrorMessage(err) {
|
|
3226
3266
|
const message = err instanceof Error ? err.message : String(err);
|
|
3227
3267
|
const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
|
|
@@ -3240,13 +3280,17 @@ function toStructuredContent(value) {
|
|
|
3240
3280
|
}
|
|
3241
3281
|
return { value };
|
|
3242
3282
|
}
|
|
3243
|
-
function createServer(serverCwd) {
|
|
3244
|
-
const sessionManager = new SessionManager();
|
|
3283
|
+
function createServer(serverCwd, options) {
|
|
3284
|
+
const sessionManager = new SessionManager(options);
|
|
3245
3285
|
const server = new McpServer({
|
|
3246
3286
|
name: "codex-mcp",
|
|
3247
3287
|
version: SERVER_VERSION
|
|
3248
3288
|
});
|
|
3249
|
-
registerResources(server, {
|
|
3289
|
+
registerResources(server, {
|
|
3290
|
+
version: SERVER_VERSION,
|
|
3291
|
+
sessionManager,
|
|
3292
|
+
clientMode: options?.clientMode
|
|
3293
|
+
});
|
|
3250
3294
|
const publicSessionInfoSchema = z.object({
|
|
3251
3295
|
sessionId: z.string(),
|
|
3252
3296
|
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]),
|
|
@@ -3585,9 +3629,10 @@ function createServer(serverCwd) {
|
|
|
3585
3629
|
|
|
3586
3630
|
POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
|
|
3587
3631
|
- Treat pollInterval as a minimum hint, not a fixed schedule.
|
|
3588
|
-
- "running":
|
|
3632
|
+
- "running": sleep at least 2 minutes between polls; increase for complex tasks. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.
|
|
3589
3633
|
- "waiting_approval": poll about every 1000ms and respond quickly to actions[].
|
|
3590
3634
|
- When status is "idle"/"error"/"cancelled": stop polling, the session is done.
|
|
3635
|
+
- Adapt interval based on task complexity and whether the previous poll returned new events.
|
|
3591
3636
|
|
|
3592
3637
|
poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
|
|
3593
3638
|
|
|
@@ -3685,6 +3730,625 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
|
|
|
3685
3730
|
return server;
|
|
3686
3731
|
}
|
|
3687
3732
|
|
|
3733
|
+
// src/app-server/detect.ts
|
|
3734
|
+
import { spawn as spawn2 } from "child_process";
|
|
3735
|
+
var DETECTION_TIMEOUT_MS = 5e3;
|
|
3736
|
+
async function detectClientMode(codexCommand, codexIsPath = false, env = process.env) {
|
|
3737
|
+
const override = env.CODEX_MCP_MODE;
|
|
3738
|
+
if (override === "app-server" || override === "exec") {
|
|
3739
|
+
return override;
|
|
3740
|
+
}
|
|
3741
|
+
try {
|
|
3742
|
+
const supported = await probeAppServer(codexCommand, codexIsPath, env);
|
|
3743
|
+
return supported ? "app-server" : "exec";
|
|
3744
|
+
} catch {
|
|
3745
|
+
return "exec";
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
function probeAppServer(codexCommand, codexIsPath, env) {
|
|
3749
|
+
return new Promise((resolve) => {
|
|
3750
|
+
const invocation = resolveCodexInvocation(["app-server", "--help"], {
|
|
3751
|
+
env,
|
|
3752
|
+
codexCommand,
|
|
3753
|
+
codexIsPath
|
|
3754
|
+
});
|
|
3755
|
+
let settled = false;
|
|
3756
|
+
const settle = (result) => {
|
|
3757
|
+
if (settled) return;
|
|
3758
|
+
settled = true;
|
|
3759
|
+
clearTimeout(timer);
|
|
3760
|
+
resolve(result);
|
|
3761
|
+
};
|
|
3762
|
+
let stdout = "";
|
|
3763
|
+
let stderr = "";
|
|
3764
|
+
const proc = spawn2(invocation.cmd, invocation.args, {
|
|
3765
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3766
|
+
env,
|
|
3767
|
+
windowsHide: true
|
|
3768
|
+
});
|
|
3769
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3770
|
+
stdout += chunk.toString();
|
|
3771
|
+
});
|
|
3772
|
+
proc.stderr?.on("data", (chunk) => {
|
|
3773
|
+
stderr += chunk.toString();
|
|
3774
|
+
});
|
|
3775
|
+
proc.on("error", () => {
|
|
3776
|
+
settle(false);
|
|
3777
|
+
});
|
|
3778
|
+
proc.on("exit", (code) => {
|
|
3779
|
+
if (code === 0) {
|
|
3780
|
+
settle(true);
|
|
3781
|
+
} else {
|
|
3782
|
+
const combined = (stdout + stderr).toLowerCase();
|
|
3783
|
+
const isUnknown = combined.includes("unknown") || combined.includes("unrecognized") || combined.includes("not found") || combined.includes("no such subcommand");
|
|
3784
|
+
settle(!isUnknown && combined.includes("app-server"));
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
const timer = setTimeout(() => {
|
|
3788
|
+
try {
|
|
3789
|
+
proc.kill("SIGTERM");
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
const forceKill = setTimeout(() => {
|
|
3793
|
+
try {
|
|
3794
|
+
if (!proc.killed && proc.exitCode === null) {
|
|
3795
|
+
proc.kill("SIGKILL");
|
|
3796
|
+
}
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
}, 2e3);
|
|
3800
|
+
if (forceKill.unref) forceKill.unref();
|
|
3801
|
+
settle(false);
|
|
3802
|
+
}, DETECTION_TIMEOUT_MS);
|
|
3803
|
+
if (timer.unref) timer.unref();
|
|
3804
|
+
});
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
// src/app-server/exec-client.ts
|
|
3808
|
+
import { spawn as spawn3 } from "child_process";
|
|
3809
|
+
import { writeFileSync, mkdtempSync, rmSync } from "fs";
|
|
3810
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3811
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3812
|
+
import { tmpdir } from "os";
|
|
3813
|
+
import { join } from "path";
|
|
3814
|
+
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
3815
|
+
var FORCE_KILL_TIMEOUT_MS = 5e3;
|
|
3816
|
+
function camelCaseItemType(snakeType) {
|
|
3817
|
+
return snakeType.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
3818
|
+
}
|
|
3819
|
+
function transformItem(item) {
|
|
3820
|
+
const result = { ...item };
|
|
3821
|
+
if (typeof result.type === "string") {
|
|
3822
|
+
result.type = camelCaseItemType(result.type);
|
|
3823
|
+
}
|
|
3824
|
+
return result;
|
|
3825
|
+
}
|
|
3826
|
+
function isRetryableError(event) {
|
|
3827
|
+
const msg = typeof event.message === "string" ? event.message : "";
|
|
3828
|
+
return /reconnect/i.test(msg) || /\d+\/\d+/.test(msg);
|
|
3829
|
+
}
|
|
3830
|
+
function sandboxPolicyToMode(policy) {
|
|
3831
|
+
switch (policy.type) {
|
|
3832
|
+
case "readOnly":
|
|
3833
|
+
return "read-only";
|
|
3834
|
+
case "workspaceWrite":
|
|
3835
|
+
return "workspace-write";
|
|
3836
|
+
case "dangerFullAccess":
|
|
3837
|
+
return "danger-full-access";
|
|
3838
|
+
case "externalSandbox":
|
|
3839
|
+
console.error(
|
|
3840
|
+
`[exec-client] SandboxPolicy type "externalSandbox" cannot be mapped to exec -s flag; using thread-level sandbox`
|
|
3841
|
+
);
|
|
3842
|
+
return void 0;
|
|
3843
|
+
default:
|
|
3844
|
+
return void 0;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
var EXEC_EVENT_TO_METHOD = {
|
|
3848
|
+
// Agent message deltas
|
|
3849
|
+
agent_message_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3850
|
+
agent_message_content_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3851
|
+
// Command execution
|
|
3852
|
+
exec_command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3853
|
+
command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3854
|
+
terminal_interaction: Methods.COMMAND_TERMINAL_INTERACTION,
|
|
3855
|
+
// File changes
|
|
3856
|
+
file_change_output_delta: Methods.FILE_CHANGE_OUTPUT_DELTA,
|
|
3857
|
+
// Reasoning
|
|
3858
|
+
reasoning_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3859
|
+
reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3860
|
+
agent_reasoning_delta: Methods.REASONING_TEXT_DELTA,
|
|
3861
|
+
agent_reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3862
|
+
reasoning_summary_delta: Methods.REASONING_SUMMARY_DELTA,
|
|
3863
|
+
agent_reasoning_section_break: Methods.REASONING_SUMMARY_PART_ADDED,
|
|
3864
|
+
// Plan
|
|
3865
|
+
plan_delta: Methods.PLAN_DELTA,
|
|
3866
|
+
plan_update: Methods.TURN_PLAN_UPDATED,
|
|
3867
|
+
// Turn-level
|
|
3868
|
+
turn_diff: Methods.TURN_DIFF_UPDATED,
|
|
3869
|
+
diff_update: Methods.TURN_DIFF_UPDATED,
|
|
3870
|
+
// MCP
|
|
3871
|
+
mcp_tool_call_begin: Methods.MCP_TOOL_PROGRESS,
|
|
3872
|
+
mcp_tool_call_end: Methods.MCP_TOOL_PROGRESS,
|
|
3873
|
+
mcp_startup_update: Methods.MCP_TOOL_PROGRESS,
|
|
3874
|
+
mcp_startup_complete: Methods.MCP_TOOL_PROGRESS,
|
|
3875
|
+
// Model routing
|
|
3876
|
+
model_reroute: Methods.MODEL_REROUTED,
|
|
3877
|
+
// Thread/session events
|
|
3878
|
+
thread_name_updated: Methods.THREAD_NAME_UPDATED,
|
|
3879
|
+
token_count: Methods.THREAD_TOKEN_USAGE_UPDATED,
|
|
3880
|
+
session_configured: Methods.SESSION_CONFIGURED,
|
|
3881
|
+
// Item lifecycle (in case exec emits these outside the dot-notation variants)
|
|
3882
|
+
item_started: Methods.ITEM_STARTED,
|
|
3883
|
+
item_completed: Methods.ITEM_COMPLETED,
|
|
3884
|
+
raw_response_item: Methods.RAW_RESPONSE_ITEM_COMPLETED,
|
|
3885
|
+
// Stream errors — map to error method so retryable detection can handle it
|
|
3886
|
+
stream_error: Methods.ERROR,
|
|
3887
|
+
// Legacy turn lifecycle (v1 wire format used by older CLIs)
|
|
3888
|
+
// These are critical for exec fallback since it targets CLIs without app-server.
|
|
3889
|
+
task_started: Methods.TURN_STARTED,
|
|
3890
|
+
task_complete: Methods.TURN_COMPLETED,
|
|
3891
|
+
turn_aborted: Methods.TURN_COMPLETED
|
|
3892
|
+
};
|
|
3893
|
+
var ExecClient = class extends EventEmitter2 {
|
|
3894
|
+
_destroyed = false;
|
|
3895
|
+
process = null;
|
|
3896
|
+
spawnOpts = null;
|
|
3897
|
+
// Thread/turn state
|
|
3898
|
+
threadId = null;
|
|
3899
|
+
/** Real thread ID from CLI (received via thread.started event). Used for exec resume. */
|
|
3900
|
+
realThreadId = null;
|
|
3901
|
+
turnId = null;
|
|
3902
|
+
turnCount = 0;
|
|
3903
|
+
threadStartParams = null;
|
|
3904
|
+
lastAgentMessageText = "";
|
|
3905
|
+
turnCompleted = false;
|
|
3906
|
+
schemaTmpDirs = [];
|
|
3907
|
+
// Handlers
|
|
3908
|
+
notificationHandler = null;
|
|
3909
|
+
serverRequestHandler = null;
|
|
3910
|
+
// Stdout buffer for JSONL parsing
|
|
3911
|
+
buffer = "";
|
|
3912
|
+
decoder = new StringDecoder2("utf8");
|
|
3913
|
+
get destroyed() {
|
|
3914
|
+
return this._destroyed;
|
|
3915
|
+
}
|
|
3916
|
+
get supportsTurnOverrides() {
|
|
3917
|
+
return this.turnCount <= 1 || this.realThreadId == null;
|
|
3918
|
+
}
|
|
3919
|
+
async start(opts) {
|
|
3920
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3921
|
+
this.spawnOpts = opts;
|
|
3922
|
+
return { userAgent: "codex-exec" };
|
|
3923
|
+
}
|
|
3924
|
+
async threadStart(params) {
|
|
3925
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3926
|
+
this.threadStartParams = params;
|
|
3927
|
+
this.threadId = `exec_thread_${randomUUID2().slice(0, 12)}`;
|
|
3928
|
+
return { thread: { id: this.threadId } };
|
|
3929
|
+
}
|
|
3930
|
+
async threadFork(_params) {
|
|
3931
|
+
throw new Error(
|
|
3932
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadFork is not supported in exec mode`
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
async threadResume(_params) {
|
|
3936
|
+
throw new Error(
|
|
3937
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadResume is not supported in exec mode`
|
|
3938
|
+
);
|
|
3939
|
+
}
|
|
3940
|
+
async threadBackgroundTerminalsClean(_params) {
|
|
3941
|
+
return {};
|
|
3942
|
+
}
|
|
3943
|
+
async turnStart(params) {
|
|
3944
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3945
|
+
if (!this.threadId) throw new Error("No thread started");
|
|
3946
|
+
this.killProcess();
|
|
3947
|
+
this.turnCount++;
|
|
3948
|
+
this.turnId = `exec_turn_${randomUUID2().slice(0, 12)}`;
|
|
3949
|
+
this.lastAgentMessageText = "";
|
|
3950
|
+
this.turnCompleted = false;
|
|
3951
|
+
this.buffer = "";
|
|
3952
|
+
this.decoder = new StringDecoder2("utf8");
|
|
3953
|
+
const prompt = params.input.filter((i) => i.type === "text").map((i) => i.text).join("\n");
|
|
3954
|
+
const images = params.input.filter((i) => i.type === "localImage").map((i) => i.path);
|
|
3955
|
+
const isResume = this.turnCount > 1 && this.realThreadId != null;
|
|
3956
|
+
if (this.turnCount > 1 && !this.realThreadId) {
|
|
3957
|
+
console.error(
|
|
3958
|
+
"[exec-client] No realThreadId available for resume; falling back to fresh exec (context will be lost)"
|
|
3959
|
+
);
|
|
3960
|
+
this.emitNotification(Methods.ERROR, {
|
|
3961
|
+
threadId: this.threadId,
|
|
3962
|
+
turnId: this.turnId,
|
|
3963
|
+
error: "exec mode: multi-turn context unavailable (CLI did not provide thread ID). This turn runs without prior context.",
|
|
3964
|
+
willRetry: true
|
|
3965
|
+
// non-terminal: session continues, just without context
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
const args = isResume ? this.buildResumeArgs(prompt, params, images) : this.buildExecArgs(prompt, params, images);
|
|
3969
|
+
const executable = getDefaultCodexExecutable();
|
|
3970
|
+
const invocation = resolveCodexInvocation(args, {
|
|
3971
|
+
codexCommand: executable.command,
|
|
3972
|
+
codexIsPath: executable.isPath
|
|
3973
|
+
});
|
|
3974
|
+
const proc = spawn3(invocation.cmd, invocation.args, {
|
|
3975
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3976
|
+
env: { ...process.env },
|
|
3977
|
+
detached: process.platform !== "win32",
|
|
3978
|
+
windowsHide: process.platform === "win32"
|
|
3979
|
+
});
|
|
3980
|
+
this.process = proc;
|
|
3981
|
+
proc.stdin?.end();
|
|
3982
|
+
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
3983
|
+
proc.stderr.on("data", (chunk) => {
|
|
3984
|
+
console.error(`[exec-client stderr] ${chunk.toString().trimEnd()}`);
|
|
3985
|
+
});
|
|
3986
|
+
proc.on("error", (err) => {
|
|
3987
|
+
if (!this._destroyed) {
|
|
3988
|
+
this.emit("error", err);
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
proc.on("exit", (code, signal) => {
|
|
3992
|
+
if (this.turnId && !this._destroyed && !this.turnCompleted) {
|
|
3993
|
+
this.turnCompleted = true;
|
|
3994
|
+
if (code !== 0 && code !== null) {
|
|
3995
|
+
this.emitNotification(Methods.ERROR, {
|
|
3996
|
+
threadId: this.threadId,
|
|
3997
|
+
turnId: this.turnId,
|
|
3998
|
+
error: { message: `exec process exited with code ${code}` },
|
|
3999
|
+
willRetry: false
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
const turnId2 = this.turnId ?? "";
|
|
4003
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4004
|
+
threadId: this.threadId,
|
|
4005
|
+
turn: {
|
|
4006
|
+
id: turnId2,
|
|
4007
|
+
status: code === 0 ? "completed" : "failed",
|
|
4008
|
+
output: this.lastAgentMessageText || void 0,
|
|
4009
|
+
...code !== 0 && code !== null ? { error: { message: `exec process exited with code ${code}` } } : signal ? { error: { message: `exec process killed by signal ${signal}` } } : {}
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
this.turnId = null;
|
|
4013
|
+
}
|
|
4014
|
+
if (!this._destroyed) {
|
|
4015
|
+
this.emit("exit", code, signal);
|
|
4016
|
+
}
|
|
4017
|
+
this.process = null;
|
|
4018
|
+
});
|
|
4019
|
+
const turnId = this.turnId;
|
|
4020
|
+
return { turn: { id: turnId } };
|
|
4021
|
+
}
|
|
4022
|
+
async turnInterrupt(_params) {
|
|
4023
|
+
this.killProcess();
|
|
4024
|
+
}
|
|
4025
|
+
onNotification(handler) {
|
|
4026
|
+
this.notificationHandler = handler;
|
|
4027
|
+
}
|
|
4028
|
+
onServerRequest(handler) {
|
|
4029
|
+
this.serverRequestHandler = handler;
|
|
4030
|
+
}
|
|
4031
|
+
respondToServer(_id, _result) {
|
|
4032
|
+
}
|
|
4033
|
+
respondErrorToServer(_id, _code, _message) {
|
|
4034
|
+
}
|
|
4035
|
+
async destroy() {
|
|
4036
|
+
if (this._destroyed) return;
|
|
4037
|
+
this._destroyed = true;
|
|
4038
|
+
const proc = this.process;
|
|
4039
|
+
if (proc && !proc.killed) {
|
|
4040
|
+
const alreadyExited = proc.exitCode !== null;
|
|
4041
|
+
proc.stdin?.end();
|
|
4042
|
+
this.killProcess();
|
|
4043
|
+
const forceKill = setTimeout(() => {
|
|
4044
|
+
if (proc && !proc.killed && proc.exitCode === null) {
|
|
4045
|
+
if (process.platform === "win32" && proc.pid) {
|
|
4046
|
+
try {
|
|
4047
|
+
spawn3("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
|
|
4048
|
+
stdio: "ignore",
|
|
4049
|
+
windowsHide: true
|
|
4050
|
+
});
|
|
4051
|
+
} catch {
|
|
4052
|
+
}
|
|
4053
|
+
} else {
|
|
4054
|
+
try {
|
|
4055
|
+
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
4056
|
+
else proc.kill("SIGKILL");
|
|
4057
|
+
} catch {
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}, FORCE_KILL_TIMEOUT_MS);
|
|
4062
|
+
forceKill.unref();
|
|
4063
|
+
if (!alreadyExited) {
|
|
4064
|
+
await new Promise((resolve) => {
|
|
4065
|
+
proc.on("exit", () => {
|
|
4066
|
+
clearTimeout(forceKill);
|
|
4067
|
+
resolve();
|
|
4068
|
+
});
|
|
4069
|
+
const fallback = setTimeout(resolve, FORCE_KILL_TIMEOUT_MS + 1e3);
|
|
4070
|
+
fallback.unref();
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
this.process = null;
|
|
4075
|
+
this.removeAllListeners();
|
|
4076
|
+
for (const dir of this.schemaTmpDirs) {
|
|
4077
|
+
try {
|
|
4078
|
+
rmSync(dir, { recursive: true, force: true });
|
|
4079
|
+
} catch {
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
this.schemaTmpDirs = [];
|
|
4083
|
+
}
|
|
4084
|
+
// ── Private helpers ─────────────────────────────────────────────
|
|
4085
|
+
/**
|
|
4086
|
+
* Build args for the first turn: `codex exec "<prompt>" --json --skip-git-repo-check [flags]`.
|
|
4087
|
+
* No --ephemeral so the session persists for subsequent resume turns.
|
|
4088
|
+
*/
|
|
4089
|
+
buildExecArgs(prompt, params, images) {
|
|
4090
|
+
const args = ["exec", prompt, "--json", "--skip-git-repo-check"];
|
|
4091
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4092
|
+
if (model) args.push("-m", model);
|
|
4093
|
+
let effectiveSandbox;
|
|
4094
|
+
if (params.sandboxPolicy) {
|
|
4095
|
+
effectiveSandbox = sandboxPolicyToMode(params.sandboxPolicy);
|
|
4096
|
+
}
|
|
4097
|
+
if (!effectiveSandbox) {
|
|
4098
|
+
effectiveSandbox = this.threadStartParams?.sandbox ?? this.spawnOpts?.sandbox;
|
|
4099
|
+
}
|
|
4100
|
+
if (effectiveSandbox) args.push("-s", effectiveSandbox);
|
|
4101
|
+
if (this.spawnOpts?.profile) args.push("-p", this.spawnOpts.profile);
|
|
4102
|
+
const cwd = params.cwd ?? this.threadStartParams?.cwd;
|
|
4103
|
+
if (cwd) args.push("-C", cwd);
|
|
4104
|
+
for (const img of images) args.push("-i", img);
|
|
4105
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4106
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4107
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4108
|
+
try {
|
|
4109
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "codex-mcp-schema-"));
|
|
4110
|
+
this.schemaTmpDirs.push(tmpDir);
|
|
4111
|
+
const schemaPath = join(tmpDir, "output-schema.json");
|
|
4112
|
+
writeFileSync(schemaPath, JSON.stringify(params.outputSchema));
|
|
4113
|
+
args.push("--output-schema", schemaPath);
|
|
4114
|
+
} catch (err) {
|
|
4115
|
+
console.error(
|
|
4116
|
+
`[exec-client] Failed to write output schema to temp file: ${err instanceof Error ? err.message : String(err)}`
|
|
4117
|
+
);
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
const configs = {
|
|
4121
|
+
...this.spawnOpts?.config,
|
|
4122
|
+
...this.threadStartParams?.config
|
|
4123
|
+
};
|
|
4124
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4125
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4126
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4127
|
+
}
|
|
4128
|
+
return args;
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Build args for subsequent turns: `codex exec resume <threadId> "<prompt>" --json [flags]`.
|
|
4132
|
+
* Resumes the persisted session for multi-turn context continuity.
|
|
4133
|
+
* Note: exec resume only supports -m, -c, -i, --json, --skip-git-repo-check.
|
|
4134
|
+
* -s, -p, -C are NOT supported and inherit from the first turn's session.
|
|
4135
|
+
*/
|
|
4136
|
+
buildResumeArgs(prompt, params, images) {
|
|
4137
|
+
const args = [
|
|
4138
|
+
"exec",
|
|
4139
|
+
"resume",
|
|
4140
|
+
this.realThreadId,
|
|
4141
|
+
prompt,
|
|
4142
|
+
"--json",
|
|
4143
|
+
"--skip-git-repo-check"
|
|
4144
|
+
];
|
|
4145
|
+
if (params.sandboxPolicy) {
|
|
4146
|
+
console.error(
|
|
4147
|
+
"[exec-client] sandbox override ignored in resume mode (exec resume does not support -s)"
|
|
4148
|
+
);
|
|
4149
|
+
}
|
|
4150
|
+
if (params.cwd) {
|
|
4151
|
+
console.error(
|
|
4152
|
+
"[exec-client] cwd override ignored in resume mode (exec resume does not support -C)"
|
|
4153
|
+
);
|
|
4154
|
+
}
|
|
4155
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4156
|
+
console.error(
|
|
4157
|
+
"[exec-client] outputSchema ignored in resume mode (exec resume does not support --output-schema)"
|
|
4158
|
+
);
|
|
4159
|
+
}
|
|
4160
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4161
|
+
if (model) args.push("-m", model);
|
|
4162
|
+
for (const img of images) args.push("-i", img);
|
|
4163
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4164
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4165
|
+
const configs = {
|
|
4166
|
+
...this.spawnOpts?.config,
|
|
4167
|
+
...this.threadStartParams?.config
|
|
4168
|
+
};
|
|
4169
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4170
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4171
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4172
|
+
}
|
|
4173
|
+
return args;
|
|
4174
|
+
}
|
|
4175
|
+
onData(chunk) {
|
|
4176
|
+
this.buffer += this.decoder.write(chunk);
|
|
4177
|
+
const lines = this.buffer.split("\n");
|
|
4178
|
+
this.buffer = lines.pop() ?? "";
|
|
4179
|
+
for (const line of lines) {
|
|
4180
|
+
const trimmed = line.trim();
|
|
4181
|
+
if (!trimmed || trimmed[0] !== "{") continue;
|
|
4182
|
+
try {
|
|
4183
|
+
const event = JSON.parse(trimmed);
|
|
4184
|
+
this.handleExecEvent(event);
|
|
4185
|
+
} catch {
|
|
4186
|
+
console.error(`[exec-client] Failed to parse JSONL: ${trimmed.slice(0, 200)}`);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* Transform exec JSONL event into app-server notification and dispatch.
|
|
4192
|
+
*/
|
|
4193
|
+
handleExecEvent(event) {
|
|
4194
|
+
const type = event.type;
|
|
4195
|
+
switch (type) {
|
|
4196
|
+
case "thread.started": {
|
|
4197
|
+
const cliThreadId = event.thread_id ?? event.threadId;
|
|
4198
|
+
if (cliThreadId) {
|
|
4199
|
+
this.threadId = cliThreadId;
|
|
4200
|
+
this.realThreadId = cliThreadId;
|
|
4201
|
+
}
|
|
4202
|
+
this.emitNotification(Methods.THREAD_STARTED, {
|
|
4203
|
+
thread: { id: this.threadId }
|
|
4204
|
+
});
|
|
4205
|
+
return;
|
|
4206
|
+
}
|
|
4207
|
+
case "turn.started":
|
|
4208
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4209
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4210
|
+
});
|
|
4211
|
+
return;
|
|
4212
|
+
case "item.started": {
|
|
4213
|
+
const item = event.item;
|
|
4214
|
+
if (item) {
|
|
4215
|
+
this.emitNotification(Methods.ITEM_STARTED, {
|
|
4216
|
+
threadId: this.threadId,
|
|
4217
|
+
turnId: this.turnId,
|
|
4218
|
+
item: transformItem(item)
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
case "item.completed": {
|
|
4224
|
+
const item = event.item;
|
|
4225
|
+
if (item) {
|
|
4226
|
+
const transformed = transformItem(item);
|
|
4227
|
+
if (transformed.type === "agentMessage" && typeof transformed.text === "string") {
|
|
4228
|
+
this.lastAgentMessageText = transformed.text;
|
|
4229
|
+
}
|
|
4230
|
+
this.emitNotification(Methods.ITEM_COMPLETED, {
|
|
4231
|
+
threadId: this.threadId,
|
|
4232
|
+
turnId: this.turnId,
|
|
4233
|
+
item: transformed
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
case "turn.completed": {
|
|
4239
|
+
const turnId = this.turnId ?? "";
|
|
4240
|
+
this.turnCompleted = true;
|
|
4241
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4242
|
+
threadId: this.threadId,
|
|
4243
|
+
turn: {
|
|
4244
|
+
id: turnId,
|
|
4245
|
+
status: "completed",
|
|
4246
|
+
output: this.lastAgentMessageText || void 0,
|
|
4247
|
+
usage: event.usage
|
|
4248
|
+
}
|
|
4249
|
+
});
|
|
4250
|
+
this.turnId = null;
|
|
4251
|
+
return;
|
|
4252
|
+
}
|
|
4253
|
+
case "turn.failed": {
|
|
4254
|
+
const turnId = this.turnId ?? "";
|
|
4255
|
+
const error = event.error;
|
|
4256
|
+
this.turnCompleted = true;
|
|
4257
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4258
|
+
threadId: this.threadId,
|
|
4259
|
+
turn: {
|
|
4260
|
+
id: turnId,
|
|
4261
|
+
status: "failed",
|
|
4262
|
+
error: error ?? { message: "Turn failed" }
|
|
4263
|
+
}
|
|
4264
|
+
});
|
|
4265
|
+
this.turnId = null;
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
case "error": {
|
|
4269
|
+
const willRetry = isRetryableError(event);
|
|
4270
|
+
this.emitNotification(Methods.ERROR, {
|
|
4271
|
+
threadId: this.threadId,
|
|
4272
|
+
turnId: this.turnId,
|
|
4273
|
+
error: event.message ?? event.error,
|
|
4274
|
+
willRetry
|
|
4275
|
+
});
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
default:
|
|
4279
|
+
break;
|
|
4280
|
+
}
|
|
4281
|
+
const mappedMethod = EXEC_EVENT_TO_METHOD[type];
|
|
4282
|
+
if (mappedMethod) {
|
|
4283
|
+
if (type === "task_started") {
|
|
4284
|
+
const turnId = event.turn_id ?? this.turnId;
|
|
4285
|
+
if (turnId) this.turnId = turnId;
|
|
4286
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4287
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4288
|
+
});
|
|
4289
|
+
} else if (type === "task_complete") {
|
|
4290
|
+
const turnId = this.turnId ?? "";
|
|
4291
|
+
this.turnCompleted = true;
|
|
4292
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4293
|
+
threadId: this.threadId,
|
|
4294
|
+
turn: {
|
|
4295
|
+
id: turnId,
|
|
4296
|
+
status: "completed",
|
|
4297
|
+
output: this.lastAgentMessageText || void 0
|
|
4298
|
+
}
|
|
4299
|
+
});
|
|
4300
|
+
this.turnId = null;
|
|
4301
|
+
} else if (type === "turn_aborted") {
|
|
4302
|
+
const turnId = this.turnId ?? "";
|
|
4303
|
+
this.turnCompleted = true;
|
|
4304
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4305
|
+
threadId: this.threadId,
|
|
4306
|
+
turn: {
|
|
4307
|
+
id: turnId,
|
|
4308
|
+
status: "cancelled",
|
|
4309
|
+
error: event.reason ?? { message: "Turn aborted" }
|
|
4310
|
+
}
|
|
4311
|
+
});
|
|
4312
|
+
this.turnId = null;
|
|
4313
|
+
} else if (mappedMethod === Methods.ERROR) {
|
|
4314
|
+
this.emitNotification(Methods.ERROR, {
|
|
4315
|
+
threadId: this.threadId,
|
|
4316
|
+
turnId: this.turnId,
|
|
4317
|
+
error: event.message ?? event.error ?? type,
|
|
4318
|
+
willRetry: isRetryableError(event)
|
|
4319
|
+
});
|
|
4320
|
+
} else {
|
|
4321
|
+
this.emitNotification(mappedMethod, {
|
|
4322
|
+
threadId: this.threadId,
|
|
4323
|
+
turnId: this.turnId,
|
|
4324
|
+
...event
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
console.error(`[exec-client] Unmapped exec event type: ${type}`);
|
|
4330
|
+
}
|
|
4331
|
+
emitNotification(method, params) {
|
|
4332
|
+
if (this.notificationHandler) {
|
|
4333
|
+
this.notificationHandler(method, params);
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
killProcess() {
|
|
4337
|
+
if (!this.process || this.process.killed) return;
|
|
4338
|
+
if (process.platform !== "win32" && this.process.pid) {
|
|
4339
|
+
try {
|
|
4340
|
+
process.kill(-this.process.pid, "SIGTERM");
|
|
4341
|
+
return;
|
|
4342
|
+
} catch {
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
try {
|
|
4346
|
+
this.process.kill("SIGTERM");
|
|
4347
|
+
} catch {
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
};
|
|
4351
|
+
|
|
3688
4352
|
// src/index.ts
|
|
3689
4353
|
async function main() {
|
|
3690
4354
|
const preflight = runStdioPreflight();
|
|
@@ -3706,8 +4370,15 @@ async function main() {
|
|
|
3706
4370
|
);
|
|
3707
4371
|
}
|
|
3708
4372
|
checkDefaultCodexExecutableAvailability();
|
|
4373
|
+
const executable = getDefaultCodexExecutable();
|
|
4374
|
+
const clientMode = await detectClientMode(executable.command, executable.isPath);
|
|
4375
|
+
console.error(`[codex-mcp] client mode: ${clientMode} (binary: ${executable.command})`);
|
|
4376
|
+
const createClient = () => clientMode === "exec" ? new ExecClient() : new AppServerClient();
|
|
3709
4377
|
const serverCwd = process.cwd();
|
|
3710
|
-
const server = createServer(serverCwd
|
|
4378
|
+
const server = createServer(serverCwd, {
|
|
4379
|
+
createClient,
|
|
4380
|
+
clientMode
|
|
4381
|
+
});
|
|
3711
4382
|
const transport = new StdioServerTransport();
|
|
3712
4383
|
let closing = false;
|
|
3713
4384
|
const shutdown = async () => {
|