@leo000001/codex-mcp 2.1.0 → 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/dist/index.js CHANGED
@@ -111,21 +111,38 @@ function resolveCodexInvocation(codexArgs, deps = {}) {
111
111
  const readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
112
112
  const pathApi = platform === "win32" ? path.win32 : path.posix;
113
113
  const delimiter = platform === "win32" ? ";" : ":";
114
+ const codexCommand = deps.codexCommand ?? "codex";
115
+ const codexIsPath = deps.codexIsPath ?? false;
116
+ if (codexIsPath) {
117
+ if (platform === "win32" && (codexCommand.toLowerCase().endsWith(".cmd") || codexCommand.toLowerCase().endsWith(".bat"))) {
118
+ const comspec2 = env.ComSpec || env.COMSPEC || "cmd.exe";
119
+ return {
120
+ cmd: comspec2,
121
+ args: ["/d", "/s", "/c", codexCommand, ...codexArgs],
122
+ spawnedViaCmd: true
123
+ };
124
+ }
125
+ return { cmd: codexCommand, args: codexArgs, spawnedViaCmd: false };
126
+ }
114
127
  if (platform !== "win32") {
115
- return { cmd: "codex", args: codexArgs, spawnedViaCmd: false };
128
+ return { cmd: codexCommand, args: codexArgs, spawnedViaCmd: false };
116
129
  }
117
- const shim = findOnPath("codex", env, exists, pathApi, delimiter, [".exe", ".cmd", ".bat"]);
130
+ const shim = findOnPath(codexCommand, env, exists, pathApi, delimiter, [".exe", ".cmd", ".bat"]);
118
131
  if (shim && shim.toLowerCase().endsWith(".exe")) {
119
132
  return { cmd: shim, args: codexArgs, spawnedViaCmd: false };
120
133
  }
121
134
  if (shim && (shim.toLowerCase().endsWith(".cmd") || shim.toLowerCase().endsWith(".bat"))) {
122
- const script = tryResolveNodeScriptFromShim(shim, exists, readFile, pathApi);
135
+ const script = tryResolveNodeScriptFromShim(shim, codexCommand, exists, readFile, pathApi);
123
136
  if (script) {
124
137
  return { cmd: process.execPath, args: [script, ...codexArgs], spawnedViaCmd: false };
125
138
  }
126
139
  }
127
140
  const comspec = env.ComSpec || env.COMSPEC || "cmd.exe";
128
- return { cmd: comspec, args: ["/d", "/s", "/c", "codex", ...codexArgs], spawnedViaCmd: true };
141
+ return {
142
+ cmd: comspec,
143
+ args: ["/d", "/s", "/c", codexCommand, ...codexArgs],
144
+ spawnedViaCmd: true
145
+ };
129
146
  }
130
147
  function findOnPath(base, env, exists, pathApi, delimiter, exts) {
131
148
  const pathEnv = env.PATH || env.Path || env.path || "";
@@ -146,7 +163,7 @@ function stripSurroundingQuotes(value) {
146
163
  }
147
164
  return value;
148
165
  }
149
- function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
166
+ function tryResolveNodeScriptFromShim(shimPath, codexCommand, exists, readFile, pathApi) {
150
167
  let contents;
151
168
  try {
152
169
  contents = readFile(shimPath);
@@ -161,7 +178,13 @@ function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
161
178
  matches.push(m[1]);
162
179
  }
163
180
  if (matches.length === 0) return void 0;
164
- const preferred = matches.find((m) => /codex/i.test(pathApi.basename(m))) ?? matches.find((m) => /@openai\\codex|\\codex\\|\/codex\//i.test(m)) ?? matches[matches.length - 1];
181
+ const escapedCommand = codexCommand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
182
+ const baseNameRe = new RegExp(escapedCommand, "i");
183
+ const pathRe = new RegExp(
184
+ `@openai\\\\${escapedCommand}|\\\\${escapedCommand}\\\\|\\/${escapedCommand}\\/`,
185
+ "i"
186
+ );
187
+ const preferred = matches.find((m) => baseNameRe.test(pathApi.basename(m))) ?? matches.find((m) => pathRe.test(m)) ?? matches[matches.length - 1];
165
188
  const shimDir = pathApi.dirname(shimPath);
166
189
  const dp0 = shimDir.endsWith(pathApi.sep) ? shimDir : shimDir + pathApi.sep;
167
190
  let resolved = preferred.replace(/%~dp0/gi, dp0).replace(/%dp0%/gi, dp0);
@@ -171,6 +194,139 @@ function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
171
194
  return abs;
172
195
  }
173
196
 
197
+ // src/utils/codex-executable.ts
198
+ import { accessSync, constants, existsSync as existsSync2, statSync } from "fs";
199
+ import path2 from "path";
200
+ var CODEX_MCP_COMMAND = "CODEX_MCP_COMMAND";
201
+ var CODEX_MCP_PATH = "CODEX_MCP_PATH";
202
+ var AUTO_CODEX_COMMANDS = ["codex", "codex-internal"];
203
+ var _resolved;
204
+ var WINDOWS_SUPPORTED_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
205
+ function stripSurroundingQuotes2(value) {
206
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
207
+ return value.slice(1, -1);
208
+ }
209
+ return value;
210
+ }
211
+ function normalizeMaybeQuotedToken(raw) {
212
+ return stripSurroundingQuotes2(raw.trim());
213
+ }
214
+ function normalizeWindowsExtension(value) {
215
+ const trimmed = stripSurroundingQuotes2(value.trim());
216
+ if (!trimmed) return void 0;
217
+ return trimmed.startsWith(".") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;
218
+ }
219
+ function isExecutableFile(candidate) {
220
+ try {
221
+ const stat = statSync(candidate);
222
+ if (!stat.isFile()) return false;
223
+ if (process.platform === "win32") return true;
224
+ accessSync(candidate, constants.X_OK);
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+ function getPathEntries(env) {
231
+ const pathEnv = env.PATH || env.Path || env.path || "";
232
+ return pathEnv.split(process.platform === "win32" ? ";" : ":").map((entry) => stripSurroundingQuotes2(entry.trim())).filter(Boolean);
233
+ }
234
+ function getPathExtensions(env) {
235
+ if (process.platform !== "win32") return [""];
236
+ const configured = env.PATHEXT ?? env.Pathext ?? env.pathext ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
237
+ const configuredExts = configured.split(";").map((entry) => normalizeWindowsExtension(entry)).filter((entry) => Boolean(entry));
238
+ const merged = configuredExts.filter(
239
+ (ext) => WINDOWS_SUPPORTED_EXTENSIONS.includes(ext)
240
+ );
241
+ for (const ext of WINDOWS_SUPPORTED_EXTENSIONS) {
242
+ if (!merged.includes(ext)) merged.push(ext);
243
+ }
244
+ return Array.from(new Set(merged));
245
+ }
246
+ function commandExistsOnPath(command, env) {
247
+ const dirs = getPathEntries(env);
248
+ const ext = process.platform === "win32" ? path2.extname(command) : "";
249
+ const names = process.platform === "win32" ? Array.from(
250
+ new Set(
251
+ ext ? [command] : [...getPathExtensions(env).map((suffix) => `${command}${suffix}`), command]
252
+ )
253
+ ) : [command];
254
+ for (const dir of dirs) {
255
+ for (const name of names) {
256
+ const candidate = path2.join(dir, name);
257
+ if (isExecutableFile(candidate)) return true;
258
+ }
259
+ }
260
+ return false;
261
+ }
262
+ function looksLikePath(value) {
263
+ return value.includes("/") || value.includes("\\");
264
+ }
265
+ function resolveDefaultCodexExecutable(env = process.env) {
266
+ const envPathRaw = env[CODEX_MCP_PATH]?.trim();
267
+ const envCommandRaw = env[CODEX_MCP_COMMAND]?.trim();
268
+ const envPath = envPathRaw ? normalizeMaybeQuotedToken(envPathRaw) : void 0;
269
+ const envCommand = envCommandRaw ? normalizeMaybeQuotedToken(envCommandRaw) : void 0;
270
+ if (envPath && envCommand) {
271
+ throw new Error(
272
+ `Cannot set both ${CODEX_MCP_PATH} and ${CODEX_MCP_COMMAND}. Use one or the other.`
273
+ );
274
+ }
275
+ if (envPath) {
276
+ const resolvedPath = path2.resolve(envPath);
277
+ if (!existsSync2(resolvedPath)) {
278
+ throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 file does not exist.`);
279
+ }
280
+ if (!isExecutableFile(resolvedPath)) {
281
+ throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 not an executable file.`);
282
+ }
283
+ return { command: resolvedPath, isPath: true, source: "env_path" };
284
+ }
285
+ if (envCommand) {
286
+ if (looksLikePath(envCommand)) {
287
+ throw new Error(
288
+ `${CODEX_MCP_COMMAND}="${envCommand}" looks like a path. Use ${CODEX_MCP_PATH} for filesystem paths.`
289
+ );
290
+ }
291
+ if (!commandExistsOnPath(envCommand, env)) {
292
+ throw new Error(`${CODEX_MCP_COMMAND}="${envCommand}" was not found in PATH.`);
293
+ }
294
+ return { command: envCommand, isPath: false, source: "env_command" };
295
+ }
296
+ for (const candidate of AUTO_CODEX_COMMANDS) {
297
+ if (commandExistsOnPath(candidate, env)) {
298
+ return { command: candidate, isPath: false, source: "auto_detect" };
299
+ }
300
+ }
301
+ return { command: "codex", isPath: false, source: "default" };
302
+ }
303
+ function getDefaultCodexExecutable() {
304
+ if (!_resolved) {
305
+ _resolved = resolveDefaultCodexExecutable();
306
+ }
307
+ return _resolved;
308
+ }
309
+ function checkDefaultCodexExecutableAvailability() {
310
+ const info = getDefaultCodexExecutable();
311
+ const label = info.isPath ? "path" : "command";
312
+ switch (info.source) {
313
+ case "env_path":
314
+ console.error(`[codex-executable] Using ${CODEX_MCP_PATH}: ${info.command}`);
315
+ break;
316
+ case "env_command":
317
+ console.error(`[codex-executable] Using ${CODEX_MCP_COMMAND}: ${info.command}`);
318
+ break;
319
+ case "auto_detect":
320
+ console.error(`[codex-executable] Auto-detected ${label}: ${info.command}`);
321
+ break;
322
+ case "default":
323
+ console.error(
324
+ `[codex-executable] No codex found on PATH; falling back to "${info.command}". Set ${CODEX_MCP_COMMAND} or ${CODEX_MCP_PATH} to configure.`
325
+ );
326
+ break;
327
+ }
328
+ }
329
+
174
330
  // src/types.ts
175
331
  var APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
176
332
  var SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
@@ -216,6 +372,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
216
372
  ErrorCode2["THREAD_FORK_RESUME_FAILED"] = "THREAD_FORK_RESUME_FAILED";
217
373
  ErrorCode2["PROTOCOL_PARSE_ERROR"] = "PROTOCOL_PARSE_ERROR";
218
374
  ErrorCode2["WRITE_QUEUE_DROPPED"] = "WRITE_QUEUE_DROPPED";
375
+ ErrorCode2["EXEC_NOT_SUPPORTED"] = "EXEC_NOT_SUPPORTED";
219
376
  ErrorCode2["INTERNAL"] = "INTERNAL";
220
377
  return ErrorCode2;
221
378
  })(ErrorCode || {});
@@ -235,7 +392,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
235
392
  var CLEANUP_INTERVAL_MS = 6e4;
236
393
 
237
394
  // src/app-server/client.ts
238
- var CLIENT_VERSION = true ? "2.1.0" : "0.0.0-dev";
395
+ var CLIENT_VERSION = true ? "2.1.3" : "0.0.0-dev";
239
396
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
240
397
  var STARTUP_REQUEST_TIMEOUT = 9e4;
241
398
  var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
@@ -257,6 +414,9 @@ var AppServerClient = class extends EventEmitter {
257
414
  get destroyed() {
258
415
  return this._destroyed;
259
416
  }
417
+ get supportsTurnOverrides() {
418
+ return true;
419
+ }
260
420
  /**
261
421
  * Spawn codex app-server and perform initialization handshake.
262
422
  */
@@ -264,7 +424,11 @@ var AppServerClient = class extends EventEmitter {
264
424
  const args = buildAppServerArgs(opts);
265
425
  const env = { ...process.env };
266
426
  const stdio = ["pipe", "pipe", "pipe"];
267
- const invocation = resolveCodexInvocation(args);
427
+ const exe = getDefaultCodexExecutable();
428
+ const invocation = resolveCodexInvocation(args, {
429
+ codexCommand: exe.command,
430
+ codexIsPath: exe.isPath
431
+ });
268
432
  this.spawnedViaCmd = invocation.spawnedViaCmd;
269
433
  this.spawnedDetached = process.platform !== "win32";
270
434
  const proc = spawn(invocation.cmd, invocation.args, {
@@ -624,16 +788,16 @@ var AppServerClient = class extends EventEmitter {
624
788
  };
625
789
 
626
790
  // src/utils/cwd.ts
627
- import { existsSync as existsSync2, statSync } from "fs";
628
- import path2 from "path";
791
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
792
+ import path3 from "path";
629
793
  function resolveAndValidateCwd(inputCwd, baseCwd) {
630
794
  const candidate = inputCwd ?? baseCwd;
631
- const resolved = path2.isAbsolute(candidate) ? candidate : path2.resolve(baseCwd, candidate);
632
- if (!existsSync2(resolved)) {
795
+ const resolved = path3.isAbsolute(candidate) ? candidate : path3.resolve(baseCwd, candidate);
796
+ if (!existsSync3(resolved)) {
633
797
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd does not exist: ${resolved}`);
634
798
  }
635
799
  try {
636
- const stat = statSync(resolved);
800
+ const stat = statSync2(resolved);
637
801
  if (!stat.isDirectory()) {
638
802
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd is not a directory: ${resolved}`);
639
803
  }
@@ -655,15 +819,15 @@ function redactPaths(message) {
655
819
  }
656
820
 
657
821
  // src/utils/files.ts
658
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
659
- import path3 from "path";
822
+ import { existsSync as existsSync4, statSync as statSync3 } from "fs";
823
+ import path4 from "path";
660
824
  function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
661
- const resolved = path3.isAbsolute(inputPath) ? inputPath : path3.resolve(baseDir, inputPath);
662
- if (!existsSync3(resolved)) {
825
+ const resolved = path4.isAbsolute(inputPath) ? inputPath : path4.resolve(baseDir, inputPath);
826
+ if (!existsSync4(resolved)) {
663
827
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} does not exist: ${resolved}`);
664
828
  }
665
829
  try {
666
- const stat = statSync2(resolved);
830
+ const stat = statSync3(resolved);
667
831
  if (!stat.isFile()) {
668
832
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} is not a file: ${resolved}`);
669
833
  }
@@ -841,12 +1005,13 @@ var SessionManager = class {
841
1005
  const turnStartResult = await client.turnStart(turnParams);
842
1006
  const startedTurnId = extractTurnId(turnStartResult);
843
1007
  if (startedTurnId) session.activeTurnId = startedTurnId;
844
- if (resolvedCwd) session.cwd = resolvedCwd;
1008
+ const canOverride = client.supportsTurnOverrides;
1009
+ if (resolvedCwd && canOverride) session.cwd = resolvedCwd;
845
1010
  if (overrides?.model) session.model = overrides.model;
846
1011
  if (overrides?.approvalPolicy) {
847
1012
  session.approvalPolicy = overrides.approvalPolicy;
848
1013
  }
849
- if (overrides?.sandbox) {
1014
+ if (overrides?.sandbox && canOverride) {
850
1015
  session.sandbox = overrides.sandbox;
851
1016
  }
852
1017
  } catch (err) {
@@ -1398,10 +1563,14 @@ var SessionManager = class {
1398
1563
  switch (method) {
1399
1564
  case Methods.THREAD_STARTED: {
1400
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
+ }
1401
1570
  pushEvent(session.eventBuffer, "progress", {
1402
1571
  method,
1403
1572
  ...p,
1404
- threadId: normalizeOptionalString(p.threadId) ?? normalizeOptionalString(thread?.id),
1573
+ threadId: notifiedThreadId,
1405
1574
  status: normalizeOptionalString(thread?.status)
1406
1575
  });
1407
1576
  break;
@@ -2648,6 +2817,7 @@ var ERROR_CODE_HINTS = {
2648
2817
  ["THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */]: "Forked thread could not resume in new process. Retry fork from current source session.",
2649
2818
  ["PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */]: "Non-JSON or malformed app-server line. Check shell/profile noise and transport health.",
2650
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.",
2651
2821
  ["INTERNAL" /* INTERNAL */]: "Unexpected server-side failure. Inspect logs and retry safely."
2652
2822
  };
2653
2823
  function asTextResource(uri, text, mimeType) {
@@ -2757,6 +2927,7 @@ function buildGotchasText() {
2757
2927
  "- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
2758
2928
  "- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
2759
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.",
2760
2931
  "",
2761
2932
  "## Approval behavior",
2762
2933
  "",
@@ -2790,6 +2961,13 @@ function buildGotchasText() {
2790
2961
  "",
2791
2962
  "- codex-mcp does not hard-code a strict concurrent-session cap.",
2792
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`).",
2793
2971
  ""
2794
2972
  ].join("\n");
2795
2973
  }
@@ -2970,6 +3148,7 @@ function registerResources(server, deps) {
2970
3148
  name: "codex-mcp",
2971
3149
  version: deps.version,
2972
3150
  codexCliVersion: getCodexCliVersion(),
3151
+ clientMode: deps.clientMode ?? "app-server",
2973
3152
  node: process.version,
2974
3153
  platform: process.platform,
2975
3154
  arch: process.arch,
@@ -3061,7 +3240,7 @@ function registerResources(server, deps) {
3061
3240
  }
3062
3241
 
3063
3242
  // src/server.ts
3064
- var SERVER_VERSION = true ? "2.1.0" : "0.0.0-dev";
3243
+ var SERVER_VERSION = true ? "2.1.3" : "0.0.0-dev";
3065
3244
  function formatErrorMessage(err) {
3066
3245
  const message = err instanceof Error ? err.message : String(err);
3067
3246
  const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
@@ -3080,13 +3259,17 @@ function toStructuredContent(value) {
3080
3259
  }
3081
3260
  return { value };
3082
3261
  }
3083
- function createServer(serverCwd) {
3084
- const sessionManager = new SessionManager();
3262
+ function createServer(serverCwd, options) {
3263
+ const sessionManager = new SessionManager(options);
3085
3264
  const server = new McpServer({
3086
3265
  name: "codex-mcp",
3087
3266
  version: SERVER_VERSION
3088
3267
  });
3089
- registerResources(server, { version: SERVER_VERSION, sessionManager });
3268
+ registerResources(server, {
3269
+ version: SERVER_VERSION,
3270
+ sessionManager,
3271
+ clientMode: options?.clientMode
3272
+ });
3090
3273
  const publicSessionInfoSchema = z.object({
3091
3274
  sessionId: z.string(),
3092
3275
  status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]),
@@ -3146,10 +3329,10 @@ function createServer(serverCwd) {
3146
3329
  })
3147
3330
  ).optional().describe("question-id -> answers map (id from actions[] user_input request).")
3148
3331
  }).superRefine((value, ctx) => {
3149
- const addIssue = (path4, message) => {
3332
+ const addIssue = (path5, message) => {
3150
3333
  ctx.addIssue({
3151
3334
  code: z.ZodIssueCode.custom,
3152
- path: [path4],
3335
+ path: [path5],
3153
3336
  message
3154
3337
  });
3155
3338
  };
@@ -3425,9 +3608,10 @@ function createServer(serverCwd) {
3425
3608
 
3426
3609
  POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
3427
3610
  - Treat pollInterval as a minimum hint, not a fixed schedule.
3428
- - "running": never poll faster than 120000ms (2 minutes); use longer intervals for longer tasks.
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.
3429
3612
  - "waiting_approval": poll about every 1000ms and respond quickly to actions[].
3430
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.
3431
3615
 
3432
3616
  poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
3433
3617
 
@@ -3525,6 +3709,595 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
3525
3709
  return server;
3526
3710
  }
3527
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
+
3528
4301
  // src/index.ts
3529
4302
  async function main() {
3530
4303
  const preflight = runStdioPreflight();
@@ -3545,8 +4318,16 @@ async function main() {
3545
4318
  "STDIO preflight failed in strict mode due to blocking stdout contamination risk"
3546
4319
  );
3547
4320
  }
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();
3548
4326
  const serverCwd = process.cwd();
3549
- const server = createServer(serverCwd);
4327
+ const server = createServer(serverCwd, {
4328
+ createClient,
4329
+ clientMode
4330
+ });
3550
4331
  const transport = new StdioServerTransport();
3551
4332
  let closing = false;
3552
4333
  const shutdown = async () => {