@agentconnect/host 0.2.0

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,908 @@
1
+ import { spawn } from 'child_process';
2
+ import { readFile } from 'fs/promises';
3
+ import https from 'https';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { buildInstallCommandAuto, buildLoginCommand, buildStatusCommand, checkCommandVersion, commandExists, createLineParser, debugLog, resolveWindowsCommand, resolveCommandPath, resolveCommandRealPath, runCommand, } from './utils.js';
7
+ const CODEX_PACKAGE = '@openai/codex';
8
+ const DEFAULT_LOGIN = 'codex login';
9
+ const DEFAULT_STATUS = 'codex login status';
10
+ const CODEX_MODELS_CACHE_TTL_MS = 60_000;
11
+ const CODEX_UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
12
+ let codexModelsCache = null;
13
+ let codexModelsCacheAt = 0;
14
+ let codexUpdateCache = null;
15
+ let codexUpdatePromise = null;
16
+ function trimOutput(value, limit = 400) {
17
+ const cleaned = value.trim();
18
+ if (!cleaned)
19
+ return '';
20
+ if (cleaned.length <= limit)
21
+ return cleaned;
22
+ return `${cleaned.slice(0, limit)}...`;
23
+ }
24
+ function normalizePath(value) {
25
+ const normalized = value.replace(/\\/g, '/');
26
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
27
+ }
28
+ function getCodexConfigDir() {
29
+ return process.env.CODEX_CONFIG_DIR || path.join(os.homedir(), '.codex');
30
+ }
31
+ function fetchJson(url) {
32
+ return new Promise((resolve) => {
33
+ https
34
+ .get(url, (res) => {
35
+ let data = '';
36
+ res.on('data', (chunk) => {
37
+ data += chunk;
38
+ });
39
+ res.on('end', () => {
40
+ try {
41
+ resolve(JSON.parse(data));
42
+ }
43
+ catch {
44
+ resolve(null);
45
+ }
46
+ });
47
+ })
48
+ .on('error', () => resolve(null));
49
+ });
50
+ }
51
+ function parseSemver(value) {
52
+ if (!value)
53
+ return null;
54
+ const match = value.match(/(\d+)\.(\d+)\.(\d+)/);
55
+ if (!match)
56
+ return null;
57
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
58
+ }
59
+ function compareSemver(a, b) {
60
+ if (a[0] !== b[0])
61
+ return a[0] - b[0];
62
+ if (a[1] !== b[1])
63
+ return a[1] - b[1];
64
+ return a[2] - b[2];
65
+ }
66
+ async function fetchLatestNpmVersion(pkg) {
67
+ const encoded = encodeURIComponent(pkg);
68
+ const data = (await fetchJson(`https://registry.npmjs.org/${encoded}`));
69
+ if (!data || typeof data !== 'object')
70
+ return null;
71
+ const latest = data['dist-tags']?.latest;
72
+ return typeof latest === 'string' ? latest : null;
73
+ }
74
+ async function fetchBrewFormulaVersion(formula) {
75
+ if (!commandExists('brew'))
76
+ return null;
77
+ const result = await runCommand('brew', ['info', '--json=v2', formula]);
78
+ if (result.code !== 0)
79
+ return null;
80
+ try {
81
+ const parsed = JSON.parse(result.stdout);
82
+ const version = parsed?.formulae?.[0]?.versions?.stable;
83
+ return typeof version === 'string' ? version : null;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function getCodexUpdateAction(commandPath) {
90
+ if (process.env.CODEX_MANAGED_BY_NPM) {
91
+ return {
92
+ command: 'npm',
93
+ args: ['install', '-g', CODEX_PACKAGE],
94
+ source: 'npm',
95
+ commandLabel: 'npm install -g @openai/codex',
96
+ };
97
+ }
98
+ if (process.env.CODEX_MANAGED_BY_BUN) {
99
+ return {
100
+ command: 'bun',
101
+ args: ['install', '-g', CODEX_PACKAGE],
102
+ source: 'bun',
103
+ commandLabel: 'bun install -g @openai/codex',
104
+ };
105
+ }
106
+ if (commandPath) {
107
+ const normalized = normalizePath(commandPath);
108
+ if (normalized.includes('.bun/install/global')) {
109
+ return {
110
+ command: 'bun',
111
+ args: ['install', '-g', CODEX_PACKAGE],
112
+ source: 'bun',
113
+ commandLabel: 'bun install -g @openai/codex',
114
+ };
115
+ }
116
+ if (normalized.includes('/node_modules/.bin/') || normalized.includes('/lib/node_modules/')) {
117
+ return {
118
+ command: 'npm',
119
+ args: ['install', '-g', CODEX_PACKAGE],
120
+ source: 'npm',
121
+ commandLabel: 'npm install -g @openai/codex',
122
+ };
123
+ }
124
+ }
125
+ if (process.platform === 'darwin' &&
126
+ commandPath &&
127
+ (commandPath.startsWith('/opt/homebrew') || commandPath.startsWith('/usr/local'))) {
128
+ return {
129
+ command: 'brew',
130
+ args: ['upgrade', 'codex'],
131
+ source: 'brew',
132
+ commandLabel: 'brew upgrade codex',
133
+ };
134
+ }
135
+ return null;
136
+ }
137
+ function getCodexUpdateSnapshot(commandPath) {
138
+ if (codexUpdateCache && Date.now() - codexUpdateCache.checkedAt < CODEX_UPDATE_CACHE_TTL_MS) {
139
+ const action = getCodexUpdateAction(commandPath);
140
+ return {
141
+ updateAvailable: codexUpdateCache.updateAvailable,
142
+ latestVersion: codexUpdateCache.latestVersion,
143
+ updateCheckedAt: codexUpdateCache.checkedAt,
144
+ updateSource: action?.source ?? 'unknown',
145
+ updateCommand: action?.commandLabel,
146
+ updateMessage: codexUpdateCache.updateMessage,
147
+ };
148
+ }
149
+ return {};
150
+ }
151
+ function ensureCodexUpdateCheck(currentVersion, commandPath) {
152
+ if (codexUpdateCache && Date.now() - codexUpdateCache.checkedAt < CODEX_UPDATE_CACHE_TTL_MS) {
153
+ return;
154
+ }
155
+ if (codexUpdatePromise)
156
+ return;
157
+ codexUpdatePromise = (async () => {
158
+ const action = getCodexUpdateAction(commandPath || null);
159
+ let latest = null;
160
+ if (action?.source === 'brew') {
161
+ latest = await fetchBrewFormulaVersion('codex');
162
+ }
163
+ else {
164
+ latest = await fetchLatestNpmVersion(CODEX_PACKAGE);
165
+ }
166
+ let updateAvailable;
167
+ let updateMessage;
168
+ if (latest && currentVersion) {
169
+ const a = parseSemver(currentVersion);
170
+ const b = parseSemver(latest);
171
+ if (a && b) {
172
+ updateAvailable = compareSemver(a, b) < 0;
173
+ updateMessage = updateAvailable
174
+ ? `Update available: ${currentVersion} -> ${latest}`
175
+ : `Up to date (${currentVersion})`;
176
+ }
177
+ }
178
+ codexUpdateCache = {
179
+ checkedAt: Date.now(),
180
+ updateAvailable,
181
+ latestVersion: latest ?? undefined,
182
+ updateMessage,
183
+ };
184
+ debugLog('Codex', 'update-check', {
185
+ currentVersion,
186
+ latest,
187
+ updateAvailable,
188
+ message: updateMessage,
189
+ });
190
+ })().finally(() => {
191
+ codexUpdatePromise = null;
192
+ });
193
+ }
194
+ function hasAuthValue(value) {
195
+ return typeof value === 'string' && value.trim().length > 0;
196
+ }
197
+ async function hasCodexAuth() {
198
+ const home = os.homedir();
199
+ const candidates = [
200
+ path.join(getCodexConfigDir(), 'auth.json'),
201
+ path.join(home, '.config', 'codex', 'auth.json'),
202
+ ];
203
+ for (const filePath of candidates) {
204
+ try {
205
+ const raw = await readFile(filePath, 'utf8');
206
+ const parsed = JSON.parse(raw);
207
+ if (hasAuthValue(parsed.OPENAI_API_KEY))
208
+ return true;
209
+ if (hasAuthValue(parsed.access_token))
210
+ return true;
211
+ if (hasAuthValue(parsed.token))
212
+ return true;
213
+ const tokens = parsed.tokens;
214
+ if (tokens) {
215
+ if (hasAuthValue(tokens.access_token))
216
+ return true;
217
+ if (hasAuthValue(tokens.refresh_token))
218
+ return true;
219
+ if (hasAuthValue(tokens.id_token))
220
+ return true;
221
+ }
222
+ }
223
+ catch {
224
+ // try next path
225
+ }
226
+ }
227
+ return false;
228
+ }
229
+ function buildCodexExecArgs(options) {
230
+ const { prompt, cdTarget, resumeSessionId, model, reasoningEffort, mode } = options;
231
+ const args = ['exec', '--skip-git-repo-check'];
232
+ if (mode === 'legacy') {
233
+ args.push('--json', '-C', cdTarget);
234
+ }
235
+ else {
236
+ args.push('--experimental-json', '--cd', cdTarget);
237
+ }
238
+ args.push('--yolo');
239
+ if (model) {
240
+ args.push('--model', String(model));
241
+ }
242
+ if (reasoningEffort) {
243
+ args.push('--config', `model_reasoning_effort=${reasoningEffort}`);
244
+ }
245
+ if (resumeSessionId) {
246
+ args.push('resume', resumeSessionId);
247
+ }
248
+ args.push(prompt);
249
+ return args;
250
+ }
251
+ function shouldFallbackToLegacy(lines) {
252
+ const combined = lines.join(' ').toLowerCase();
253
+ if (!(combined.includes('unknown flag') ||
254
+ combined.includes('unknown option') ||
255
+ combined.includes('unrecognized option') ||
256
+ combined.includes('unknown argument') ||
257
+ combined.includes('unexpected argument') ||
258
+ combined.includes('invalid option') ||
259
+ combined.includes('invalid argument'))) {
260
+ return false;
261
+ }
262
+ return combined.includes('experimental-json') || combined.includes('--cd');
263
+ }
264
+ export function getCodexCommand() {
265
+ const override = process.env.AGENTCONNECT_CODEX_COMMAND;
266
+ const base = override || 'codex';
267
+ const resolved = resolveCommandPath(base);
268
+ return resolved || resolveWindowsCommand(base);
269
+ }
270
+ export async function ensureCodexInstalled() {
271
+ const command = getCodexCommand();
272
+ const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
273
+ if (versionCheck.ok) {
274
+ return { installed: true, version: versionCheck.version || undefined };
275
+ }
276
+ if (commandExists(command)) {
277
+ return { installed: true, version: undefined };
278
+ }
279
+ const install = await buildInstallCommandAuto(CODEX_PACKAGE);
280
+ if (!install.command) {
281
+ return { installed: false, version: undefined, packageManager: install.packageManager };
282
+ }
283
+ debugLog('Codex', 'install', { command: install.command, args: install.args });
284
+ const installResult = await runCommand(install.command, install.args, {
285
+ shell: process.platform === 'win32',
286
+ });
287
+ debugLog('Codex', 'install-result', {
288
+ code: installResult.code,
289
+ stderr: trimOutput(installResult.stderr),
290
+ });
291
+ const after = await checkCommandVersion(command, [['--version'], ['-V']]);
292
+ // Invalidate models cache after installation so fresh models are fetched
293
+ codexModelsCache = null;
294
+ codexModelsCacheAt = 0;
295
+ return {
296
+ installed: after.ok,
297
+ version: after.version || undefined,
298
+ packageManager: install.packageManager,
299
+ };
300
+ }
301
+ export async function getCodexStatus() {
302
+ const command = getCodexCommand();
303
+ const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
304
+ const installed = versionCheck.ok || commandExists(command);
305
+ let loggedIn = false;
306
+ let explicitLoggedOut = false;
307
+ if (installed) {
308
+ const status = buildStatusCommand('AGENTCONNECT_CODEX_STATUS', DEFAULT_STATUS);
309
+ if (status.command) {
310
+ const statusCommand = resolveWindowsCommand(status.command);
311
+ const result = await runCommand(statusCommand, status.args, { env: { ...process.env, CI: '1' } });
312
+ const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
313
+ if (output.includes('not logged in') ||
314
+ output.includes('not logged') ||
315
+ output.includes('login required') ||
316
+ output.includes('please login') ||
317
+ output.includes('run codex login')) {
318
+ explicitLoggedOut = true;
319
+ loggedIn = false;
320
+ }
321
+ else if (output.includes('logged in') ||
322
+ output.includes('signed in') ||
323
+ output.includes('authenticated')) {
324
+ loggedIn = true;
325
+ }
326
+ else {
327
+ loggedIn = result.code === 0;
328
+ }
329
+ }
330
+ if (!loggedIn && !explicitLoggedOut) {
331
+ loggedIn = await hasCodexAuth();
332
+ }
333
+ }
334
+ const resolved = resolveCommandRealPath(command);
335
+ if (installed) {
336
+ ensureCodexUpdateCheck(versionCheck.version, resolved || null);
337
+ }
338
+ const updateInfo = installed ? getCodexUpdateSnapshot(resolved || null) : {};
339
+ return { installed, loggedIn, version: versionCheck.version || undefined, ...updateInfo };
340
+ }
341
+ export async function getCodexFastStatus() {
342
+ const command = getCodexCommand();
343
+ const loggedIn = await hasCodexAuth();
344
+ const installed = commandExists(command) || loggedIn;
345
+ return { installed, loggedIn };
346
+ }
347
+ export async function updateCodex() {
348
+ const command = getCodexCommand();
349
+ if (!commandExists(command)) {
350
+ return { installed: false, loggedIn: false };
351
+ }
352
+ const resolved = resolveCommandRealPath(command);
353
+ const updateOverride = buildStatusCommand('AGENTCONNECT_CODEX_UPDATE', '');
354
+ const action = updateOverride.command ? null : getCodexUpdateAction(resolved || null);
355
+ const updateCommand = updateOverride.command || action?.command || '';
356
+ const updateArgs = updateOverride.command ? updateOverride.args : action?.args || [];
357
+ if (!updateCommand) {
358
+ throw new Error('No update command available. Please update Codex manually.');
359
+ }
360
+ const cmd = resolveWindowsCommand(updateCommand);
361
+ debugLog('Codex', 'update-run', { command: cmd, args: updateArgs });
362
+ const result = await runCommand(cmd, updateArgs, {
363
+ env: { ...process.env, CI: '1' },
364
+ });
365
+ debugLog('Codex', 'update-result', {
366
+ code: result.code,
367
+ stdout: trimOutput(result.stdout),
368
+ stderr: trimOutput(result.stderr),
369
+ });
370
+ if (result.code !== 0 && result.code !== null) {
371
+ const message = trimOutput(`${result.stdout}\n${result.stderr}`, 800) || 'Update failed';
372
+ throw new Error(message);
373
+ }
374
+ codexUpdateCache = null;
375
+ codexUpdatePromise = null;
376
+ return getCodexStatus();
377
+ }
378
+ export async function loginCodex() {
379
+ const login = buildLoginCommand('AGENTCONNECT_CODEX_LOGIN', DEFAULT_LOGIN);
380
+ if (!login.command) {
381
+ return { loggedIn: false };
382
+ }
383
+ const command = resolveWindowsCommand(login.command);
384
+ debugLog('Codex', 'login', { command, args: login.args });
385
+ const result = await runCommand(command, login.args, { env: { ...process.env, CI: '1' } });
386
+ debugLog('Codex', 'login-result', { code: result.code, stderr: trimOutput(result.stderr) });
387
+ const status = await getCodexStatus();
388
+ codexModelsCache = null;
389
+ codexModelsCacheAt = 0;
390
+ return { loggedIn: status.loggedIn };
391
+ }
392
+ function safeJsonParse(line) {
393
+ try {
394
+ return JSON.parse(line);
395
+ }
396
+ catch {
397
+ return null;
398
+ }
399
+ }
400
+ function extractSessionId(ev) {
401
+ const t = String(ev.type ?? '');
402
+ if (t === 'thread.started') {
403
+ return typeof ev.thread_id === 'string' ? ev.thread_id : null;
404
+ }
405
+ const id = ev.thread_id ?? ev.threadId ?? ev.session_id ?? ev.sessionId;
406
+ return typeof id === 'string' ? id : null;
407
+ }
408
+ function extractUsage(ev) {
409
+ const usage = ev.usage ?? ev.token_usage ?? ev.tokens ?? ev.tokenUsage;
410
+ if (!usage || typeof usage !== 'object')
411
+ return null;
412
+ const toNumber = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
413
+ const input = toNumber(usage.input_tokens ?? usage.prompt_tokens ?? usage.inputTokens ?? usage.promptTokens);
414
+ const output = toNumber(usage.output_tokens ?? usage.completion_tokens ?? usage.outputTokens ?? usage.completionTokens);
415
+ const total = toNumber(usage.total_tokens ?? usage.totalTokens);
416
+ const cached = toNumber(usage.cached_input_tokens ?? usage.cachedInputTokens);
417
+ const out = {};
418
+ if (input !== undefined)
419
+ out.input_tokens = input;
420
+ if (output !== undefined)
421
+ out.output_tokens = output;
422
+ if (total !== undefined)
423
+ out.total_tokens = total;
424
+ if (cached !== undefined)
425
+ out.cached_input_tokens = cached;
426
+ return Object.keys(out).length ? out : null;
427
+ }
428
+ function normalizeItem(raw) {
429
+ if (!raw || typeof raw !== 'object')
430
+ return raw;
431
+ const item = raw;
432
+ const type = item.type;
433
+ const id = item.id;
434
+ if (type === 'command_execution' && id) {
435
+ return {
436
+ id,
437
+ type,
438
+ command: item.command || '',
439
+ aggregated_output: item.aggregated_output,
440
+ exit_code: item.exit_code,
441
+ status: item.status,
442
+ };
443
+ }
444
+ if (type === 'reasoning' && id) {
445
+ return { id, type, text: item.text || '' };
446
+ }
447
+ if (type === 'agent_message' && id) {
448
+ return { id, type, text: item.text || '' };
449
+ }
450
+ return raw;
451
+ }
452
+ function normalizeEvent(raw) {
453
+ const type = typeof raw.type === 'string' ? raw.type : 'unknown';
454
+ if (type === 'item.started' || type === 'item.completed') {
455
+ return { type, item: normalizeItem(raw.item) };
456
+ }
457
+ if (type === 'agent_message') {
458
+ return { type, text: raw.text || '' };
459
+ }
460
+ if (type === 'error') {
461
+ return { type, message: raw.message || 'Unknown error' };
462
+ }
463
+ return { ...raw, type };
464
+ }
465
+ function isTerminalEvent(ev) {
466
+ const t = String(ev.type ?? '');
467
+ return t === 'turn.completed' || t === 'turn.failed';
468
+ }
469
+ function normalizeEffortId(raw) {
470
+ if (!raw)
471
+ return null;
472
+ return String(raw).trim().toLowerCase();
473
+ }
474
+ function formatEffortLabel(id) {
475
+ if (!id)
476
+ return '';
477
+ const normalized = String(id).trim().toLowerCase();
478
+ if (normalized === 'xhigh')
479
+ return 'X-High';
480
+ if (normalized === 'none')
481
+ return 'None';
482
+ if (normalized === 'minimal')
483
+ return 'Minimal';
484
+ if (normalized === 'low')
485
+ return 'Low';
486
+ if (normalized === 'medium')
487
+ return 'Medium';
488
+ if (normalized === 'high')
489
+ return 'High';
490
+ return normalized.toUpperCase();
491
+ }
492
+ function normalizeEffortOptions(raw) {
493
+ if (!Array.isArray(raw))
494
+ return [];
495
+ const options = raw
496
+ .map((entry) => {
497
+ if (!entry || typeof entry !== 'object')
498
+ return null;
499
+ const opt = entry;
500
+ const id = normalizeEffortId(opt.reasoning_effort ?? opt.reasoningEffort ?? opt.effort ?? opt.level);
501
+ if (!id)
502
+ return null;
503
+ const label = formatEffortLabel(id);
504
+ return { id, label };
505
+ })
506
+ .filter((x) => x !== null);
507
+ const seen = new Set();
508
+ return options.filter((option) => {
509
+ if (seen.has(option.id))
510
+ return false;
511
+ seen.add(option.id);
512
+ return true;
513
+ });
514
+ }
515
+ function normalizeCodexModels(raw) {
516
+ const response = raw;
517
+ const list = Array.isArray(response?.data)
518
+ ? response.data
519
+ : Array.isArray(response?.items)
520
+ ? response.items
521
+ : [];
522
+ const mapped = [];
523
+ for (const item of list) {
524
+ if (!item || typeof item !== 'object')
525
+ continue;
526
+ const id = item.id || item.model;
527
+ if (!id)
528
+ continue;
529
+ const displayName = item.displayName || item.display_name || item.name || item.title || String(id);
530
+ const reasoningEfforts = normalizeEffortOptions(item.supportedReasoningEfforts ||
531
+ item.supported_reasoning_efforts ||
532
+ item.supportedReasoningLevels ||
533
+ item.supported_reasoning_levels);
534
+ const defaultReasoningEffort = normalizeEffortId(item.defaultReasoningEffort || item.default_reasoning_effort);
535
+ mapped.push({
536
+ id: String(id),
537
+ provider: 'codex',
538
+ displayName: String(displayName),
539
+ reasoningEfforts: reasoningEfforts.length ? reasoningEfforts : undefined,
540
+ defaultReasoningEffort: defaultReasoningEffort || undefined,
541
+ });
542
+ }
543
+ if (!mapped.length)
544
+ return [];
545
+ const seen = new Set();
546
+ return mapped.filter((model) => {
547
+ const key = model.id;
548
+ if (seen.has(key))
549
+ return false;
550
+ seen.add(key);
551
+ return true;
552
+ });
553
+ }
554
+ async function fetchCodexModels(command) {
555
+ return new Promise((resolve) => {
556
+ const child = spawn(command, ['app-server'], {
557
+ env: { ...process.env },
558
+ stdio: ['pipe', 'pipe', 'pipe'],
559
+ });
560
+ let settled = false;
561
+ const initId = 1;
562
+ const listId = 2;
563
+ const timeout = setTimeout(() => {
564
+ finalize(null);
565
+ }, 8000);
566
+ const finalize = (models) => {
567
+ if (settled)
568
+ return;
569
+ settled = true;
570
+ clearTimeout(timeout);
571
+ child.kill('SIGTERM');
572
+ resolve(models);
573
+ };
574
+ const handleLine = (line) => {
575
+ const parsed = safeJsonParse(line);
576
+ if (!parsed || typeof parsed !== 'object')
577
+ return;
578
+ if (parsed.id === listId && parsed.result) {
579
+ finalize(normalizeCodexModels(parsed.result));
580
+ }
581
+ if (parsed.id === listId && parsed.error) {
582
+ finalize(null);
583
+ }
584
+ };
585
+ const stdoutParser = createLineParser(handleLine);
586
+ child.stdout?.on('data', stdoutParser);
587
+ child.stderr?.on('data', () => { });
588
+ child.on('error', () => finalize(null));
589
+ child.on('close', () => finalize(null));
590
+ if (!child.stdin) {
591
+ finalize(null);
592
+ return;
593
+ }
594
+ const initialize = {
595
+ method: 'initialize',
596
+ id: initId,
597
+ params: {
598
+ clientInfo: { name: 'agentconnect', title: 'AgentConnect', version: '0.1.0' },
599
+ },
600
+ };
601
+ const initialized = { method: 'initialized' };
602
+ const listRequest = { method: 'model/list', id: listId, params: { cursor: null, limit: null } };
603
+ const payload = `${JSON.stringify(initialize)}\n${JSON.stringify(initialized)}\n${JSON.stringify(listRequest)}\n`;
604
+ child.stdin.write(payload);
605
+ });
606
+ }
607
+ export async function listCodexModels() {
608
+ if (codexModelsCache && Date.now() - codexModelsCacheAt < CODEX_MODELS_CACHE_TTL_MS) {
609
+ return codexModelsCache;
610
+ }
611
+ const command = getCodexCommand();
612
+ const versionCheck = await checkCommandVersion(command, [['--version'], ['-V']]);
613
+ if (!versionCheck.ok) {
614
+ // Codex not installed - return empty, don't cache
615
+ return [];
616
+ }
617
+ const models = await fetchCodexModels(command);
618
+ if (models && models.length) {
619
+ codexModelsCache = models;
620
+ codexModelsCacheAt = Date.now();
621
+ return models;
622
+ }
623
+ // Fetch failed - return empty, don't cache so it retries next time
624
+ return [];
625
+ }
626
+ export function runCodexPrompt({ prompt, resumeSessionId, model, reasoningEffort, repoRoot, cwd, providerDetailLevel, onEvent, signal, }) {
627
+ return new Promise((resolve) => {
628
+ const command = getCodexCommand();
629
+ const resolvedRepoRoot = repoRoot ? path.resolve(repoRoot) : null;
630
+ const resolvedCwd = cwd ? path.resolve(cwd) : null;
631
+ const runDir = resolvedCwd || resolvedRepoRoot || process.cwd();
632
+ const cdTarget = resolvedRepoRoot || resolvedCwd || '.';
633
+ const runAttempt = (mode) => new Promise((attemptResolve) => {
634
+ const args = buildCodexExecArgs({
635
+ prompt,
636
+ cdTarget,
637
+ resumeSessionId,
638
+ model,
639
+ reasoningEffort,
640
+ mode,
641
+ });
642
+ const argsPreview = [...args];
643
+ if (argsPreview.length > 0) {
644
+ argsPreview[argsPreview.length - 1] = '[prompt]';
645
+ }
646
+ debugLog('Codex', 'spawn', { command, args: argsPreview, cwd: runDir, mode });
647
+ const child = spawn(command, args, {
648
+ cwd: runDir,
649
+ env: { ...process.env },
650
+ stdio: ['ignore', 'pipe', 'pipe'],
651
+ });
652
+ if (signal) {
653
+ signal.addEventListener('abort', () => {
654
+ child.kill('SIGTERM');
655
+ });
656
+ }
657
+ let aggregated = '';
658
+ let finalSessionId = null;
659
+ let didFinalize = false;
660
+ let sawError = false;
661
+ const includeRaw = providerDetailLevel === 'raw';
662
+ const buildProviderDetail = (eventType, data, raw) => {
663
+ const detail = { eventType };
664
+ if (data && Object.keys(data).length)
665
+ detail.data = data;
666
+ if (includeRaw && raw !== undefined)
667
+ detail.raw = raw;
668
+ return detail;
669
+ };
670
+ const emit = (event) => {
671
+ if (finalSessionId) {
672
+ onEvent({ ...event, providerSessionId: finalSessionId });
673
+ }
674
+ else {
675
+ onEvent(event);
676
+ }
677
+ };
678
+ const emitError = (message, providerDetail) => {
679
+ if (sawError)
680
+ return;
681
+ sawError = true;
682
+ emit({ type: 'error', message, providerDetail });
683
+ };
684
+ const emitItemEvent = (item, phase) => {
685
+ const itemType = typeof item.type === 'string' ? item.type : '';
686
+ if (!itemType)
687
+ return;
688
+ const providerDetail = buildProviderDetail(phase === 'start' ? 'item.started' : 'item.completed', {
689
+ itemType,
690
+ itemId: item.id,
691
+ status: item.status,
692
+ }, item);
693
+ if (itemType === 'agent_message') {
694
+ if (phase === 'completed' && typeof item.text === 'string') {
695
+ emit({
696
+ type: 'message',
697
+ provider: 'codex',
698
+ role: 'assistant',
699
+ content: item.text,
700
+ contentParts: item,
701
+ providerDetail,
702
+ });
703
+ }
704
+ return;
705
+ }
706
+ if (itemType === 'reasoning') {
707
+ emit({
708
+ type: 'thinking',
709
+ provider: 'codex',
710
+ phase,
711
+ text: typeof item.text === 'string' ? item.text : undefined,
712
+ providerDetail,
713
+ });
714
+ return;
715
+ }
716
+ if (itemType === 'command_execution') {
717
+ const output = phase === 'completed'
718
+ ? {
719
+ output: item.aggregated_output,
720
+ exitCode: item.exit_code,
721
+ status: item.status,
722
+ }
723
+ : undefined;
724
+ emit({
725
+ type: 'tool_call',
726
+ provider: 'codex',
727
+ name: 'command_execution',
728
+ callId: item.id,
729
+ input: { command: item.command },
730
+ output,
731
+ phase,
732
+ providerDetail,
733
+ });
734
+ return;
735
+ }
736
+ emit({
737
+ type: 'tool_call',
738
+ provider: 'codex',
739
+ name: itemType,
740
+ callId: item.id,
741
+ input: phase === 'start' ? item : undefined,
742
+ output: phase === 'completed' ? item : undefined,
743
+ phase,
744
+ providerDetail,
745
+ });
746
+ };
747
+ let sawJson = false;
748
+ const stdoutLines = [];
749
+ const stderrLines = [];
750
+ const pushLine = (list, line) => {
751
+ if (!line)
752
+ return;
753
+ list.push(line);
754
+ if (list.length > 12)
755
+ list.shift();
756
+ };
757
+ const emitFinal = (text, providerDetail) => {
758
+ emit({ type: 'final', text, providerDetail });
759
+ };
760
+ const handleLine = (line, source) => {
761
+ const parsed = safeJsonParse(line);
762
+ if (!parsed || typeof parsed !== 'object') {
763
+ if (line.trim()) {
764
+ emit({ type: 'raw_line', line });
765
+ }
766
+ if (source === 'stdout') {
767
+ pushLine(stdoutLines, line);
768
+ }
769
+ else {
770
+ pushLine(stderrLines, line);
771
+ }
772
+ return;
773
+ }
774
+ sawJson = true;
775
+ const ev = parsed;
776
+ const normalized = normalizeEvent(ev);
777
+ const sid = extractSessionId(ev);
778
+ if (sid)
779
+ finalSessionId = sid;
780
+ const eventType = typeof ev.type === 'string' ? ev.type : normalized.type;
781
+ const detailData = {};
782
+ const threadId = ev.thread_id ?? ev.threadId;
783
+ if (typeof threadId === 'string' && threadId)
784
+ detailData.threadId = threadId;
785
+ const providerDetail = buildProviderDetail(eventType || 'unknown', detailData, ev);
786
+ let handled = false;
787
+ const usage = extractUsage(ev);
788
+ if (usage) {
789
+ emit({
790
+ type: 'usage',
791
+ inputTokens: usage.input_tokens,
792
+ outputTokens: usage.output_tokens,
793
+ providerDetail,
794
+ });
795
+ handled = true;
796
+ }
797
+ if (normalized.type === 'agent_message') {
798
+ const text = normalized.text;
799
+ if (typeof text === 'string' && text) {
800
+ aggregated += text;
801
+ emit({ type: 'delta', text, providerDetail });
802
+ emit({
803
+ type: 'message',
804
+ provider: 'codex',
805
+ role: 'assistant',
806
+ content: text,
807
+ contentParts: ev,
808
+ providerDetail,
809
+ });
810
+ handled = true;
811
+ }
812
+ }
813
+ else if (normalized.type === 'item.completed') {
814
+ const item = normalized.item;
815
+ if (item && typeof item === 'object') {
816
+ const itemDetail = buildProviderDetail('item.completed', {
817
+ itemType: item.type,
818
+ itemId: item.id,
819
+ status: item.status,
820
+ }, item);
821
+ if (item.type === 'command_execution' && typeof item.aggregated_output === 'string') {
822
+ emit({ type: 'delta', text: item.aggregated_output, providerDetail: itemDetail });
823
+ }
824
+ if (item.type === 'agent_message' && typeof item.text === 'string') {
825
+ aggregated += item.text;
826
+ emit({ type: 'delta', text: item.text, providerDetail: itemDetail });
827
+ }
828
+ }
829
+ }
830
+ if (normalized.type === 'item.started' && ev.item && typeof ev.item === 'object') {
831
+ emitItemEvent(ev.item, 'start');
832
+ handled = true;
833
+ }
834
+ if (normalized.type === 'item.completed' && ev.item && typeof ev.item === 'object') {
835
+ emitItemEvent(ev.item, 'completed');
836
+ handled = true;
837
+ }
838
+ if (normalized.type === 'error') {
839
+ emitError(normalized.message || 'Codex run failed', providerDetail);
840
+ handled = true;
841
+ }
842
+ if (isTerminalEvent(ev) && !didFinalize) {
843
+ if (ev.type === 'turn.failed') {
844
+ const message = ev.error?.message;
845
+ emitError(typeof message === 'string' ? message : 'Codex run failed', providerDetail);
846
+ didFinalize = true;
847
+ handled = true;
848
+ return;
849
+ }
850
+ if (!sawError) {
851
+ didFinalize = true;
852
+ emitFinal(aggregated, providerDetail);
853
+ handled = true;
854
+ }
855
+ }
856
+ if (!handled) {
857
+ emit({ type: 'detail', provider: 'codex', providerDetail });
858
+ }
859
+ };
860
+ const stdoutParser = createLineParser((line) => handleLine(line, 'stdout'));
861
+ const stderrParser = createLineParser((line) => handleLine(line, 'stderr'));
862
+ child.stdout?.on('data', stdoutParser);
863
+ child.stderr?.on('data', stderrParser);
864
+ child.on('close', (code) => {
865
+ if (!didFinalize) {
866
+ if (code && code !== 0) {
867
+ const hint = stderrLines.at(-1) || stdoutLines.at(-1) || '';
868
+ const suffix = hint ? `: ${hint}` : '';
869
+ const fallback = mode === 'modern' && !sawJson && shouldFallbackToLegacy([
870
+ ...stderrLines,
871
+ ...stdoutLines,
872
+ ]);
873
+ debugLog('Codex', 'exit', {
874
+ code,
875
+ stderr: stderrLines,
876
+ stdout: stdoutLines,
877
+ fallback,
878
+ });
879
+ if (fallback) {
880
+ attemptResolve({ sessionId: finalSessionId, fallback: true });
881
+ return;
882
+ }
883
+ emitError(`Codex exited with code ${code}${suffix}`);
884
+ }
885
+ else if (!sawError) {
886
+ emitFinal(aggregated);
887
+ }
888
+ }
889
+ attemptResolve({ sessionId: finalSessionId, fallback: false });
890
+ });
891
+ child.on('error', (err) => {
892
+ debugLog('Codex', 'spawn-error', { message: err?.message });
893
+ emitError(err?.message ?? 'Codex failed to start');
894
+ attemptResolve({ sessionId: finalSessionId, fallback: false });
895
+ });
896
+ });
897
+ void (async () => {
898
+ const primary = await runAttempt('modern');
899
+ if (primary.fallback) {
900
+ debugLog('Codex', 'fallback', { from: 'modern', to: 'legacy' });
901
+ const legacy = await runAttempt('legacy');
902
+ resolve({ sessionId: legacy.sessionId });
903
+ return;
904
+ }
905
+ resolve({ sessionId: primary.sessionId });
906
+ })();
907
+ });
908
+ }