@leo000001/codex-mcp 2.1.1 → 2.1.3
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 +642 -22
- 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.3" : "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
|
*/
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -2808,6 +2817,7 @@ var ERROR_CODE_HINTS = {
|
|
|
2808
2817
|
["THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */]: "Forked thread could not resume in new process. Retry fork from current source session.",
|
|
2809
2818
|
["PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */]: "Non-JSON or malformed app-server line. Check shell/profile noise and transport health.",
|
|
2810
2819
|
["WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */]: "stdin backpressure overflow. Reduce burst size and re-run in smaller turns.",
|
|
2820
|
+
["EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */]: "Operation not supported in exec mode. Features like threadFork and threadResume require app-server mode.",
|
|
2811
2821
|
["INTERNAL" /* INTERNAL */]: "Unexpected server-side failure. Inspect logs and retry safely."
|
|
2812
2822
|
};
|
|
2813
2823
|
function asTextResource(uri, text, mimeType) {
|
|
@@ -2917,6 +2927,7 @@ function buildGotchasText() {
|
|
|
2917
2927
|
"- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
|
|
2918
2928
|
"- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
|
|
2919
2929
|
"- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
|
|
2930
|
+
"- **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
2931
|
"",
|
|
2921
2932
|
"## Approval behavior",
|
|
2922
2933
|
"",
|
|
@@ -2950,6 +2961,13 @@ function buildGotchasText() {
|
|
|
2950
2961
|
"",
|
|
2951
2962
|
"- codex-mcp does not hard-code a strict concurrent-session cap.",
|
|
2952
2963
|
"- Practical limit depends on machine resources and child-process load.",
|
|
2964
|
+
"",
|
|
2965
|
+
"## Exec fallback mode",
|
|
2966
|
+
"",
|
|
2967
|
+
"- When the codex binary does not support `app-server`, codex-mcp falls back to `exec` mode (`codex exec --json`).",
|
|
2968
|
+
"- Check `codex-mcp:///server-info` `clientMode` field to detect which mode is active.",
|
|
2969
|
+
"- **Exec mode supports multi-turn**: first turn uses `codex exec`, subsequent turns use `codex exec resume <threadId>` for context continuity.",
|
|
2970
|
+
"- **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
2971
|
""
|
|
2954
2972
|
].join("\n");
|
|
2955
2973
|
}
|
|
@@ -3130,6 +3148,7 @@ function registerResources(server, deps) {
|
|
|
3130
3148
|
name: "codex-mcp",
|
|
3131
3149
|
version: deps.version,
|
|
3132
3150
|
codexCliVersion: getCodexCliVersion(),
|
|
3151
|
+
clientMode: deps.clientMode ?? "app-server",
|
|
3133
3152
|
node: process.version,
|
|
3134
3153
|
platform: process.platform,
|
|
3135
3154
|
arch: process.arch,
|
|
@@ -3221,7 +3240,7 @@ function registerResources(server, deps) {
|
|
|
3221
3240
|
}
|
|
3222
3241
|
|
|
3223
3242
|
// src/server.ts
|
|
3224
|
-
var SERVER_VERSION = true ? "2.1.
|
|
3243
|
+
var SERVER_VERSION = true ? "2.1.3" : "0.0.0-dev";
|
|
3225
3244
|
function formatErrorMessage(err) {
|
|
3226
3245
|
const message = err instanceof Error ? err.message : String(err);
|
|
3227
3246
|
const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
|
|
@@ -3240,13 +3259,17 @@ function toStructuredContent(value) {
|
|
|
3240
3259
|
}
|
|
3241
3260
|
return { value };
|
|
3242
3261
|
}
|
|
3243
|
-
function createServer(serverCwd) {
|
|
3244
|
-
const sessionManager = new SessionManager();
|
|
3262
|
+
function createServer(serverCwd, options) {
|
|
3263
|
+
const sessionManager = new SessionManager(options);
|
|
3245
3264
|
const server = new McpServer({
|
|
3246
3265
|
name: "codex-mcp",
|
|
3247
3266
|
version: SERVER_VERSION
|
|
3248
3267
|
});
|
|
3249
|
-
registerResources(server, {
|
|
3268
|
+
registerResources(server, {
|
|
3269
|
+
version: SERVER_VERSION,
|
|
3270
|
+
sessionManager,
|
|
3271
|
+
clientMode: options?.clientMode
|
|
3272
|
+
});
|
|
3250
3273
|
const publicSessionInfoSchema = z.object({
|
|
3251
3274
|
sessionId: z.string(),
|
|
3252
3275
|
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]),
|
|
@@ -3585,9 +3608,10 @@ function createServer(serverCwd) {
|
|
|
3585
3608
|
|
|
3586
3609
|
POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
|
|
3587
3610
|
- Treat pollInterval as a minimum hint, not a fixed schedule.
|
|
3588
|
-
- "running":
|
|
3611
|
+
- "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
3612
|
- "waiting_approval": poll about every 1000ms and respond quickly to actions[].
|
|
3590
3613
|
- When status is "idle"/"error"/"cancelled": stop polling, the session is done.
|
|
3614
|
+
- Adapt interval based on task complexity and whether the previous poll returned new events.
|
|
3591
3615
|
|
|
3592
3616
|
poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
|
|
3593
3617
|
|
|
@@ -3685,6 +3709,595 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
|
|
|
3685
3709
|
return server;
|
|
3686
3710
|
}
|
|
3687
3711
|
|
|
3712
|
+
// src/app-server/detect.ts
|
|
3713
|
+
import { spawn as spawn2 } from "child_process";
|
|
3714
|
+
var DETECTION_TIMEOUT_MS = 5e3;
|
|
3715
|
+
async function detectClientMode(codexCommand, codexIsPath = false, env = process.env) {
|
|
3716
|
+
const override = env.CODEX_MCP_MODE;
|
|
3717
|
+
if (override === "app-server" || override === "exec") {
|
|
3718
|
+
return override;
|
|
3719
|
+
}
|
|
3720
|
+
try {
|
|
3721
|
+
const supported = await probeAppServer(codexCommand, codexIsPath, env);
|
|
3722
|
+
return supported ? "app-server" : "exec";
|
|
3723
|
+
} catch {
|
|
3724
|
+
return "exec";
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
function probeAppServer(codexCommand, codexIsPath, env) {
|
|
3728
|
+
return new Promise((resolve) => {
|
|
3729
|
+
const invocation = resolveCodexInvocation(["app-server", "--help"], {
|
|
3730
|
+
env,
|
|
3731
|
+
codexCommand,
|
|
3732
|
+
codexIsPath
|
|
3733
|
+
});
|
|
3734
|
+
let settled = false;
|
|
3735
|
+
const settle = (result) => {
|
|
3736
|
+
if (settled) return;
|
|
3737
|
+
settled = true;
|
|
3738
|
+
clearTimeout(timer);
|
|
3739
|
+
resolve(result);
|
|
3740
|
+
};
|
|
3741
|
+
let stdout = "";
|
|
3742
|
+
let stderr = "";
|
|
3743
|
+
const proc = spawn2(invocation.cmd, invocation.args, {
|
|
3744
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3745
|
+
env,
|
|
3746
|
+
windowsHide: true
|
|
3747
|
+
});
|
|
3748
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3749
|
+
stdout += chunk.toString();
|
|
3750
|
+
});
|
|
3751
|
+
proc.stderr?.on("data", (chunk) => {
|
|
3752
|
+
stderr += chunk.toString();
|
|
3753
|
+
});
|
|
3754
|
+
proc.on("error", () => {
|
|
3755
|
+
settle(false);
|
|
3756
|
+
});
|
|
3757
|
+
proc.on("exit", (code) => {
|
|
3758
|
+
if (code === 0) {
|
|
3759
|
+
settle(true);
|
|
3760
|
+
} else {
|
|
3761
|
+
const combined = (stdout + stderr).toLowerCase();
|
|
3762
|
+
const isUnknown = combined.includes("unknown") || combined.includes("unrecognized") || combined.includes("not found") || combined.includes("no such subcommand");
|
|
3763
|
+
settle(!isUnknown && combined.includes("app-server"));
|
|
3764
|
+
}
|
|
3765
|
+
});
|
|
3766
|
+
const timer = setTimeout(() => {
|
|
3767
|
+
try {
|
|
3768
|
+
proc.kill("SIGTERM");
|
|
3769
|
+
} catch {
|
|
3770
|
+
}
|
|
3771
|
+
settle(false);
|
|
3772
|
+
}, DETECTION_TIMEOUT_MS);
|
|
3773
|
+
if (timer.unref) timer.unref();
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/app-server/exec-client.ts
|
|
3778
|
+
import { spawn as spawn3 } from "child_process";
|
|
3779
|
+
import { writeFileSync, mkdtempSync } from "fs";
|
|
3780
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3781
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3782
|
+
import { tmpdir } from "os";
|
|
3783
|
+
import { join } from "path";
|
|
3784
|
+
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
3785
|
+
var FORCE_KILL_TIMEOUT_MS = 5e3;
|
|
3786
|
+
function camelCaseItemType(snakeType) {
|
|
3787
|
+
return snakeType.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
3788
|
+
}
|
|
3789
|
+
function transformItem(item) {
|
|
3790
|
+
const result = { ...item };
|
|
3791
|
+
if (typeof result.type === "string") {
|
|
3792
|
+
result.type = camelCaseItemType(result.type);
|
|
3793
|
+
}
|
|
3794
|
+
return result;
|
|
3795
|
+
}
|
|
3796
|
+
function isRetryableError(event) {
|
|
3797
|
+
const msg = typeof event.message === "string" ? event.message : "";
|
|
3798
|
+
return /reconnect/i.test(msg) || /\d+\/\d+/.test(msg);
|
|
3799
|
+
}
|
|
3800
|
+
function sandboxPolicyToMode(policy) {
|
|
3801
|
+
switch (policy.type) {
|
|
3802
|
+
case "readOnly":
|
|
3803
|
+
return "read-only";
|
|
3804
|
+
case "workspaceWrite":
|
|
3805
|
+
return "workspace-write";
|
|
3806
|
+
case "dangerFullAccess":
|
|
3807
|
+
return "danger-full-access";
|
|
3808
|
+
case "externalSandbox":
|
|
3809
|
+
console.error(
|
|
3810
|
+
`[exec-client] SandboxPolicy type "externalSandbox" cannot be mapped to exec -s flag; using thread-level sandbox`
|
|
3811
|
+
);
|
|
3812
|
+
return void 0;
|
|
3813
|
+
default:
|
|
3814
|
+
return void 0;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
var EXEC_EVENT_TO_METHOD = {
|
|
3818
|
+
// Agent message deltas
|
|
3819
|
+
agent_message_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3820
|
+
agent_message_content_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3821
|
+
// Command execution
|
|
3822
|
+
exec_command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3823
|
+
command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3824
|
+
terminal_interaction: Methods.COMMAND_TERMINAL_INTERACTION,
|
|
3825
|
+
// File changes
|
|
3826
|
+
file_change_output_delta: Methods.FILE_CHANGE_OUTPUT_DELTA,
|
|
3827
|
+
// Reasoning
|
|
3828
|
+
reasoning_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3829
|
+
reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3830
|
+
agent_reasoning_delta: Methods.REASONING_TEXT_DELTA,
|
|
3831
|
+
agent_reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3832
|
+
reasoning_summary_delta: Methods.REASONING_SUMMARY_DELTA,
|
|
3833
|
+
agent_reasoning_section_break: Methods.REASONING_SUMMARY_PART_ADDED,
|
|
3834
|
+
// Plan
|
|
3835
|
+
plan_delta: Methods.PLAN_DELTA,
|
|
3836
|
+
plan_update: Methods.TURN_PLAN_UPDATED,
|
|
3837
|
+
// Turn-level
|
|
3838
|
+
turn_diff: Methods.TURN_DIFF_UPDATED,
|
|
3839
|
+
diff_update: Methods.TURN_DIFF_UPDATED,
|
|
3840
|
+
// MCP
|
|
3841
|
+
mcp_tool_call_begin: Methods.MCP_TOOL_PROGRESS,
|
|
3842
|
+
mcp_tool_call_end: Methods.MCP_TOOL_PROGRESS,
|
|
3843
|
+
mcp_startup_update: Methods.MCP_TOOL_PROGRESS,
|
|
3844
|
+
mcp_startup_complete: Methods.MCP_TOOL_PROGRESS,
|
|
3845
|
+
// Model routing
|
|
3846
|
+
model_reroute: Methods.MODEL_REROUTED,
|
|
3847
|
+
// Thread/session events
|
|
3848
|
+
thread_name_updated: Methods.THREAD_NAME_UPDATED,
|
|
3849
|
+
token_count: Methods.THREAD_TOKEN_USAGE_UPDATED,
|
|
3850
|
+
session_configured: Methods.SESSION_CONFIGURED,
|
|
3851
|
+
// Item lifecycle (in case exec emits these outside the dot-notation variants)
|
|
3852
|
+
item_started: Methods.ITEM_STARTED,
|
|
3853
|
+
item_completed: Methods.ITEM_COMPLETED,
|
|
3854
|
+
raw_response_item: Methods.RAW_RESPONSE_ITEM_COMPLETED,
|
|
3855
|
+
// Stream errors — map to error method so retryable detection can handle it
|
|
3856
|
+
stream_error: Methods.ERROR,
|
|
3857
|
+
// Legacy turn lifecycle (v1 wire format used by older CLIs)
|
|
3858
|
+
// These are critical for exec fallback since it targets CLIs without app-server.
|
|
3859
|
+
task_started: Methods.TURN_STARTED,
|
|
3860
|
+
task_complete: Methods.TURN_COMPLETED,
|
|
3861
|
+
turn_aborted: Methods.TURN_COMPLETED
|
|
3862
|
+
};
|
|
3863
|
+
var ExecClient = class extends EventEmitter2 {
|
|
3864
|
+
_destroyed = false;
|
|
3865
|
+
process = null;
|
|
3866
|
+
spawnOpts = null;
|
|
3867
|
+
// Thread/turn state
|
|
3868
|
+
threadId = null;
|
|
3869
|
+
/** Real thread ID from CLI (received via thread.started event). Used for exec resume. */
|
|
3870
|
+
realThreadId = null;
|
|
3871
|
+
turnId = null;
|
|
3872
|
+
turnCount = 0;
|
|
3873
|
+
threadStartParams = null;
|
|
3874
|
+
lastAgentMessageText = "";
|
|
3875
|
+
turnCompleted = false;
|
|
3876
|
+
// Handlers
|
|
3877
|
+
notificationHandler = null;
|
|
3878
|
+
serverRequestHandler = null;
|
|
3879
|
+
// Stdout buffer for JSONL parsing
|
|
3880
|
+
buffer = "";
|
|
3881
|
+
decoder = new StringDecoder2("utf8");
|
|
3882
|
+
get destroyed() {
|
|
3883
|
+
return this._destroyed;
|
|
3884
|
+
}
|
|
3885
|
+
get supportsTurnOverrides() {
|
|
3886
|
+
return this.turnCount <= 1 || this.realThreadId == null;
|
|
3887
|
+
}
|
|
3888
|
+
async start(opts) {
|
|
3889
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3890
|
+
this.spawnOpts = opts;
|
|
3891
|
+
return { userAgent: "codex-exec" };
|
|
3892
|
+
}
|
|
3893
|
+
async threadStart(params) {
|
|
3894
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3895
|
+
this.threadStartParams = params;
|
|
3896
|
+
this.threadId = `exec_thread_${randomUUID2().slice(0, 12)}`;
|
|
3897
|
+
return { thread: { id: this.threadId } };
|
|
3898
|
+
}
|
|
3899
|
+
async threadFork(_params) {
|
|
3900
|
+
throw new Error(
|
|
3901
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadFork is not supported in exec mode`
|
|
3902
|
+
);
|
|
3903
|
+
}
|
|
3904
|
+
async threadResume(_params) {
|
|
3905
|
+
throw new Error(
|
|
3906
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadResume is not supported in exec mode`
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
async threadBackgroundTerminalsClean(_params) {
|
|
3910
|
+
return {};
|
|
3911
|
+
}
|
|
3912
|
+
async turnStart(params) {
|
|
3913
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3914
|
+
if (!this.threadId) throw new Error("No thread started");
|
|
3915
|
+
this.killProcess();
|
|
3916
|
+
this.turnCount++;
|
|
3917
|
+
this.turnId = `exec_turn_${randomUUID2().slice(0, 12)}`;
|
|
3918
|
+
this.lastAgentMessageText = "";
|
|
3919
|
+
this.turnCompleted = false;
|
|
3920
|
+
this.buffer = "";
|
|
3921
|
+
this.decoder = new StringDecoder2("utf8");
|
|
3922
|
+
const prompt = params.input.filter((i) => i.type === "text").map((i) => i.text).join("\n");
|
|
3923
|
+
const images = params.input.filter((i) => i.type === "localImage").map((i) => i.path);
|
|
3924
|
+
const isResume = this.turnCount > 1 && this.realThreadId != null;
|
|
3925
|
+
if (this.turnCount > 1 && !this.realThreadId) {
|
|
3926
|
+
console.error(
|
|
3927
|
+
"[exec-client] No realThreadId available for resume; falling back to fresh exec (context will be lost)"
|
|
3928
|
+
);
|
|
3929
|
+
this.emitNotification(Methods.ERROR, {
|
|
3930
|
+
threadId: this.threadId,
|
|
3931
|
+
turnId: this.turnId,
|
|
3932
|
+
error: "exec mode: multi-turn context unavailable (CLI did not provide thread ID). This turn runs without prior context.",
|
|
3933
|
+
willRetry: true
|
|
3934
|
+
// non-terminal: session continues, just without context
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
const args = isResume ? this.buildResumeArgs(prompt, params, images) : this.buildExecArgs(prompt, params, images);
|
|
3938
|
+
const executable = getDefaultCodexExecutable();
|
|
3939
|
+
const invocation = resolveCodexInvocation(args, {
|
|
3940
|
+
codexCommand: executable.command,
|
|
3941
|
+
codexIsPath: executable.isPath
|
|
3942
|
+
});
|
|
3943
|
+
const proc = spawn3(invocation.cmd, invocation.args, {
|
|
3944
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3945
|
+
env: { ...process.env },
|
|
3946
|
+
detached: process.platform !== "win32",
|
|
3947
|
+
windowsHide: process.platform === "win32"
|
|
3948
|
+
});
|
|
3949
|
+
this.process = proc;
|
|
3950
|
+
proc.stdin?.end();
|
|
3951
|
+
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
3952
|
+
proc.stderr.on("data", (chunk) => {
|
|
3953
|
+
console.error(`[exec-client stderr] ${chunk.toString().trimEnd()}`);
|
|
3954
|
+
});
|
|
3955
|
+
proc.on("error", (err) => {
|
|
3956
|
+
if (!this._destroyed) {
|
|
3957
|
+
this.emit("error", err);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
proc.on("exit", (code, signal) => {
|
|
3961
|
+
if (this.turnId && !this._destroyed && !this.turnCompleted) {
|
|
3962
|
+
if (code !== 0 && code !== null) {
|
|
3963
|
+
this.emitNotification(Methods.ERROR, {
|
|
3964
|
+
threadId: this.threadId,
|
|
3965
|
+
turnId: this.turnId,
|
|
3966
|
+
error: { message: `exec process exited with code ${code}` },
|
|
3967
|
+
willRetry: false
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
if (!this._destroyed) {
|
|
3972
|
+
this.emit("exit", code, signal);
|
|
3973
|
+
}
|
|
3974
|
+
this.process = null;
|
|
3975
|
+
});
|
|
3976
|
+
const turnId = this.turnId;
|
|
3977
|
+
return { turn: { id: turnId } };
|
|
3978
|
+
}
|
|
3979
|
+
async turnInterrupt(_params) {
|
|
3980
|
+
this.killProcess();
|
|
3981
|
+
}
|
|
3982
|
+
onNotification(handler) {
|
|
3983
|
+
this.notificationHandler = handler;
|
|
3984
|
+
}
|
|
3985
|
+
onServerRequest(handler) {
|
|
3986
|
+
this.serverRequestHandler = handler;
|
|
3987
|
+
}
|
|
3988
|
+
respondToServer(_id, _result) {
|
|
3989
|
+
}
|
|
3990
|
+
respondErrorToServer(_id, _code, _message) {
|
|
3991
|
+
}
|
|
3992
|
+
async destroy() {
|
|
3993
|
+
if (this._destroyed) return;
|
|
3994
|
+
this._destroyed = true;
|
|
3995
|
+
const proc = this.process;
|
|
3996
|
+
if (proc && !proc.killed) {
|
|
3997
|
+
const alreadyExited = proc.exitCode !== null;
|
|
3998
|
+
proc.stdin?.end();
|
|
3999
|
+
this.killProcess();
|
|
4000
|
+
const forceKill = setTimeout(() => {
|
|
4001
|
+
if (proc && !proc.killed && proc.exitCode === null) {
|
|
4002
|
+
if (process.platform === "win32" && proc.pid) {
|
|
4003
|
+
try {
|
|
4004
|
+
spawn3("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
|
|
4005
|
+
stdio: "ignore",
|
|
4006
|
+
windowsHide: true
|
|
4007
|
+
});
|
|
4008
|
+
} catch {
|
|
4009
|
+
}
|
|
4010
|
+
} else {
|
|
4011
|
+
try {
|
|
4012
|
+
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
4013
|
+
else proc.kill("SIGKILL");
|
|
4014
|
+
} catch {
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}, FORCE_KILL_TIMEOUT_MS);
|
|
4019
|
+
forceKill.unref();
|
|
4020
|
+
if (!alreadyExited) {
|
|
4021
|
+
await new Promise((resolve) => {
|
|
4022
|
+
proc.on("exit", () => {
|
|
4023
|
+
clearTimeout(forceKill);
|
|
4024
|
+
resolve();
|
|
4025
|
+
});
|
|
4026
|
+
const fallback = setTimeout(resolve, FORCE_KILL_TIMEOUT_MS + 1e3);
|
|
4027
|
+
fallback.unref();
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
this.process = null;
|
|
4032
|
+
this.removeAllListeners();
|
|
4033
|
+
}
|
|
4034
|
+
// ── Private helpers ─────────────────────────────────────────────
|
|
4035
|
+
/**
|
|
4036
|
+
* Build args for the first turn: `codex exec "<prompt>" --json --skip-git-repo-check [flags]`.
|
|
4037
|
+
* No --ephemeral so the session persists for subsequent resume turns.
|
|
4038
|
+
*/
|
|
4039
|
+
buildExecArgs(prompt, params, images) {
|
|
4040
|
+
const args = ["exec", prompt, "--json", "--skip-git-repo-check"];
|
|
4041
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4042
|
+
if (model) args.push("-m", model);
|
|
4043
|
+
let effectiveSandbox;
|
|
4044
|
+
if (params.sandboxPolicy) {
|
|
4045
|
+
effectiveSandbox = sandboxPolicyToMode(params.sandboxPolicy);
|
|
4046
|
+
}
|
|
4047
|
+
if (!effectiveSandbox) {
|
|
4048
|
+
effectiveSandbox = this.threadStartParams?.sandbox ?? this.spawnOpts?.sandbox;
|
|
4049
|
+
}
|
|
4050
|
+
if (effectiveSandbox) args.push("-s", effectiveSandbox);
|
|
4051
|
+
if (this.spawnOpts?.profile) args.push("-p", this.spawnOpts.profile);
|
|
4052
|
+
const cwd = params.cwd ?? this.threadStartParams?.cwd;
|
|
4053
|
+
if (cwd) args.push("-C", cwd);
|
|
4054
|
+
for (const img of images) args.push("-i", img);
|
|
4055
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4056
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4057
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4058
|
+
try {
|
|
4059
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "codex-mcp-schema-"));
|
|
4060
|
+
const schemaPath = join(tmpDir, "output-schema.json");
|
|
4061
|
+
writeFileSync(schemaPath, JSON.stringify(params.outputSchema));
|
|
4062
|
+
args.push("--output-schema", schemaPath);
|
|
4063
|
+
} catch (err) {
|
|
4064
|
+
console.error(
|
|
4065
|
+
`[exec-client] Failed to write output schema to temp file: ${err instanceof Error ? err.message : String(err)}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
const configs = {
|
|
4070
|
+
...this.spawnOpts?.config,
|
|
4071
|
+
...this.threadStartParams?.config
|
|
4072
|
+
};
|
|
4073
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4074
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4075
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4076
|
+
}
|
|
4077
|
+
return args;
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Build args for subsequent turns: `codex exec resume <threadId> "<prompt>" --json [flags]`.
|
|
4081
|
+
* Resumes the persisted session for multi-turn context continuity.
|
|
4082
|
+
* Note: exec resume only supports -m, -c, -i, --json, --skip-git-repo-check.
|
|
4083
|
+
* -s, -p, -C are NOT supported and inherit from the first turn's session.
|
|
4084
|
+
*/
|
|
4085
|
+
buildResumeArgs(prompt, params, images) {
|
|
4086
|
+
const args = [
|
|
4087
|
+
"exec",
|
|
4088
|
+
"resume",
|
|
4089
|
+
this.realThreadId,
|
|
4090
|
+
prompt,
|
|
4091
|
+
"--json",
|
|
4092
|
+
"--skip-git-repo-check"
|
|
4093
|
+
];
|
|
4094
|
+
if (params.sandboxPolicy) {
|
|
4095
|
+
console.error(
|
|
4096
|
+
"[exec-client] sandbox override ignored in resume mode (exec resume does not support -s)"
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
if (params.cwd) {
|
|
4100
|
+
console.error(
|
|
4101
|
+
"[exec-client] cwd override ignored in resume mode (exec resume does not support -C)"
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4105
|
+
console.error(
|
|
4106
|
+
"[exec-client] outputSchema ignored in resume mode (exec resume does not support --output-schema)"
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4110
|
+
if (model) args.push("-m", model);
|
|
4111
|
+
for (const img of images) args.push("-i", img);
|
|
4112
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4113
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4114
|
+
const configs = {
|
|
4115
|
+
...this.spawnOpts?.config,
|
|
4116
|
+
...this.threadStartParams?.config
|
|
4117
|
+
};
|
|
4118
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4119
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4120
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4121
|
+
}
|
|
4122
|
+
return args;
|
|
4123
|
+
}
|
|
4124
|
+
onData(chunk) {
|
|
4125
|
+
this.buffer += this.decoder.write(chunk);
|
|
4126
|
+
const lines = this.buffer.split("\n");
|
|
4127
|
+
this.buffer = lines.pop() ?? "";
|
|
4128
|
+
for (const line of lines) {
|
|
4129
|
+
const trimmed = line.trim();
|
|
4130
|
+
if (!trimmed || trimmed[0] !== "{") continue;
|
|
4131
|
+
try {
|
|
4132
|
+
const event = JSON.parse(trimmed);
|
|
4133
|
+
this.handleExecEvent(event);
|
|
4134
|
+
} catch {
|
|
4135
|
+
console.error(`[exec-client] Failed to parse JSONL: ${trimmed.slice(0, 200)}`);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Transform exec JSONL event into app-server notification and dispatch.
|
|
4141
|
+
*/
|
|
4142
|
+
handleExecEvent(event) {
|
|
4143
|
+
const type = event.type;
|
|
4144
|
+
switch (type) {
|
|
4145
|
+
case "thread.started": {
|
|
4146
|
+
const cliThreadId = event.thread_id;
|
|
4147
|
+
if (cliThreadId) {
|
|
4148
|
+
this.threadId = cliThreadId;
|
|
4149
|
+
this.realThreadId = cliThreadId;
|
|
4150
|
+
}
|
|
4151
|
+
this.emitNotification(Methods.THREAD_STARTED, {
|
|
4152
|
+
thread: { id: this.threadId }
|
|
4153
|
+
});
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
case "turn.started":
|
|
4157
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4158
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4159
|
+
});
|
|
4160
|
+
return;
|
|
4161
|
+
case "item.started": {
|
|
4162
|
+
const item = event.item;
|
|
4163
|
+
if (item) {
|
|
4164
|
+
this.emitNotification(Methods.ITEM_STARTED, {
|
|
4165
|
+
threadId: this.threadId,
|
|
4166
|
+
turnId: this.turnId,
|
|
4167
|
+
item: transformItem(item)
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
case "item.completed": {
|
|
4173
|
+
const item = event.item;
|
|
4174
|
+
if (item) {
|
|
4175
|
+
const transformed = transformItem(item);
|
|
4176
|
+
if (transformed.type === "agentMessage" && typeof transformed.text === "string") {
|
|
4177
|
+
this.lastAgentMessageText = transformed.text;
|
|
4178
|
+
}
|
|
4179
|
+
this.emitNotification(Methods.ITEM_COMPLETED, {
|
|
4180
|
+
threadId: this.threadId,
|
|
4181
|
+
turnId: this.turnId,
|
|
4182
|
+
item: transformed
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
case "turn.completed": {
|
|
4188
|
+
const turnId = this.turnId ?? "";
|
|
4189
|
+
this.turnCompleted = true;
|
|
4190
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4191
|
+
threadId: this.threadId,
|
|
4192
|
+
turn: {
|
|
4193
|
+
id: turnId,
|
|
4194
|
+
status: "completed",
|
|
4195
|
+
output: this.lastAgentMessageText || void 0,
|
|
4196
|
+
usage: event.usage
|
|
4197
|
+
}
|
|
4198
|
+
});
|
|
4199
|
+
this.turnId = null;
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
case "turn.failed": {
|
|
4203
|
+
const turnId = this.turnId ?? "";
|
|
4204
|
+
const error = event.error;
|
|
4205
|
+
this.turnCompleted = true;
|
|
4206
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4207
|
+
threadId: this.threadId,
|
|
4208
|
+
turn: {
|
|
4209
|
+
id: turnId,
|
|
4210
|
+
status: "failed",
|
|
4211
|
+
error: error ?? { message: "Turn failed" }
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
this.turnId = null;
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
case "error": {
|
|
4218
|
+
const willRetry = isRetryableError(event);
|
|
4219
|
+
this.emitNotification(Methods.ERROR, {
|
|
4220
|
+
threadId: this.threadId,
|
|
4221
|
+
turnId: this.turnId,
|
|
4222
|
+
error: event.message ?? event.error,
|
|
4223
|
+
willRetry
|
|
4224
|
+
});
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
default:
|
|
4228
|
+
break;
|
|
4229
|
+
}
|
|
4230
|
+
const mappedMethod = EXEC_EVENT_TO_METHOD[type];
|
|
4231
|
+
if (mappedMethod) {
|
|
4232
|
+
if (type === "task_started") {
|
|
4233
|
+
const turnId = event.turn_id ?? this.turnId;
|
|
4234
|
+
if (turnId) this.turnId = turnId;
|
|
4235
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4236
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4237
|
+
});
|
|
4238
|
+
} else if (type === "task_complete") {
|
|
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
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
this.turnId = null;
|
|
4250
|
+
} else if (type === "turn_aborted") {
|
|
4251
|
+
const turnId = this.turnId ?? "";
|
|
4252
|
+
this.turnCompleted = true;
|
|
4253
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4254
|
+
threadId: this.threadId,
|
|
4255
|
+
turn: {
|
|
4256
|
+
id: turnId,
|
|
4257
|
+
status: "cancelled",
|
|
4258
|
+
error: event.reason ?? { message: "Turn aborted" }
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4261
|
+
this.turnId = null;
|
|
4262
|
+
} else if (mappedMethod === Methods.ERROR) {
|
|
4263
|
+
this.emitNotification(Methods.ERROR, {
|
|
4264
|
+
threadId: this.threadId,
|
|
4265
|
+
turnId: this.turnId,
|
|
4266
|
+
error: event.message ?? event.error ?? type,
|
|
4267
|
+
willRetry: isRetryableError(event)
|
|
4268
|
+
});
|
|
4269
|
+
} else {
|
|
4270
|
+
this.emitNotification(mappedMethod, {
|
|
4271
|
+
threadId: this.threadId,
|
|
4272
|
+
turnId: this.turnId,
|
|
4273
|
+
...event
|
|
4274
|
+
});
|
|
4275
|
+
}
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
console.error(`[exec-client] Unmapped exec event type: ${type}`);
|
|
4279
|
+
}
|
|
4280
|
+
emitNotification(method, params) {
|
|
4281
|
+
if (this.notificationHandler) {
|
|
4282
|
+
this.notificationHandler(method, params);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
killProcess() {
|
|
4286
|
+
if (!this.process || this.process.killed) return;
|
|
4287
|
+
if (process.platform !== "win32" && this.process.pid) {
|
|
4288
|
+
try {
|
|
4289
|
+
process.kill(-this.process.pid, "SIGTERM");
|
|
4290
|
+
return;
|
|
4291
|
+
} catch {
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
try {
|
|
4295
|
+
this.process.kill("SIGTERM");
|
|
4296
|
+
} catch {
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
};
|
|
4300
|
+
|
|
3688
4301
|
// src/index.ts
|
|
3689
4302
|
async function main() {
|
|
3690
4303
|
const preflight = runStdioPreflight();
|
|
@@ -3706,8 +4319,15 @@ async function main() {
|
|
|
3706
4319
|
);
|
|
3707
4320
|
}
|
|
3708
4321
|
checkDefaultCodexExecutableAvailability();
|
|
4322
|
+
const executable = getDefaultCodexExecutable();
|
|
4323
|
+
const clientMode = await detectClientMode(executable.command, executable.isPath);
|
|
4324
|
+
console.error(`[codex-mcp] client mode: ${clientMode} (binary: ${executable.command})`);
|
|
4325
|
+
const createClient = () => clientMode === "exec" ? new ExecClient() : new AppServerClient();
|
|
3709
4326
|
const serverCwd = process.cwd();
|
|
3710
|
-
const server = createServer(serverCwd
|
|
4327
|
+
const server = createServer(serverCwd, {
|
|
4328
|
+
createClient,
|
|
4329
|
+
clientMode
|
|
4330
|
+
});
|
|
3711
4331
|
const transport = new StdioServerTransport();
|
|
3712
4332
|
let closing = false;
|
|
3713
4333
|
const shutdown = async () => {
|