cyrus-codex-runner 0.2.64-test.6 → 0.2.64

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.
Files changed (55) hide show
  1. package/dist/CodexEventMapper.d.ts +56 -0
  2. package/dist/CodexEventMapper.d.ts.map +1 -0
  3. package/dist/CodexEventMapper.js +469 -0
  4. package/dist/CodexEventMapper.js.map +1 -0
  5. package/dist/CodexRunner.d.ts +37 -46
  6. package/dist/CodexRunner.d.ts.map +1 -1
  7. package/dist/CodexRunner.js +136 -851
  8. package/dist/CodexRunner.js.map +1 -1
  9. package/dist/CodexSkillStager.d.ts +42 -0
  10. package/dist/CodexSkillStager.d.ts.map +1 -0
  11. package/dist/CodexSkillStager.js +182 -0
  12. package/dist/CodexSkillStager.js.map +1 -0
  13. package/dist/backend/AppServerCodexBackend.d.ts +74 -0
  14. package/dist/backend/AppServerCodexBackend.d.ts.map +1 -0
  15. package/dist/backend/AppServerCodexBackend.js +352 -0
  16. package/dist/backend/AppServerCodexBackend.js.map +1 -0
  17. package/dist/backend/appServerClient.d.ts +75 -0
  18. package/dist/backend/appServerClient.d.ts.map +1 -0
  19. package/dist/backend/appServerClient.js +223 -0
  20. package/dist/backend/appServerClient.js.map +1 -0
  21. package/dist/backend/appServerEvents.d.ts +9 -0
  22. package/dist/backend/appServerEvents.d.ts.map +1 -0
  23. package/dist/backend/appServerEvents.js +110 -0
  24. package/dist/backend/appServerEvents.js.map +1 -0
  25. package/dist/backend/appServerProcess.d.ts +38 -0
  26. package/dist/backend/appServerProcess.d.ts.map +1 -0
  27. package/dist/backend/appServerProcess.js +283 -0
  28. package/dist/backend/appServerProcess.js.map +1 -0
  29. package/dist/backend/codexBinary.d.ts +24 -0
  30. package/dist/backend/codexBinary.d.ts.map +1 -0
  31. package/dist/backend/codexBinary.js +44 -0
  32. package/dist/backend/codexBinary.js.map +1 -0
  33. package/dist/backend/types.d.ts +210 -0
  34. package/dist/backend/types.d.ts.map +1 -0
  35. package/dist/backend/types.js +2 -0
  36. package/dist/backend/types.js.map +1 -0
  37. package/dist/config/CodexConfigBuilder.d.ts +40 -0
  38. package/dist/config/CodexConfigBuilder.d.ts.map +1 -0
  39. package/dist/config/CodexConfigBuilder.js +182 -0
  40. package/dist/config/CodexConfigBuilder.js.map +1 -0
  41. package/dist/config/mcpConfigTranslator.d.ts +17 -0
  42. package/dist/config/mcpConfigTranslator.d.ts.map +1 -0
  43. package/dist/config/mcpConfigTranslator.js +245 -0
  44. package/dist/config/mcpConfigTranslator.js.map +1 -0
  45. package/dist/config/sandboxPolicy.d.ts +43 -0
  46. package/dist/config/sandboxPolicy.d.ts.map +1 -0
  47. package/dist/config/sandboxPolicy.js +56 -0
  48. package/dist/config/sandboxPolicy.js.map +1 -0
  49. package/dist/index.d.ts +3 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/types.d.ts +9 -7
  54. package/dist/types.d.ts.map +1 -1
  55. package/package.json +5 -4
@@ -1,297 +1,48 @@
1
1
  import crypto from "node:crypto";
2
2
  import { EventEmitter } from "node:events";
3
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
4
- import { homedir } from "node:os";
5
- import { join, relative as pathRelative } from "node:path";
6
- import { cwd } from "node:process";
7
- import { Codex } from "@openai/codex-sdk";
3
+ import { AppServerCodexBackend } from "./backend/AppServerCodexBackend.js";
4
+ import { CodexEventMapper } from "./CodexEventMapper.js";
5
+ import { CodexSkillStager } from "./CodexSkillStager.js";
6
+ import { CodexConfigBuilder } from "./config/CodexConfigBuilder.js";
8
7
  import { CodexMessageFormatter } from "./formatter.js";
9
- const DEFAULT_CODEX_MODEL = "gpt-5.5";
10
- const CODEX_MCP_DOCS_URL = "https://platform.openai.com/docs/docs-mcp";
11
- function toFiniteNumber(value) {
12
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
13
- }
14
- function safeStringify(value) {
15
- try {
16
- return JSON.stringify(value, null, 2);
17
- }
18
- catch {
19
- return String(value);
20
- }
21
- }
22
- function createAssistantToolUseMessage(toolUseId, toolName, toolInput, messageId = crypto.randomUUID()) {
23
- const contentBlocks = [
24
- {
25
- type: "tool_use",
26
- id: toolUseId,
27
- name: toolName,
28
- input: toolInput,
29
- },
30
- ];
31
- return {
32
- id: messageId,
33
- type: "message",
34
- role: "assistant",
35
- content: contentBlocks,
36
- model: DEFAULT_CODEX_MODEL,
37
- stop_reason: null,
38
- stop_sequence: null,
39
- stop_details: null,
40
- usage: {
41
- input_tokens: 0,
42
- output_tokens: 0,
43
- cache_creation_input_tokens: 0,
44
- cache_read_input_tokens: 0,
45
- output_tokens_details: null,
46
- cache_creation: null,
47
- inference_geo: null,
48
- iterations: null,
49
- server_tool_use: null,
50
- service_tier: null,
51
- speed: null,
52
- },
53
- container: null,
54
- context_management: null,
55
- diagnostics: null,
56
- };
57
- }
58
- function createUserToolResultMessage(toolUseId, result, isError) {
59
- const contentBlocks = [
60
- {
61
- type: "tool_result",
62
- tool_use_id: toolUseId,
63
- content: result,
64
- is_error: isError,
65
- },
66
- ];
67
- return {
68
- role: "user",
69
- content: contentBlocks,
70
- };
71
- }
72
- function createAssistantBetaMessage(content, messageId = crypto.randomUUID()) {
73
- const contentBlocks = [
74
- { type: "text", text: content },
75
- ];
76
- return {
77
- id: messageId,
78
- type: "message",
79
- role: "assistant",
80
- content: contentBlocks,
81
- model: DEFAULT_CODEX_MODEL,
82
- stop_reason: null,
83
- stop_sequence: null,
84
- stop_details: null,
85
- usage: {
86
- input_tokens: 0,
87
- output_tokens: 0,
88
- cache_creation_input_tokens: 0,
89
- cache_read_input_tokens: 0,
90
- output_tokens_details: null,
91
- cache_creation: null,
92
- inference_geo: null,
93
- iterations: null,
94
- server_tool_use: null,
95
- service_tier: null,
96
- speed: null,
97
- },
98
- container: null,
99
- context_management: null,
100
- diagnostics: null,
101
- };
102
- }
103
- function parseUsage(usage) {
104
- if (!usage) {
105
- return {
106
- inputTokens: 0,
107
- outputTokens: 0,
108
- cachedInputTokens: 0,
109
- };
110
- }
111
- return {
112
- inputTokens: toFiniteNumber(usage.input_tokens),
113
- outputTokens: toFiniteNumber(usage.output_tokens),
114
- cachedInputTokens: toFiniteNumber(usage.cached_input_tokens),
115
- };
116
- }
117
- function createResultUsage(parsed) {
118
- return {
119
- input_tokens: parsed.inputTokens,
120
- output_tokens: parsed.outputTokens,
121
- cache_creation_input_tokens: 0,
122
- cache_read_input_tokens: parsed.cachedInputTokens,
123
- output_tokens_details: { thinking_tokens: 0 },
124
- cache_creation: {
125
- ephemeral_1h_input_tokens: 0,
126
- ephemeral_5m_input_tokens: 0,
127
- },
128
- inference_geo: "unknown",
129
- iterations: [],
130
- server_tool_use: {
131
- web_fetch_requests: 0,
132
- web_search_requests: 0,
133
- },
134
- service_tier: "standard",
135
- speed: "standard",
136
- };
137
- }
138
- function getDefaultReasoningEffortForModel(model) {
139
- // All gpt-5 variants (including plain "gpt-5") reject xhigh; pin to "high".
140
- return /^gpt-5/i.test(model || "") ? "high" : undefined;
141
- }
142
- function normalizeError(error) {
143
- if (error instanceof Error) {
144
- return error.message;
145
- }
146
- if (typeof error === "string") {
147
- return error;
148
- }
149
- return "Codex execution failed";
150
- }
151
- function inferCommandToolName(command) {
152
- const normalized = command.toLowerCase();
153
- if (/\brg\b|\bgrep\b/.test(normalized)) {
154
- return "Grep";
155
- }
156
- if (/\bglob\.glob\b|\bfind\b.+\s-name\s/.test(normalized)) {
157
- return "Glob";
158
- }
159
- if (/\bcat\b/.test(normalized) && !/>/.test(normalized)) {
160
- return "Read";
161
- }
162
- if (/<<\s*['"]?eof['"]?\s*>/i.test(command) ||
163
- /\becho\b.+>/.test(normalized)) {
164
- return "Write";
165
- }
166
- return "Bash";
167
- }
168
- function normalizeFilePath(path, workingDirectory) {
169
- if (!path) {
170
- return path;
171
- }
172
- if (workingDirectory && path.startsWith(workingDirectory)) {
173
- const relativePath = pathRelative(workingDirectory, path);
174
- if (relativePath && relativePath !== ".") {
175
- return relativePath;
176
- }
177
- }
178
- return path;
179
- }
180
- function summarizeFileChanges(item, workingDirectory) {
181
- if (!item.changes.length) {
182
- return item.status === "failed" ? "Patch failed" : "No file changes";
183
- }
184
- return item.changes
185
- .map((change) => {
186
- const filePath = normalizeFilePath(change.path, workingDirectory);
187
- return `${change.kind} ${filePath}`;
188
- })
189
- .join("\n");
190
- }
191
- function asRecord(value) {
192
- if (value && typeof value === "object") {
193
- return value;
194
- }
195
- return null;
196
- }
197
- function toMcpResultString(item) {
198
- if (item.error?.message) {
199
- return item.error.message;
200
- }
201
- const textBlocks = [];
202
- for (const block of item.result?.content || []) {
203
- const text = asRecord(block)?.text;
204
- if (typeof text === "string" && text.trim().length > 0) {
205
- textBlocks.push(text);
206
- }
207
- }
208
- if (textBlocks.length > 0) {
209
- return textBlocks.join("\n");
210
- }
211
- if (item.result?.structured_content !== undefined) {
212
- return safeStringify(item.result.structured_content);
213
- }
214
- return item.status === "failed"
215
- ? "MCP tool call failed"
216
- : "MCP tool call completed";
217
- }
218
- function normalizeMcpIdentifier(value) {
219
- const normalized = value
220
- .toLowerCase()
221
- .replace(/[^a-z0-9_]+/g, "_")
222
- .replace(/^_+|_+$/g, "");
223
- return normalized || "unknown";
224
- }
225
- function autoDetectMcpConfigPath(workingDirectory) {
226
- if (!workingDirectory) {
227
- return undefined;
228
- }
229
- const mcpPath = join(workingDirectory, ".mcp.json");
230
- if (!existsSync(mcpPath)) {
231
- return undefined;
232
- }
233
- try {
234
- JSON.parse(readFileSync(mcpPath, "utf8"));
235
- return mcpPath;
236
- }
237
- catch {
238
- console.warn(`[CodexRunner] Found .mcp.json at ${mcpPath} but it is invalid JSON, skipping`);
239
- return undefined;
240
- }
241
- }
242
- function loadMcpConfigFromPaths(configPaths) {
243
- if (!configPaths) {
244
- return {};
245
- }
246
- const paths = Array.isArray(configPaths) ? configPaths : [configPaths];
247
- let mcpServers = {};
248
- for (const configPath of paths) {
249
- try {
250
- const mcpConfigContent = readFileSync(configPath, "utf8");
251
- const mcpConfig = JSON.parse(mcpConfigContent);
252
- const servers = mcpConfig &&
253
- typeof mcpConfig === "object" &&
254
- !Array.isArray(mcpConfig) &&
255
- mcpConfig.mcpServers &&
256
- typeof mcpConfig.mcpServers === "object" &&
257
- !Array.isArray(mcpConfig.mcpServers)
258
- ? mcpConfig.mcpServers
259
- : {};
260
- mcpServers = { ...mcpServers, ...servers };
261
- console.log(`[CodexRunner] Loaded MCP config from ${configPath}: ${Object.keys(servers).join(", ")}`);
262
- }
263
- catch (error) {
264
- console.warn(`[CodexRunner] Failed to load MCP config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
265
- }
266
- }
267
- return mcpServers;
268
- }
269
8
  /**
270
- * Runner that adapts Codex SDK streaming output to Cyrus SDK message types.
9
+ * Adapts Codex to Cyrus's {@link IAgentRunner} contract.
10
+ *
11
+ * The runner is a thin orchestrator: it owns session lifecycle and delegates
12
+ * configuration assembly ({@link CodexConfigBuilder}), skill staging
13
+ * ({@link CodexSkillStager}), event→message mapping ({@link CodexEventMapper}),
14
+ * and transport ({@link CodexBackend}) to dedicated collaborators. Codex is
15
+ * driven exclusively through the app-server backend, which supports mid-turn
16
+ * input injection (`turn/steer`).
271
17
  */
272
18
  export class CodexRunner extends EventEmitter {
273
- supportsStreamingInput = false;
19
+ supportsStreamingInput = true;
274
20
  config;
275
- sessionInfo = null;
276
- messages = [];
277
21
  formatter;
278
- hasInitMessage = false;
279
- pendingResultMessage = null;
280
- lastAssistantText = null;
281
- lastUsage = {
282
- inputTokens: 0,
283
- outputTokens: 0,
284
- cachedInputTokens: 0,
285
- };
286
- errorMessages = [];
287
- startTimestampMs = 0;
22
+ skillStager;
23
+ mapper;
24
+ sessionInfo = null;
25
+ backend = null;
288
26
  wasStopped = false;
289
- abortController = null;
290
- emittedToolUseIds = new Set();
27
+ /** Set once the turn reaches a terminal state; gates {@link isStreaming}. */
28
+ turnFinished = false;
29
+ /**
30
+ * Follow-up messages that arrived before the turn became steerable (during
31
+ * config build / process spawn / thread start). Flushed via `steer` once the
32
+ * turn starts, so a fast follow-up is never lost or wrongly deferred.
33
+ */
34
+ pendingFollowups = [];
291
35
  constructor(config) {
292
36
  super();
293
37
  this.config = config;
294
38
  this.formatter = new CodexMessageFormatter();
39
+ this.skillStager = new CodexSkillStager({
40
+ workingDirectory: config.workingDirectory,
41
+ additionalDirectories: config.additionalDirectories,
42
+ skills: config.skills,
43
+ plugins: config.plugins,
44
+ });
45
+ this.mapper = new CodexEventMapper(this.buildMapperContext());
295
46
  if (config.onMessage)
296
47
  this.on("message", config.onMessage);
297
48
  if (config.onError)
@@ -303,49 +54,79 @@ export class CodexRunner extends EventEmitter {
303
54
  return this.startWithPrompt(prompt);
304
55
  }
305
56
  async startStreaming(initialPrompt) {
306
- return this.startWithPrompt(null, initialPrompt);
57
+ return this.startWithPrompt(initialPrompt);
307
58
  }
308
- addStreamMessage(_content) {
309
- throw new Error("CodexRunner does not support streaming input messages");
59
+ /**
60
+ * Inject a message mid-session. While a turn is steerable it is sent
61
+ * immediately (`turn/steer`); during the startup window (before the turn
62
+ * begins) it is buffered and flushed once the turn starts. Throws only once
63
+ * the turn has finished, where the caller should resume with a new turn.
64
+ */
65
+ addStreamMessage(content) {
66
+ if (this.backend?.isTurnActive()) {
67
+ this.steer(content);
68
+ return;
69
+ }
70
+ if (this.isRunning() && !this.turnFinished) {
71
+ this.pendingFollowups.push(content);
72
+ return;
73
+ }
74
+ throw new Error("Cannot stream message: no active Codex turn");
310
75
  }
311
76
  completeStream() {
312
- // No-op: CodexRunner does not support streaming input.
77
+ // No-op: each turn's input is delivered up front (or via steer); there is
78
+ // no open input stream to close.
79
+ }
80
+ isStreaming() {
81
+ // True for the whole running, not-yet-finished window — including the
82
+ // startup gap before the turn is active — so callers stream follow-ups in
83
+ // (buffered if needed) rather than deferring them.
84
+ return (this.supportsStreamingInput && this.isRunning() && !this.turnFinished);
85
+ }
86
+ stop() {
87
+ if (this.sessionInfo?.isRunning) {
88
+ this.wasStopped = true;
89
+ }
90
+ this.cleanupRuntimeState();
91
+ }
92
+ isRunning() {
93
+ return this.sessionInfo?.isRunning ?? false;
313
94
  }
314
- async startWithPrompt(stringPrompt, streamingInitialPrompt) {
95
+ getMessages() {
96
+ return this.mapper.getMessages();
97
+ }
98
+ getFormatter() {
99
+ return this.formatter;
100
+ }
101
+ // ---- internals ----------------------------------------------------------
102
+ async startWithPrompt(prompt) {
315
103
  if (this.isRunning()) {
316
104
  throw new Error("Codex session already running");
317
105
  }
318
- const sessionId = this.config.resumeSessionId || crypto.randomUUID();
319
106
  this.sessionInfo = {
320
- sessionId,
107
+ sessionId: this.config.resumeSessionId || crypto.randomUUID(),
321
108
  startedAt: new Date(),
322
109
  isRunning: true,
323
110
  };
324
- this.messages = [];
325
- this.hasInitMessage = false;
326
- this.pendingResultMessage = null;
327
- this.lastAssistantText = null;
328
- this.lastUsage = {
329
- inputTokens: 0,
330
- outputTokens: 0,
331
- cachedInputTokens: 0,
332
- };
333
- this.errorMessages = [];
334
111
  this.wasStopped = false;
335
- this.startTimestampMs = Date.now();
336
- this.emittedToolUseIds.clear();
337
- await this.resolveModelWithFallback();
338
- const prompt = (stringPrompt ?? streamingInitialPrompt ?? "").trim();
339
- const threadOptions = this.buildThreadOptions();
340
- const codex = this.createCodexClient();
341
- const thread = this.config.resumeSessionId
342
- ? codex.resumeThread(this.config.resumeSessionId, threadOptions)
343
- : codex.startThread(threadOptions);
344
- const abortController = new AbortController();
345
- this.abortController = abortController;
112
+ this.turnFinished = false;
113
+ this.pendingFollowups = [];
114
+ this.mapper.reset();
115
+ // Create the backend up front (before the slow config build / process
116
+ // spawn) so addStreamMessage can buffer follow-ups that arrive during the
117
+ // startup window rather than throwing.
118
+ const backend = this.createBackend();
119
+ this.backend = backend;
120
+ backend.on("event", (event) => this.handleBackendEvent(event));
121
+ const resolved = await new CodexConfigBuilder(this.config).build();
122
+ this.skillStager.stage();
123
+ const input = prompt?.trim()
124
+ ? [{ type: "text", text: prompt.trim() }]
125
+ : [];
346
126
  let caughtError;
347
127
  try {
348
- await this.runTurn(thread, prompt, abortController.signal);
128
+ await backend.open(resolved);
129
+ await backend.runTurn(input);
349
130
  }
350
131
  catch (error) {
351
132
  caughtError = error;
@@ -355,440 +136,52 @@ export class CodexRunner extends EventEmitter {
355
136
  }
356
137
  return this.sessionInfo;
357
138
  }
358
- /**
359
- * Check if the configured model is accessible via the OpenAI API.
360
- * If not, swap to the fallback model before starting the session.
361
- *
362
- * Skipped when:
363
- * - No OPENAI_API_KEY is set (Codex-native auth handles model access)
364
- * - The user has a ChatGPT subscription (`codex login status` reports "Logged in using ChatGPT")
365
- */
366
- async resolveModelWithFallback() {
367
- const model = this.config.model;
368
- const fallback = this.config.fallbackModel;
369
- if (!model || !fallback || fallback === model)
370
- return;
371
- const apiKey = process.env.OPENAI_API_KEY;
372
- if (!apiKey)
373
- return;
374
- if (await this.hasCodexSubscription())
375
- return;
376
- const baseUrl = (process.env.OPENAI_BASE_URL ||
377
- process.env.OPENAI_API_BASE ||
378
- "https://api.openai.com/v1").replace(/\/+$/, "");
379
- try {
380
- const response = await fetch(`${baseUrl}/models/${encodeURIComponent(model)}`, {
381
- method: "GET",
382
- headers: { Authorization: `Bearer ${apiKey}` },
383
- signal: AbortSignal.timeout(10_000),
384
- });
385
- if (response.status === 404) {
386
- console.log(`[CodexRunner] Model "${model}" not found (404), falling back to "${fallback}"`);
387
- this.config.model = fallback;
388
- }
389
- }
390
- catch {
391
- // Network error or timeout — proceed with the original model
392
- // and let the Codex SDK handle any downstream failure.
393
- }
139
+ createBackend() {
140
+ return new AppServerCodexBackend();
394
141
  }
395
- /**
396
- * Check if the user has a ChatGPT/Codex subscription by running `codex login status`.
397
- * Returns true when the output contains "Logged in using ChatGPT",
398
- * meaning the user has native Codex auth and can access the default Codex model.
399
- */
400
- async hasCodexSubscription() {
401
- const codexBin = this.config.codexPath || "codex";
402
- try {
403
- const { execFile } = await import("node:child_process");
404
- const { promisify } = await import("node:util");
405
- const execFileAsync = promisify(execFile);
406
- const { stdout, stderr } = await execFileAsync(codexBin, ["login", "status"], { timeout: 5_000 });
407
- const result = /logged in using chatgpt/i.test(stdout + stderr);
408
- console.log(`[CodexRunner] hasCodexSubscription: ${result} (stdout: "${stdout.trim()}"${stderr.trim() ? `, stderr: "${stderr.trim()}"` : ""})`);
409
- return result;
142
+ handleBackendEvent(event) {
143
+ if (event.kind === "turn-started") {
144
+ // Turn is now steerable deliver anything buffered during startup.
145
+ this.flushPendingFollowups();
410
146
  }
411
- catch (error) {
412
- console.warn(`[CodexRunner] hasCodexSubscription error (returning false): ${error instanceof Error ? error.message : String(error)}`);
413
- return false;
147
+ else if (event.kind === "turn-completed" ||
148
+ event.kind === "turn-failed") {
149
+ this.turnFinished = true;
414
150
  }
151
+ this.mapper.handle(event);
415
152
  }
416
- createCodexClient() {
417
- const codexHome = this.resolveCodexHome();
418
- const envOverride = this.buildEnvOverride(codexHome);
419
- const configOverrides = this.buildConfigOverrides();
420
- return new Codex({
421
- ...(this.config.codexPath
422
- ? { codexPathOverride: this.config.codexPath }
423
- : {}),
424
- ...(envOverride ? { env: envOverride } : {}),
425
- ...(configOverrides ? { config: configOverrides } : {}),
153
+ steer(content) {
154
+ void this.backend
155
+ ?.steer?.([{ type: "text", text: content }])
156
+ .catch((error) => {
157
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
426
158
  });
427
159
  }
428
- buildThreadOptions() {
429
- const additionalDirectories = this.getAdditionalDirectories();
430
- const reasoningEffort = this.config.modelReasoningEffort ??
431
- getDefaultReasoningEffortForModel(this.config.model);
432
- const webSearchMode = this.config.webSearchMode ??
433
- (this.config.includeWebSearch ? "live" : undefined);
434
- const threadOptions = {
435
- model: this.config.model,
436
- sandboxMode: this.config.sandbox || "workspace-write",
437
- workingDirectory: this.config.workingDirectory,
438
- skipGitRepoCheck: this.config.skipGitRepoCheck ?? true,
439
- approvalPolicy: this.config.askForApproval || "never",
440
- ...(reasoningEffort ? { modelReasoningEffort: reasoningEffort } : {}),
441
- ...(webSearchMode ? { webSearchMode } : {}),
442
- ...(additionalDirectories.length > 0 ? { additionalDirectories } : {}),
443
- };
444
- return threadOptions;
445
- }
446
- getAdditionalDirectories() {
447
- const workingDirectory = this.config.workingDirectory;
448
- const uniqueDirectories = new Set();
449
- for (const directory of this.config.allowedDirectories || []) {
450
- if (!directory || directory === workingDirectory) {
451
- continue;
452
- }
453
- uniqueDirectories.add(directory);
160
+ flushPendingFollowups() {
161
+ const queued = this.pendingFollowups;
162
+ this.pendingFollowups = [];
163
+ for (const content of queued) {
164
+ this.steer(content);
454
165
  }
455
- return [...uniqueDirectories];
456
- }
457
- resolveCodexHome() {
458
- const codexHome = this.config.codexHome ||
459
- process.env.CODEX_HOME ||
460
- join(homedir(), ".codex");
461
- mkdirSync(codexHome, { recursive: true });
462
- return codexHome;
463
166
  }
464
- buildEnvOverride(codexHome) {
465
- if (!this.config.codexHome) {
466
- return undefined;
467
- }
468
- const env = {};
469
- for (const [key, value] of Object.entries(process.env)) {
470
- if (typeof value === "string") {
471
- env[key] = value;
472
- }
473
- }
474
- env.CODEX_HOME = codexHome;
475
- return env;
476
- }
477
- buildCodexMcpServersConfig() {
478
- const autoDetectedPath = autoDetectMcpConfigPath(this.config.workingDirectory);
479
- const configPaths = autoDetectedPath
480
- ? [autoDetectedPath]
481
- : [];
482
- if (this.config.mcpConfigPath) {
483
- const explicitPaths = Array.isArray(this.config.mcpConfigPath)
484
- ? this.config.mcpConfigPath
485
- : [this.config.mcpConfigPath];
486
- configPaths.push(...explicitPaths);
487
- }
488
- const fileBasedServers = loadMcpConfigFromPaths(configPaths);
489
- const mergedServers = this.config.mcpConfig
490
- ? { ...fileBasedServers, ...this.config.mcpConfig }
491
- : fileBasedServers;
492
- if (Object.keys(mergedServers).length === 0) {
493
- return undefined;
494
- }
495
- // Codex MCP configuration reference:
496
- // https://platform.openai.com/docs/docs-mcp
497
- const codexServers = {};
498
- for (const [serverName, rawConfig] of Object.entries(mergedServers)) {
499
- const configAny = rawConfig;
500
- if (typeof configAny.listTools === "function" ||
501
- typeof configAny.callTool === "function") {
502
- console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because in-process SDK server instances cannot be mapped to codex config`);
503
- continue;
504
- }
505
- const mapped = {};
506
- if (typeof configAny.command === "string") {
507
- mapped.command = configAny.command;
508
- }
509
- if (Array.isArray(configAny.args)) {
510
- mapped.args =
511
- configAny.args;
512
- }
513
- if (configAny.env &&
514
- typeof configAny.env === "object" &&
515
- !Array.isArray(configAny.env)) {
516
- mapped.env =
517
- configAny.env;
518
- }
519
- if (typeof configAny.cwd === "string") {
520
- mapped.cwd = configAny.cwd;
521
- }
522
- if (typeof configAny.url === "string") {
523
- mapped.url = configAny.url;
524
- }
525
- if (configAny.http_headers &&
526
- typeof configAny.http_headers === "object" &&
527
- !Array.isArray(configAny.http_headers)) {
528
- mapped.http_headers =
529
- configAny.http_headers;
530
- }
531
- if (configAny.headers &&
532
- typeof configAny.headers === "object" &&
533
- !Array.isArray(configAny.headers)) {
534
- mapped.http_headers =
535
- configAny.headers;
536
- }
537
- if (configAny.env_http_headers &&
538
- typeof configAny.env_http_headers === "object" &&
539
- !Array.isArray(configAny.env_http_headers)) {
540
- mapped.env_http_headers =
541
- configAny.env_http_headers;
542
- }
543
- if (typeof configAny.bearer_token_env_var === "string") {
544
- mapped.bearer_token_env_var = configAny.bearer_token_env_var;
545
- }
546
- if (typeof configAny.timeout === "number") {
547
- mapped.timeout = configAny.timeout;
548
- }
549
- if (!mapped.command && !mapped.url) {
550
- console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because it has no command/url transport`);
551
- continue;
552
- }
553
- codexServers[serverName] = mapped;
554
- }
555
- if (Object.keys(codexServers).length === 0) {
556
- return undefined;
557
- }
558
- console.log(`[CodexRunner] Configured ${Object.keys(codexServers).length} MCP server(s) for codex config (docs: ${CODEX_MCP_DOCS_URL})`);
559
- return codexServers;
560
- }
561
- buildConfigOverrides() {
562
- const appendSystemPrompt = (this.config.appendSystemPrompt ?? "").trim();
563
- const configOverrides = this.config.configOverrides
564
- ? { ...this.config.configOverrides }
565
- : {};
566
- const mcpServers = this.buildCodexMcpServersConfig();
567
- if (mcpServers) {
568
- const existingMcpServers = configOverrides.mcp_servers;
569
- if (existingMcpServers &&
570
- typeof existingMcpServers === "object" &&
571
- !Array.isArray(existingMcpServers)) {
572
- configOverrides.mcp_servers = {
573
- ...existingMcpServers,
574
- ...mcpServers,
575
- };
576
- }
577
- else {
578
- configOverrides.mcp_servers = mcpServers;
579
- }
580
- }
581
- const sandboxWorkspaceWrite = configOverrides.sandbox_workspace_write;
582
- // Keep workspace-write as the default sandbox, but enable outbound network so
583
- // common remote workflows (for example `git`/`gh` against GitHub) work without
584
- // requiring danger-full-access.
585
- if (sandboxWorkspaceWrite &&
586
- typeof sandboxWorkspaceWrite === "object" &&
587
- !Array.isArray(sandboxWorkspaceWrite)) {
588
- configOverrides.sandbox_workspace_write = {
589
- ...sandboxWorkspaceWrite,
590
- network_access: sandboxWorkspaceWrite
591
- .network_access ?? true,
592
- };
593
- }
594
- else if (!sandboxWorkspaceWrite) {
595
- configOverrides.sandbox_workspace_write = { network_access: true };
596
- }
597
- if (!appendSystemPrompt) {
598
- return Object.keys(configOverrides).length > 0
599
- ? configOverrides
600
- : undefined;
601
- }
167
+ buildMapperContext() {
168
+ const self = this;
602
169
  return {
603
- ...configOverrides,
604
- developer_instructions: appendSystemPrompt,
605
- };
606
- }
607
- async runTurn(thread, prompt, signal) {
608
- const streamedTurn = await thread.runStreamed(prompt, {
609
- signal,
610
- ...(this.config.outputSchema
611
- ? { outputSchema: this.config.outputSchema }
612
- : {}),
613
- });
614
- for await (const event of streamedTurn.events) {
615
- this.handleEvent(event);
616
- }
617
- }
618
- handleEvent(event) {
619
- this.emit("streamEvent", event);
620
- switch (event.type) {
621
- case "thread.started": {
622
- if (this.sessionInfo) {
623
- this.sessionInfo.sessionId = event.thread_id;
170
+ get workingDirectory() {
171
+ return self.config.workingDirectory;
172
+ },
173
+ get model() {
174
+ return self.config.model;
175
+ },
176
+ getSessionId: () => self.sessionInfo?.sessionId || "pending",
177
+ getStagedSkillNames: () => self.skillStager.getStagedSkillNames(),
178
+ emitMessage: (message) => self.emit("message", message),
179
+ onThreadStarted: (threadId) => {
180
+ if (self.sessionInfo) {
181
+ self.sessionInfo.sessionId = threadId;
624
182
  }
625
- this.emitSystemInitMessage(event.thread_id);
626
- break;
627
- }
628
- case "item.completed": {
629
- if (event.item.type === "agent_message") {
630
- this.emitAssistantMessage(event.item.text);
631
- }
632
- else {
633
- this.emitToolMessagesForItem(event.item, true);
634
- }
635
- break;
636
- }
637
- case "item.started": {
638
- this.emitToolMessagesForItem(event.item, false);
639
- break;
640
- }
641
- case "turn.completed": {
642
- this.lastUsage = parseUsage(event.usage);
643
- this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
644
- break;
645
- }
646
- case "turn.failed": {
647
- // Prefer event.error.message; fallback to last standalone "error" event
648
- const message = event.error?.message ||
649
- this.errorMessages.at(-1) ||
650
- "Codex execution failed";
651
- this.errorMessages.push(message);
652
- this.pendingResultMessage = this.createErrorResultMessage(message);
653
- break;
654
- }
655
- case "error": {
656
- this.errorMessages.push(event.message);
657
- break;
658
- }
659
- default:
660
- break;
661
- }
662
- }
663
- projectItemToTool(item) {
664
- switch (item.type) {
665
- case "command_execution": {
666
- const commandItem = item;
667
- const isError = commandItem.status === "failed" ||
668
- (typeof commandItem.exit_code === "number" &&
669
- commandItem.exit_code !== 0);
670
- const result = commandItem.aggregated_output?.trim() ||
671
- (isError
672
- ? `Command failed (exit code ${commandItem.exit_code ?? "unknown"})`
673
- : "Command completed with no output");
674
- return {
675
- toolUseId: commandItem.id,
676
- toolName: inferCommandToolName(commandItem.command),
677
- toolInput: { command: commandItem.command },
678
- result,
679
- isError,
680
- };
681
- }
682
- case "file_change": {
683
- const fileChangeItem = item;
684
- const primaryPath = fileChangeItem.changes[0]?.path &&
685
- normalizeFilePath(fileChangeItem.changes[0].path, this.config.workingDirectory);
686
- return {
687
- toolUseId: fileChangeItem.id,
688
- toolName: "Edit",
689
- toolInput: {
690
- ...(primaryPath ? { file_path: primaryPath } : {}),
691
- changes: fileChangeItem.changes.map((change) => ({
692
- kind: change.kind,
693
- path: normalizeFilePath(change.path, this.config.workingDirectory),
694
- })),
695
- },
696
- result: summarizeFileChanges(fileChangeItem, this.config.workingDirectory),
697
- isError: fileChangeItem.status === "failed",
698
- };
699
- }
700
- case "web_search": {
701
- const webSearchItem = item;
702
- const extendedItem = item;
703
- const action = asRecord(extendedItem.action);
704
- const actionType = typeof action?.type === "string" ? action.type : undefined;
705
- const isFetch = actionType === "open_page";
706
- const url = typeof action?.url === "string"
707
- ? action.url
708
- : typeof extendedItem.url === "string"
709
- ? extendedItem.url
710
- : undefined;
711
- const pattern = typeof action?.pattern === "string"
712
- ? action.pattern
713
- : typeof extendedItem.pattern === "string"
714
- ? extendedItem.pattern
715
- : undefined;
716
- return {
717
- toolUseId: webSearchItem.id,
718
- toolName: isFetch ? "WebFetch" : "WebSearch",
719
- toolInput: isFetch
720
- ? {
721
- url: url || webSearchItem.query,
722
- ...(pattern ? { pattern } : {}),
723
- }
724
- : { query: webSearchItem.query },
725
- result: action && Object.keys(action).length > 0
726
- ? safeStringify(action)
727
- : `Search completed for query: ${webSearchItem.query}`,
728
- isError: false,
729
- };
730
- }
731
- case "mcp_tool_call": {
732
- const mcpItem = item;
733
- return {
734
- toolUseId: mcpItem.id,
735
- toolName: `mcp__${normalizeMcpIdentifier(mcpItem.server)}__${normalizeMcpIdentifier(mcpItem.tool)}`,
736
- toolInput: asRecord(mcpItem.arguments) || {
737
- arguments: mcpItem.arguments,
738
- },
739
- result: toMcpResultString(mcpItem),
740
- isError: mcpItem.status === "failed" || Boolean(mcpItem.error),
741
- };
742
- }
743
- case "todo_list": {
744
- const todoItem = item;
745
- return {
746
- toolUseId: todoItem.id,
747
- toolName: "TodoWrite",
748
- toolInput: {
749
- todos: todoItem.items.map((todo) => ({
750
- content: todo.text,
751
- status: todo.completed ? "completed" : "pending",
752
- })),
753
- },
754
- result: `Updated todo list (${todoItem.items.length} items)`,
755
- isError: false,
756
- };
757
- }
758
- default:
759
- return null;
760
- }
761
- }
762
- emitToolMessagesForItem(item, includeResult) {
763
- const projection = this.projectItemToTool(item);
764
- if (!projection) {
765
- return;
766
- }
767
- if (!this.emittedToolUseIds.has(projection.toolUseId)) {
768
- const assistantMessage = {
769
- type: "assistant",
770
- message: createAssistantToolUseMessage(projection.toolUseId, projection.toolName, projection.toolInput),
771
- parent_tool_use_id: null,
772
- uuid: crypto.randomUUID(),
773
- session_id: this.sessionInfo?.sessionId || "pending",
774
- };
775
- this.messages.push(assistantMessage);
776
- this.emit("message", assistantMessage);
777
- this.emittedToolUseIds.add(projection.toolUseId);
778
- }
779
- if (!includeResult) {
780
- return;
781
- }
782
- const userMessage = {
783
- type: "user",
784
- message: createUserToolResultMessage(projection.toolUseId, projection.result, projection.isError),
785
- parent_tool_use_id: null,
786
- uuid: crypto.randomUUID(),
787
- session_id: this.sessionInfo?.sessionId || "pending",
183
+ },
788
184
  };
789
- this.messages.push(userMessage);
790
- this.emit("message", userMessage);
791
- this.emittedToolUseIds.delete(projection.toolUseId);
792
185
  }
793
186
  finalizeSession(caughtError) {
794
187
  if (!this.sessionInfo) {
@@ -796,128 +189,20 @@ export class CodexRunner extends EventEmitter {
796
189
  return;
797
190
  }
798
191
  this.sessionInfo.isRunning = false;
799
- // Ensure init is emitted even if stream fails before thread.started.
800
- if (!this.hasInitMessage) {
801
- this.emitSystemInitMessage(this.sessionInfo.sessionId || this.config.resumeSessionId || "pending");
802
- }
803
- if (caughtError && !this.wasStopped) {
804
- const errorMessage = normalizeError(caughtError);
805
- this.errorMessages.push(errorMessage);
806
- }
807
- if (!this.pendingResultMessage && !this.wasStopped) {
808
- if (caughtError) {
809
- this.pendingResultMessage = this.createErrorResultMessage(this.errorMessages.at(-1) || "Codex execution failed");
810
- }
811
- else {
812
- this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
813
- }
814
- }
815
- if (this.pendingResultMessage) {
816
- this.messages.push(this.pendingResultMessage);
817
- this.emit("message", this.pendingResultMessage);
818
- this.pendingResultMessage = null;
819
- }
820
- this.emit("complete", [...this.messages]);
192
+ const messages = this.mapper.finalize({
193
+ caughtError,
194
+ wasStopped: this.wasStopped,
195
+ });
196
+ this.emit("complete", messages);
821
197
  this.cleanupRuntimeState();
822
198
  }
823
- emitAssistantMessage(text) {
824
- const normalized = text.trim();
825
- if (!normalized) {
826
- return;
827
- }
828
- this.lastAssistantText = normalized;
829
- const assistantMessage = {
830
- type: "assistant",
831
- message: createAssistantBetaMessage(normalized),
832
- parent_tool_use_id: null,
833
- uuid: crypto.randomUUID(),
834
- session_id: this.sessionInfo?.sessionId || "pending",
835
- };
836
- this.messages.push(assistantMessage);
837
- this.emit("message", assistantMessage);
838
- }
839
- emitSystemInitMessage(sessionId) {
840
- if (this.hasInitMessage) {
841
- return;
842
- }
843
- this.hasInitMessage = true;
844
- const initMessage = {
845
- type: "system",
846
- subtype: "init",
847
- agents: undefined,
848
- apiKeySource: "user",
849
- claude_code_version: "codex-cli",
850
- cwd: this.config.workingDirectory || cwd(),
851
- tools: [],
852
- mcp_servers: [],
853
- model: this.config.model || DEFAULT_CODEX_MODEL,
854
- permissionMode: "default",
855
- slash_commands: [],
856
- output_style: "default",
857
- skills: [],
858
- plugins: [],
859
- uuid: crypto.randomUUID(),
860
- session_id: sessionId,
861
- };
862
- this.messages.push(initMessage);
863
- this.emit("message", initMessage);
864
- }
865
- createSuccessResultMessage(result) {
866
- const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
867
- return {
868
- type: "result",
869
- subtype: "success",
870
- duration_ms: durationMs,
871
- duration_api_ms: 0,
872
- is_error: false,
873
- num_turns: 1,
874
- result,
875
- stop_reason: null,
876
- total_cost_usd: 0,
877
- usage: createResultUsage(this.lastUsage),
878
- modelUsage: {},
879
- permission_denials: [],
880
- uuid: crypto.randomUUID(),
881
- session_id: this.sessionInfo?.sessionId || "pending",
882
- };
883
- }
884
- createErrorResultMessage(errorMessage) {
885
- const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
886
- return {
887
- type: "result",
888
- subtype: "error_during_execution",
889
- duration_ms: durationMs,
890
- duration_api_ms: 0,
891
- is_error: true,
892
- num_turns: 1,
893
- stop_reason: null,
894
- errors: [errorMessage],
895
- total_cost_usd: 0,
896
- usage: createResultUsage(this.lastUsage),
897
- modelUsage: {},
898
- permission_denials: [],
899
- uuid: crypto.randomUUID(),
900
- session_id: this.sessionInfo?.sessionId || "pending",
901
- };
902
- }
903
199
  cleanupRuntimeState() {
904
- this.abortController = null;
905
- }
906
- stop() {
907
- if (!this.sessionInfo?.isRunning) {
908
- return;
200
+ const backend = this.backend;
201
+ this.backend = null;
202
+ if (backend) {
203
+ void backend.close();
909
204
  }
910
- this.wasStopped = true;
911
- this.abortController?.abort();
912
- }
913
- isRunning() {
914
- return this.sessionInfo?.isRunning ?? false;
915
- }
916
- getMessages() {
917
- return [...this.messages];
918
- }
919
- getFormatter() {
920
- return this.formatter;
205
+ this.skillStager.cleanup();
921
206
  }
922
207
  }
923
208
  //# sourceMappingURL=CodexRunner.js.map