cyrus-codex-runner 0.2.22

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.
@@ -0,0 +1,852 @@
1
+ import crypto from "node:crypto";
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";
8
+ import { CodexMessageFormatter } from "./formatter.js";
9
+ const DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
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
+ usage: {
40
+ input_tokens: 0,
41
+ output_tokens: 0,
42
+ cache_creation_input_tokens: 0,
43
+ cache_read_input_tokens: 0,
44
+ cache_creation: null,
45
+ inference_geo: null,
46
+ iterations: null,
47
+ server_tool_use: null,
48
+ service_tier: null,
49
+ speed: null,
50
+ },
51
+ container: null,
52
+ context_management: null,
53
+ };
54
+ }
55
+ function createUserToolResultMessage(toolUseId, result, isError) {
56
+ const contentBlocks = [
57
+ {
58
+ type: "tool_result",
59
+ tool_use_id: toolUseId,
60
+ content: result,
61
+ is_error: isError,
62
+ },
63
+ ];
64
+ return {
65
+ role: "user",
66
+ content: contentBlocks,
67
+ };
68
+ }
69
+ function createAssistantBetaMessage(content, messageId = crypto.randomUUID()) {
70
+ const contentBlocks = [
71
+ { type: "text", text: content },
72
+ ];
73
+ return {
74
+ id: messageId,
75
+ type: "message",
76
+ role: "assistant",
77
+ content: contentBlocks,
78
+ model: DEFAULT_CODEX_MODEL,
79
+ stop_reason: null,
80
+ stop_sequence: null,
81
+ usage: {
82
+ input_tokens: 0,
83
+ output_tokens: 0,
84
+ cache_creation_input_tokens: 0,
85
+ cache_read_input_tokens: 0,
86
+ cache_creation: null,
87
+ inference_geo: null,
88
+ iterations: null,
89
+ server_tool_use: null,
90
+ service_tier: null,
91
+ speed: null,
92
+ },
93
+ container: null,
94
+ context_management: null,
95
+ };
96
+ }
97
+ function parseUsage(usage) {
98
+ if (!usage) {
99
+ return {
100
+ inputTokens: 0,
101
+ outputTokens: 0,
102
+ cachedInputTokens: 0,
103
+ };
104
+ }
105
+ return {
106
+ inputTokens: toFiniteNumber(usage.input_tokens),
107
+ outputTokens: toFiniteNumber(usage.output_tokens),
108
+ cachedInputTokens: toFiniteNumber(usage.cached_input_tokens),
109
+ };
110
+ }
111
+ function createResultUsage(parsed) {
112
+ return {
113
+ input_tokens: parsed.inputTokens,
114
+ output_tokens: parsed.outputTokens,
115
+ cache_creation_input_tokens: 0,
116
+ cache_read_input_tokens: parsed.cachedInputTokens,
117
+ cache_creation: {
118
+ ephemeral_1h_input_tokens: 0,
119
+ ephemeral_5m_input_tokens: 0,
120
+ },
121
+ inference_geo: "unknown",
122
+ iterations: [],
123
+ server_tool_use: {
124
+ web_fetch_requests: 0,
125
+ web_search_requests: 0,
126
+ },
127
+ service_tier: "standard",
128
+ speed: "standard",
129
+ };
130
+ }
131
+ function getDefaultReasoningEffortForModel(model) {
132
+ // gpt-5 codex variants reject xhigh in some environments; pin a compatible default.
133
+ return /gpt-5[a-z0-9.-]*codex$/i.test(model || "") ? "high" : undefined;
134
+ }
135
+ function normalizeError(error) {
136
+ if (error instanceof Error) {
137
+ return error.message;
138
+ }
139
+ if (typeof error === "string") {
140
+ return error;
141
+ }
142
+ return "Codex execution failed";
143
+ }
144
+ function inferCommandToolName(command) {
145
+ const normalized = command.toLowerCase();
146
+ if (/\brg\b|\bgrep\b/.test(normalized)) {
147
+ return "Grep";
148
+ }
149
+ if (/\bglob\.glob\b|\bfind\b.+\s-name\s/.test(normalized)) {
150
+ return "Glob";
151
+ }
152
+ if (/\bcat\b/.test(normalized) && !/>/.test(normalized)) {
153
+ return "Read";
154
+ }
155
+ if (/<<\s*['"]?eof['"]?\s*>/i.test(command) ||
156
+ /\becho\b.+>/.test(normalized)) {
157
+ return "Write";
158
+ }
159
+ return "Bash";
160
+ }
161
+ function normalizeFilePath(path, workingDirectory) {
162
+ if (!path) {
163
+ return path;
164
+ }
165
+ if (workingDirectory && path.startsWith(workingDirectory)) {
166
+ const relativePath = pathRelative(workingDirectory, path);
167
+ if (relativePath && relativePath !== ".") {
168
+ return relativePath;
169
+ }
170
+ }
171
+ return path;
172
+ }
173
+ function summarizeFileChanges(item, workingDirectory) {
174
+ if (!item.changes.length) {
175
+ return item.status === "failed" ? "Patch failed" : "No file changes";
176
+ }
177
+ return item.changes
178
+ .map((change) => {
179
+ const filePath = normalizeFilePath(change.path, workingDirectory);
180
+ return `${change.kind} ${filePath}`;
181
+ })
182
+ .join("\n");
183
+ }
184
+ function asRecord(value) {
185
+ if (value && typeof value === "object") {
186
+ return value;
187
+ }
188
+ return null;
189
+ }
190
+ function toMcpResultString(item) {
191
+ if (item.error?.message) {
192
+ return item.error.message;
193
+ }
194
+ const textBlocks = [];
195
+ for (const block of item.result?.content || []) {
196
+ const text = asRecord(block)?.text;
197
+ if (typeof text === "string" && text.trim().length > 0) {
198
+ textBlocks.push(text);
199
+ }
200
+ }
201
+ if (textBlocks.length > 0) {
202
+ return textBlocks.join("\n");
203
+ }
204
+ if (item.result?.structured_content !== undefined) {
205
+ return safeStringify(item.result.structured_content);
206
+ }
207
+ return item.status === "failed"
208
+ ? "MCP tool call failed"
209
+ : "MCP tool call completed";
210
+ }
211
+ function normalizeMcpIdentifier(value) {
212
+ const normalized = value
213
+ .toLowerCase()
214
+ .replace(/[^a-z0-9_]+/g, "_")
215
+ .replace(/^_+|_+$/g, "");
216
+ return normalized || "unknown";
217
+ }
218
+ function autoDetectMcpConfigPath(workingDirectory) {
219
+ if (!workingDirectory) {
220
+ return undefined;
221
+ }
222
+ const mcpPath = join(workingDirectory, ".mcp.json");
223
+ if (!existsSync(mcpPath)) {
224
+ return undefined;
225
+ }
226
+ try {
227
+ JSON.parse(readFileSync(mcpPath, "utf8"));
228
+ return mcpPath;
229
+ }
230
+ catch {
231
+ console.warn(`[CodexRunner] Found .mcp.json at ${mcpPath} but it is invalid JSON, skipping`);
232
+ return undefined;
233
+ }
234
+ }
235
+ function loadMcpConfigFromPaths(configPaths) {
236
+ if (!configPaths) {
237
+ return {};
238
+ }
239
+ const paths = Array.isArray(configPaths) ? configPaths : [configPaths];
240
+ let mcpServers = {};
241
+ for (const configPath of paths) {
242
+ try {
243
+ const mcpConfigContent = readFileSync(configPath, "utf8");
244
+ const mcpConfig = JSON.parse(mcpConfigContent);
245
+ const servers = mcpConfig &&
246
+ typeof mcpConfig === "object" &&
247
+ !Array.isArray(mcpConfig) &&
248
+ mcpConfig.mcpServers &&
249
+ typeof mcpConfig.mcpServers === "object" &&
250
+ !Array.isArray(mcpConfig.mcpServers)
251
+ ? mcpConfig.mcpServers
252
+ : {};
253
+ mcpServers = { ...mcpServers, ...servers };
254
+ console.log(`[CodexRunner] Loaded MCP config from ${configPath}: ${Object.keys(servers).join(", ")}`);
255
+ }
256
+ catch (error) {
257
+ console.warn(`[CodexRunner] Failed to load MCP config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
258
+ }
259
+ }
260
+ return mcpServers;
261
+ }
262
+ /**
263
+ * Runner that adapts Codex SDK streaming output to Cyrus SDK message types.
264
+ */
265
+ export class CodexRunner extends EventEmitter {
266
+ supportsStreamingInput = false;
267
+ config;
268
+ sessionInfo = null;
269
+ messages = [];
270
+ formatter;
271
+ hasInitMessage = false;
272
+ pendingResultMessage = null;
273
+ lastAssistantText = null;
274
+ lastUsage = {
275
+ inputTokens: 0,
276
+ outputTokens: 0,
277
+ cachedInputTokens: 0,
278
+ };
279
+ errorMessages = [];
280
+ startTimestampMs = 0;
281
+ wasStopped = false;
282
+ abortController = null;
283
+ emittedToolUseIds = new Set();
284
+ constructor(config) {
285
+ super();
286
+ this.config = config;
287
+ this.formatter = new CodexMessageFormatter();
288
+ if (config.onMessage)
289
+ this.on("message", config.onMessage);
290
+ if (config.onError)
291
+ this.on("error", config.onError);
292
+ if (config.onComplete)
293
+ this.on("complete", config.onComplete);
294
+ }
295
+ async start(prompt) {
296
+ return this.startWithPrompt(prompt);
297
+ }
298
+ async startStreaming(initialPrompt) {
299
+ return this.startWithPrompt(null, initialPrompt);
300
+ }
301
+ addStreamMessage(_content) {
302
+ throw new Error("CodexRunner does not support streaming input messages");
303
+ }
304
+ completeStream() {
305
+ // No-op: CodexRunner does not support streaming input.
306
+ }
307
+ async startWithPrompt(stringPrompt, streamingInitialPrompt) {
308
+ if (this.isRunning()) {
309
+ throw new Error("Codex session already running");
310
+ }
311
+ const sessionId = this.config.resumeSessionId || crypto.randomUUID();
312
+ this.sessionInfo = {
313
+ sessionId,
314
+ startedAt: new Date(),
315
+ isRunning: true,
316
+ };
317
+ this.messages = [];
318
+ this.hasInitMessage = false;
319
+ this.pendingResultMessage = null;
320
+ this.lastAssistantText = null;
321
+ this.lastUsage = {
322
+ inputTokens: 0,
323
+ outputTokens: 0,
324
+ cachedInputTokens: 0,
325
+ };
326
+ this.errorMessages = [];
327
+ this.wasStopped = false;
328
+ this.startTimestampMs = Date.now();
329
+ this.emittedToolUseIds.clear();
330
+ const prompt = (stringPrompt ?? streamingInitialPrompt ?? "").trim();
331
+ const threadOptions = this.buildThreadOptions();
332
+ const codex = this.createCodexClient();
333
+ const thread = this.config.resumeSessionId
334
+ ? codex.resumeThread(this.config.resumeSessionId, threadOptions)
335
+ : codex.startThread(threadOptions);
336
+ const abortController = new AbortController();
337
+ this.abortController = abortController;
338
+ let caughtError;
339
+ try {
340
+ await this.runTurn(thread, prompt, abortController.signal);
341
+ }
342
+ catch (error) {
343
+ caughtError = error;
344
+ }
345
+ finally {
346
+ this.finalizeSession(caughtError);
347
+ }
348
+ return this.sessionInfo;
349
+ }
350
+ createCodexClient() {
351
+ const codexHome = this.resolveCodexHome();
352
+ const envOverride = this.buildEnvOverride(codexHome);
353
+ const configOverrides = this.buildConfigOverrides();
354
+ return new Codex({
355
+ ...(this.config.codexPath
356
+ ? { codexPathOverride: this.config.codexPath }
357
+ : {}),
358
+ ...(envOverride ? { env: envOverride } : {}),
359
+ ...(configOverrides ? { config: configOverrides } : {}),
360
+ });
361
+ }
362
+ buildThreadOptions() {
363
+ const additionalDirectories = this.getAdditionalDirectories();
364
+ const reasoningEffort = this.config.modelReasoningEffort ??
365
+ getDefaultReasoningEffortForModel(this.config.model);
366
+ const webSearchMode = this.config.webSearchMode ??
367
+ (this.config.includeWebSearch ? "live" : undefined);
368
+ const threadOptions = {
369
+ model: this.config.model,
370
+ sandboxMode: this.config.sandbox || "workspace-write",
371
+ workingDirectory: this.config.workingDirectory,
372
+ skipGitRepoCheck: this.config.skipGitRepoCheck ?? true,
373
+ approvalPolicy: this.config.askForApproval || "never",
374
+ ...(reasoningEffort ? { modelReasoningEffort: reasoningEffort } : {}),
375
+ ...(webSearchMode ? { webSearchMode } : {}),
376
+ ...(additionalDirectories.length > 0 ? { additionalDirectories } : {}),
377
+ };
378
+ return threadOptions;
379
+ }
380
+ getAdditionalDirectories() {
381
+ const workingDirectory = this.config.workingDirectory;
382
+ const uniqueDirectories = new Set();
383
+ for (const directory of this.config.allowedDirectories || []) {
384
+ if (!directory || directory === workingDirectory) {
385
+ continue;
386
+ }
387
+ uniqueDirectories.add(directory);
388
+ }
389
+ return [...uniqueDirectories];
390
+ }
391
+ resolveCodexHome() {
392
+ const codexHome = this.config.codexHome ||
393
+ process.env.CODEX_HOME ||
394
+ join(homedir(), ".codex");
395
+ mkdirSync(codexHome, { recursive: true });
396
+ return codexHome;
397
+ }
398
+ buildEnvOverride(codexHome) {
399
+ if (!this.config.codexHome) {
400
+ return undefined;
401
+ }
402
+ const env = {};
403
+ for (const [key, value] of Object.entries(process.env)) {
404
+ if (typeof value === "string") {
405
+ env[key] = value;
406
+ }
407
+ }
408
+ env.CODEX_HOME = codexHome;
409
+ return env;
410
+ }
411
+ buildCodexMcpServersConfig() {
412
+ const autoDetectedPath = autoDetectMcpConfigPath(this.config.workingDirectory);
413
+ const configPaths = autoDetectedPath
414
+ ? [autoDetectedPath]
415
+ : [];
416
+ if (this.config.mcpConfigPath) {
417
+ const explicitPaths = Array.isArray(this.config.mcpConfigPath)
418
+ ? this.config.mcpConfigPath
419
+ : [this.config.mcpConfigPath];
420
+ configPaths.push(...explicitPaths);
421
+ }
422
+ const fileBasedServers = loadMcpConfigFromPaths(configPaths);
423
+ const mergedServers = this.config.mcpConfig
424
+ ? { ...fileBasedServers, ...this.config.mcpConfig }
425
+ : fileBasedServers;
426
+ if (Object.keys(mergedServers).length === 0) {
427
+ return undefined;
428
+ }
429
+ // Codex MCP configuration reference:
430
+ // https://platform.openai.com/docs/docs-mcp
431
+ const codexServers = {};
432
+ for (const [serverName, rawConfig] of Object.entries(mergedServers)) {
433
+ const configAny = rawConfig;
434
+ if (typeof configAny.listTools === "function" ||
435
+ typeof configAny.callTool === "function") {
436
+ console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because in-process SDK server instances cannot be mapped to codex config`);
437
+ continue;
438
+ }
439
+ const mapped = {};
440
+ if (typeof configAny.command === "string") {
441
+ mapped.command = configAny.command;
442
+ }
443
+ if (Array.isArray(configAny.args)) {
444
+ mapped.args =
445
+ configAny.args;
446
+ }
447
+ if (configAny.env &&
448
+ typeof configAny.env === "object" &&
449
+ !Array.isArray(configAny.env)) {
450
+ mapped.env =
451
+ configAny.env;
452
+ }
453
+ if (typeof configAny.cwd === "string") {
454
+ mapped.cwd = configAny.cwd;
455
+ }
456
+ if (typeof configAny.url === "string") {
457
+ mapped.url = configAny.url;
458
+ }
459
+ if (configAny.http_headers &&
460
+ typeof configAny.http_headers === "object" &&
461
+ !Array.isArray(configAny.http_headers)) {
462
+ mapped.http_headers =
463
+ configAny.http_headers;
464
+ }
465
+ if (configAny.headers &&
466
+ typeof configAny.headers === "object" &&
467
+ !Array.isArray(configAny.headers)) {
468
+ mapped.http_headers =
469
+ configAny.headers;
470
+ }
471
+ if (configAny.env_http_headers &&
472
+ typeof configAny.env_http_headers === "object" &&
473
+ !Array.isArray(configAny.env_http_headers)) {
474
+ mapped.env_http_headers =
475
+ configAny.env_http_headers;
476
+ }
477
+ if (typeof configAny.bearer_token_env_var === "string") {
478
+ mapped.bearer_token_env_var = configAny.bearer_token_env_var;
479
+ }
480
+ if (typeof configAny.timeout === "number") {
481
+ mapped.timeout = configAny.timeout;
482
+ }
483
+ if (!mapped.command && !mapped.url) {
484
+ console.warn(`[CodexRunner] Skipping MCP server '${serverName}' because it has no command/url transport`);
485
+ continue;
486
+ }
487
+ codexServers[serverName] = mapped;
488
+ }
489
+ if (Object.keys(codexServers).length === 0) {
490
+ return undefined;
491
+ }
492
+ console.log(`[CodexRunner] Configured ${Object.keys(codexServers).length} MCP server(s) for codex config (docs: ${CODEX_MCP_DOCS_URL})`);
493
+ return codexServers;
494
+ }
495
+ buildConfigOverrides() {
496
+ const appendSystemPrompt = (this.config.appendSystemPrompt ?? "").trim();
497
+ const configOverrides = this.config.configOverrides
498
+ ? { ...this.config.configOverrides }
499
+ : {};
500
+ const mcpServers = this.buildCodexMcpServersConfig();
501
+ if (mcpServers) {
502
+ const existingMcpServers = configOverrides.mcp_servers;
503
+ if (existingMcpServers &&
504
+ typeof existingMcpServers === "object" &&
505
+ !Array.isArray(existingMcpServers)) {
506
+ configOverrides.mcp_servers = {
507
+ ...existingMcpServers,
508
+ ...mcpServers,
509
+ };
510
+ }
511
+ else {
512
+ configOverrides.mcp_servers = mcpServers;
513
+ }
514
+ }
515
+ const sandboxWorkspaceWrite = configOverrides.sandbox_workspace_write;
516
+ // Keep workspace-write as the default sandbox, but enable outbound network so
517
+ // common remote workflows (for example `git`/`gh` against GitHub) work without
518
+ // requiring danger-full-access.
519
+ if (sandboxWorkspaceWrite &&
520
+ typeof sandboxWorkspaceWrite === "object" &&
521
+ !Array.isArray(sandboxWorkspaceWrite)) {
522
+ configOverrides.sandbox_workspace_write = {
523
+ ...sandboxWorkspaceWrite,
524
+ network_access: sandboxWorkspaceWrite
525
+ .network_access ?? true,
526
+ };
527
+ }
528
+ else if (!sandboxWorkspaceWrite) {
529
+ configOverrides.sandbox_workspace_write = { network_access: true };
530
+ }
531
+ if (!appendSystemPrompt) {
532
+ return Object.keys(configOverrides).length > 0
533
+ ? configOverrides
534
+ : undefined;
535
+ }
536
+ return {
537
+ ...configOverrides,
538
+ developer_instructions: appendSystemPrompt,
539
+ };
540
+ }
541
+ async runTurn(thread, prompt, signal) {
542
+ const streamedTurn = await thread.runStreamed(prompt, { signal });
543
+ for await (const event of streamedTurn.events) {
544
+ this.handleEvent(event);
545
+ }
546
+ }
547
+ handleEvent(event) {
548
+ this.emit("streamEvent", event);
549
+ switch (event.type) {
550
+ case "thread.started": {
551
+ if (this.sessionInfo) {
552
+ this.sessionInfo.sessionId = event.thread_id;
553
+ }
554
+ this.emitSystemInitMessage(event.thread_id);
555
+ break;
556
+ }
557
+ case "item.completed": {
558
+ if (event.item.type === "agent_message") {
559
+ this.emitAssistantMessage(event.item.text);
560
+ }
561
+ else {
562
+ this.emitToolMessagesForItem(event.item, true);
563
+ }
564
+ break;
565
+ }
566
+ case "item.started": {
567
+ this.emitToolMessagesForItem(event.item, false);
568
+ break;
569
+ }
570
+ case "turn.completed": {
571
+ this.lastUsage = parseUsage(event.usage);
572
+ this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
573
+ break;
574
+ }
575
+ case "turn.failed": {
576
+ // Prefer event.error.message; fallback to last standalone "error" event
577
+ const message = event.error?.message ||
578
+ this.errorMessages.at(-1) ||
579
+ "Codex execution failed";
580
+ this.errorMessages.push(message);
581
+ this.pendingResultMessage = this.createErrorResultMessage(message);
582
+ break;
583
+ }
584
+ case "error": {
585
+ this.errorMessages.push(event.message);
586
+ break;
587
+ }
588
+ default:
589
+ break;
590
+ }
591
+ }
592
+ projectItemToTool(item) {
593
+ switch (item.type) {
594
+ case "command_execution": {
595
+ const commandItem = item;
596
+ const isError = commandItem.status === "failed" ||
597
+ (typeof commandItem.exit_code === "number" &&
598
+ commandItem.exit_code !== 0);
599
+ const result = commandItem.aggregated_output?.trim() ||
600
+ (isError
601
+ ? `Command failed (exit code ${commandItem.exit_code ?? "unknown"})`
602
+ : "Command completed with no output");
603
+ return {
604
+ toolUseId: commandItem.id,
605
+ toolName: inferCommandToolName(commandItem.command),
606
+ toolInput: { command: commandItem.command },
607
+ result,
608
+ isError,
609
+ };
610
+ }
611
+ case "file_change": {
612
+ const fileChangeItem = item;
613
+ const primaryPath = fileChangeItem.changes[0]?.path &&
614
+ normalizeFilePath(fileChangeItem.changes[0].path, this.config.workingDirectory);
615
+ return {
616
+ toolUseId: fileChangeItem.id,
617
+ toolName: "Edit",
618
+ toolInput: {
619
+ ...(primaryPath ? { file_path: primaryPath } : {}),
620
+ changes: fileChangeItem.changes.map((change) => ({
621
+ kind: change.kind,
622
+ path: normalizeFilePath(change.path, this.config.workingDirectory),
623
+ })),
624
+ },
625
+ result: summarizeFileChanges(fileChangeItem, this.config.workingDirectory),
626
+ isError: fileChangeItem.status === "failed",
627
+ };
628
+ }
629
+ case "web_search": {
630
+ const webSearchItem = item;
631
+ const extendedItem = item;
632
+ const action = asRecord(extendedItem.action);
633
+ const actionType = typeof action?.type === "string" ? action.type : undefined;
634
+ const isFetch = actionType === "open_page";
635
+ const url = typeof action?.url === "string"
636
+ ? action.url
637
+ : typeof extendedItem.url === "string"
638
+ ? extendedItem.url
639
+ : undefined;
640
+ const pattern = typeof action?.pattern === "string"
641
+ ? action.pattern
642
+ : typeof extendedItem.pattern === "string"
643
+ ? extendedItem.pattern
644
+ : undefined;
645
+ return {
646
+ toolUseId: webSearchItem.id,
647
+ toolName: isFetch ? "WebFetch" : "WebSearch",
648
+ toolInput: isFetch
649
+ ? {
650
+ url: url || webSearchItem.query,
651
+ ...(pattern ? { pattern } : {}),
652
+ }
653
+ : { query: webSearchItem.query },
654
+ result: action && Object.keys(action).length > 0
655
+ ? safeStringify(action)
656
+ : `Search completed for query: ${webSearchItem.query}`,
657
+ isError: false,
658
+ };
659
+ }
660
+ case "mcp_tool_call": {
661
+ const mcpItem = item;
662
+ return {
663
+ toolUseId: mcpItem.id,
664
+ toolName: `mcp__${normalizeMcpIdentifier(mcpItem.server)}__${normalizeMcpIdentifier(mcpItem.tool)}`,
665
+ toolInput: asRecord(mcpItem.arguments) || {
666
+ arguments: mcpItem.arguments,
667
+ },
668
+ result: toMcpResultString(mcpItem),
669
+ isError: mcpItem.status === "failed" || Boolean(mcpItem.error),
670
+ };
671
+ }
672
+ case "todo_list": {
673
+ const todoItem = item;
674
+ return {
675
+ toolUseId: todoItem.id,
676
+ toolName: "TodoWrite",
677
+ toolInput: {
678
+ todos: todoItem.items.map((todo) => ({
679
+ content: todo.text,
680
+ status: todo.completed ? "completed" : "pending",
681
+ })),
682
+ },
683
+ result: `Updated todo list (${todoItem.items.length} items)`,
684
+ isError: false,
685
+ };
686
+ }
687
+ default:
688
+ return null;
689
+ }
690
+ }
691
+ emitToolMessagesForItem(item, includeResult) {
692
+ const projection = this.projectItemToTool(item);
693
+ if (!projection) {
694
+ return;
695
+ }
696
+ if (!this.emittedToolUseIds.has(projection.toolUseId)) {
697
+ const assistantMessage = {
698
+ type: "assistant",
699
+ message: createAssistantToolUseMessage(projection.toolUseId, projection.toolName, projection.toolInput),
700
+ parent_tool_use_id: null,
701
+ uuid: crypto.randomUUID(),
702
+ session_id: this.sessionInfo?.sessionId || "pending",
703
+ };
704
+ this.messages.push(assistantMessage);
705
+ this.emit("message", assistantMessage);
706
+ this.emittedToolUseIds.add(projection.toolUseId);
707
+ }
708
+ if (!includeResult) {
709
+ return;
710
+ }
711
+ const userMessage = {
712
+ type: "user",
713
+ message: createUserToolResultMessage(projection.toolUseId, projection.result, projection.isError),
714
+ parent_tool_use_id: null,
715
+ uuid: crypto.randomUUID(),
716
+ session_id: this.sessionInfo?.sessionId || "pending",
717
+ };
718
+ this.messages.push(userMessage);
719
+ this.emit("message", userMessage);
720
+ this.emittedToolUseIds.delete(projection.toolUseId);
721
+ }
722
+ finalizeSession(caughtError) {
723
+ if (!this.sessionInfo) {
724
+ this.cleanupRuntimeState();
725
+ return;
726
+ }
727
+ this.sessionInfo.isRunning = false;
728
+ // Ensure init is emitted even if stream fails before thread.started.
729
+ if (!this.hasInitMessage) {
730
+ this.emitSystemInitMessage(this.sessionInfo.sessionId || this.config.resumeSessionId || "pending");
731
+ }
732
+ if (caughtError && !this.wasStopped) {
733
+ const errorMessage = normalizeError(caughtError);
734
+ this.errorMessages.push(errorMessage);
735
+ }
736
+ if (!this.pendingResultMessage && !this.wasStopped) {
737
+ if (caughtError) {
738
+ this.pendingResultMessage = this.createErrorResultMessage(this.errorMessages.at(-1) || "Codex execution failed");
739
+ }
740
+ else {
741
+ this.pendingResultMessage = this.createSuccessResultMessage(this.lastAssistantText || "Codex session completed successfully");
742
+ }
743
+ }
744
+ if (this.pendingResultMessage) {
745
+ this.messages.push(this.pendingResultMessage);
746
+ this.emit("message", this.pendingResultMessage);
747
+ this.pendingResultMessage = null;
748
+ }
749
+ this.emit("complete", [...this.messages]);
750
+ this.cleanupRuntimeState();
751
+ }
752
+ emitAssistantMessage(text) {
753
+ const normalized = text.trim();
754
+ if (!normalized) {
755
+ return;
756
+ }
757
+ this.lastAssistantText = normalized;
758
+ const assistantMessage = {
759
+ type: "assistant",
760
+ message: createAssistantBetaMessage(normalized),
761
+ parent_tool_use_id: null,
762
+ uuid: crypto.randomUUID(),
763
+ session_id: this.sessionInfo?.sessionId || "pending",
764
+ };
765
+ this.messages.push(assistantMessage);
766
+ this.emit("message", assistantMessage);
767
+ }
768
+ emitSystemInitMessage(sessionId) {
769
+ if (this.hasInitMessage) {
770
+ return;
771
+ }
772
+ this.hasInitMessage = true;
773
+ const initMessage = {
774
+ type: "system",
775
+ subtype: "init",
776
+ agents: undefined,
777
+ apiKeySource: "user",
778
+ claude_code_version: "codex-cli",
779
+ cwd: this.config.workingDirectory || cwd(),
780
+ tools: [],
781
+ mcp_servers: [],
782
+ model: this.config.model || DEFAULT_CODEX_MODEL,
783
+ permissionMode: "default",
784
+ slash_commands: [],
785
+ output_style: "default",
786
+ skills: [],
787
+ plugins: [],
788
+ uuid: crypto.randomUUID(),
789
+ session_id: sessionId,
790
+ };
791
+ this.messages.push(initMessage);
792
+ this.emit("message", initMessage);
793
+ }
794
+ createSuccessResultMessage(result) {
795
+ const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
796
+ return {
797
+ type: "result",
798
+ subtype: "success",
799
+ duration_ms: durationMs,
800
+ duration_api_ms: 0,
801
+ is_error: false,
802
+ num_turns: 1,
803
+ result,
804
+ stop_reason: null,
805
+ total_cost_usd: 0,
806
+ usage: createResultUsage(this.lastUsage),
807
+ modelUsage: {},
808
+ permission_denials: [],
809
+ uuid: crypto.randomUUID(),
810
+ session_id: this.sessionInfo?.sessionId || "pending",
811
+ };
812
+ }
813
+ createErrorResultMessage(errorMessage) {
814
+ const durationMs = Math.max(Date.now() - this.startTimestampMs, 0);
815
+ return {
816
+ type: "result",
817
+ subtype: "error_during_execution",
818
+ duration_ms: durationMs,
819
+ duration_api_ms: 0,
820
+ is_error: true,
821
+ num_turns: 1,
822
+ stop_reason: null,
823
+ errors: [errorMessage],
824
+ total_cost_usd: 0,
825
+ usage: createResultUsage(this.lastUsage),
826
+ modelUsage: {},
827
+ permission_denials: [],
828
+ uuid: crypto.randomUUID(),
829
+ session_id: this.sessionInfo?.sessionId || "pending",
830
+ };
831
+ }
832
+ cleanupRuntimeState() {
833
+ this.abortController = null;
834
+ }
835
+ stop() {
836
+ if (!this.sessionInfo?.isRunning) {
837
+ return;
838
+ }
839
+ this.wasStopped = true;
840
+ this.abortController?.abort();
841
+ }
842
+ isRunning() {
843
+ return this.sessionInfo?.isRunning ?? false;
844
+ }
845
+ getMessages() {
846
+ return [...this.messages];
847
+ }
848
+ getFormatter() {
849
+ return this.formatter;
850
+ }
851
+ }
852
+ //# sourceMappingURL=CodexRunner.js.map