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.
- package/LICENSE +201 -0
- package/dist/CodexRunner.d.ts +54 -0
- package/dist/CodexRunner.d.ts.map +1 -0
- package/dist/CodexRunner.js +852 -0
- package/dist/CodexRunner.js.map +1 -0
- package/dist/SimpleCodexRunner.d.ts +27 -0
- package/dist/SimpleCodexRunner.d.ts.map +1 -0
- package/dist/SimpleCodexRunner.js +149 -0
- package/dist/SimpleCodexRunner.js.map +1 -0
- package/dist/formatter.d.ts +13 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +149 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +31 -0
|
@@ -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
|