@mearl/agent-runtime 0.0.0-stage → 2.22.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,889 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+ import { spawn } from 'node:child_process';
6
+ import { agentExecutableCandidatePaths, AgentExecutableResolver, executableFileCandidates, probeAgentExecutable, } from './executable-resolver.js';
7
+ import { agentSpawnCommand, createAgentChildEnv } from './process-env.js';
8
+ /** config/read uses null for absent options; thread config overrides must contain TOML values. */
9
+ function configOverride(value) {
10
+ if (Array.isArray(value))
11
+ return value.map(configOverride);
12
+ if (value && typeof value === 'object')
13
+ return Object.fromEntries(Object.entries(value)
14
+ .filter(([, field]) => field !== null && field !== undefined)
15
+ .map(([key, field]) => [key, configOverride(field)]));
16
+ return value;
17
+ }
18
+ const REQUEST_TIMEOUT_MS = 30_000;
19
+ const PROBE_TIMEOUT_MS = 5_000;
20
+ const MAX_MODEL_CHARS = 256;
21
+ const MAX_VERSION_OUTPUT_CHARS = 1_024;
22
+ const CODEX_NOT_FOUND_ERROR = 'Codex was not found. Install @openai/codex, install the Codex App, or set MEARL_CODEX_PATH.';
23
+ const CODEX_VERSION_PATTERN = /^(?:codex-cli\s+)?v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
24
+ export function normalizeCodexVersion(value) {
25
+ if (typeof value !== 'string' || value.length > MAX_VERSION_OUTPUT_CHARS)
26
+ return null;
27
+ const match = CODEX_VERSION_PATTERN.exec(value.trim());
28
+ return match?.[1] ?? null;
29
+ }
30
+ function parsedCodexVersion(value) {
31
+ const normalized = normalizeCodexVersion(value);
32
+ if (!normalized)
33
+ return null;
34
+ const withoutBuild = normalized.split('+', 1)[0];
35
+ const prereleaseSeparator = withoutBuild.indexOf('-');
36
+ const coreValue = prereleaseSeparator === -1 ? withoutBuild : withoutBuild.slice(0, prereleaseSeparator);
37
+ const prereleaseValue = prereleaseSeparator === -1 ? undefined : withoutBuild.slice(prereleaseSeparator + 1);
38
+ const core = coreValue?.split('.').map(Number);
39
+ if (core?.length !== 3 ||
40
+ core.some(component => !Number.isSafeInteger(component) || component < 0)) {
41
+ return null;
42
+ }
43
+ return {
44
+ core: core,
45
+ prerelease: prereleaseValue ? prereleaseValue.split('.') : [],
46
+ };
47
+ }
48
+ export function compareCodexVersions(leftValue, rightValue) {
49
+ const left = parsedCodexVersion(leftValue);
50
+ const right = parsedCodexVersion(rightValue);
51
+ if (!left || !right)
52
+ return leftValue.localeCompare(rightValue, undefined, { numeric: true });
53
+ for (let index = 0; index < left.core.length; index += 1) {
54
+ const difference = left.core[index] - right.core[index];
55
+ if (difference !== 0)
56
+ return difference;
57
+ }
58
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
59
+ if (left.prerelease.length === right.prerelease.length)
60
+ return 0;
61
+ return left.prerelease.length === 0 ? 1 : -1;
62
+ }
63
+ const identifierCount = Math.max(left.prerelease.length, right.prerelease.length);
64
+ for (let index = 0; index < identifierCount; index += 1) {
65
+ const leftIdentifier = left.prerelease[index];
66
+ const rightIdentifier = right.prerelease[index];
67
+ if (leftIdentifier === undefined || rightIdentifier === undefined) {
68
+ if (leftIdentifier === rightIdentifier)
69
+ return 0;
70
+ return leftIdentifier === undefined ? -1 : 1;
71
+ }
72
+ if (leftIdentifier === rightIdentifier)
73
+ continue;
74
+ const leftNumeric = /^\d+$/.test(leftIdentifier);
75
+ const rightNumeric = /^\d+$/.test(rightIdentifier);
76
+ if (leftNumeric && rightNumeric)
77
+ return Number(leftIdentifier) - Number(rightIdentifier);
78
+ if (leftNumeric !== rightNumeric)
79
+ return leftNumeric ? -1 : 1;
80
+ return leftIdentifier.localeCompare(rightIdentifier);
81
+ }
82
+ return 0;
83
+ }
84
+ export function codexAppExecutableCandidatePaths(options = {}) {
85
+ const env = options.env ?? process.env;
86
+ const home = (options.homedir ?? os.homedir)();
87
+ const platform = options.platform ?? process.platform;
88
+ if (platform === 'darwin') {
89
+ return ['/Applications', path.join(home, 'Applications')].flatMap(root => ['ChatGPT.app', 'Codex.app'].flatMap(app => {
90
+ const resources = path.join(root, app, 'Contents', 'Resources');
91
+ return [
92
+ path.join(resources, 'codex-cli', 'CodexCLI.app', 'Contents', 'MacOS', 'codex'),
93
+ path.join(resources, 'codex'),
94
+ ];
95
+ }));
96
+ }
97
+ if (platform === 'win32') {
98
+ const localPrograms = path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Programs');
99
+ const roots = [localPrograms, env.ProgramFiles, env['ProgramFiles(x86)']].filter((root) => typeof root === 'string' && root.length > 0);
100
+ return roots.flatMap(root => ['ChatGPT', 'Codex'].map(app => path.join(root, app, 'resources', 'codex.exe')));
101
+ }
102
+ return [];
103
+ }
104
+ function isIdeBundledCodexPath(candidate, home) {
105
+ const resolvedCandidates = [path.resolve(candidate)];
106
+ try {
107
+ resolvedCandidates.push(fs.realpathSync(candidate));
108
+ }
109
+ catch {
110
+ // Missing candidates are filtered after discovery.
111
+ }
112
+ return ['.vscode', '.vscode-insiders', '.cursor', '.windsurf'].some(directory => {
113
+ const extensionsRoot = `${path.join(home, directory, 'extensions')}${path.sep}`;
114
+ return resolvedCandidates.some(resolved => resolved.startsWith(extensionsRoot));
115
+ });
116
+ }
117
+ export function codexExecutableCandidatePaths(options = {}) {
118
+ const env = options.env ?? process.env;
119
+ const home = (options.homedir ?? os.homedir)();
120
+ const platform = options.platform ?? process.platform;
121
+ const executableName = platform === 'win32' ? 'codex.cmd' : 'codex';
122
+ return agentExecutableCandidatePaths({
123
+ executableNames: [executableName],
124
+ configuredPath: env.MEARL_CODEX_PATH,
125
+ env,
126
+ home,
127
+ platform,
128
+ processExecPath: options.processExecPath,
129
+ readdirSync: options.readdirSync,
130
+ extraUserCandidates: codexAppExecutableCandidatePaths({
131
+ env,
132
+ homedir: () => home,
133
+ platform,
134
+ }),
135
+ }).filter(candidate => candidate === env.MEARL_CODEX_PATH || !isIdeBundledCodexPath(candidate, home));
136
+ }
137
+ function codexPackageVersion(executable) {
138
+ const packageFiles = [];
139
+ try {
140
+ const resolved = fs.realpathSync(executable);
141
+ if (resolved.split(path.sep).includes('node_modules')) {
142
+ let directory = path.dirname(resolved);
143
+ for (let depth = 0; depth < 6; depth += 1) {
144
+ packageFiles.push(path.join(directory, 'package.json'));
145
+ const parent = path.dirname(directory);
146
+ if (parent === directory)
147
+ break;
148
+ directory = parent;
149
+ }
150
+ }
151
+ }
152
+ catch {
153
+ // The executable resolver reports inaccessible candidates separately.
154
+ }
155
+ if (/\.(?:cmd|bat)$/i.test(executable)) {
156
+ packageFiles.push(path.join(path.dirname(executable), 'node_modules', '@openai', 'codex', 'package.json'));
157
+ }
158
+ for (const packageFile of packageFiles) {
159
+ try {
160
+ const manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
161
+ if (manifest.name === '@openai/codex') {
162
+ const version = normalizeCodexVersion(manifest.version);
163
+ if (version)
164
+ return version;
165
+ }
166
+ }
167
+ catch {
168
+ // Continue to other package locations, then fall back to the executable.
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+ function codexExecutableVersion(executable) {
174
+ const packageVersion = codexPackageVersion(executable);
175
+ if (packageVersion)
176
+ return Promise.resolve(packageVersion);
177
+ return new Promise(resolve => {
178
+ let settled = false;
179
+ let output = '';
180
+ const launch = agentSpawnCommand(executable, ['--version']);
181
+ const child = spawn(launch.command, launch.args, {
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ windowsHide: true,
184
+ env: createAgentChildEnv(),
185
+ });
186
+ const finish = (version) => {
187
+ if (settled)
188
+ return;
189
+ settled = true;
190
+ clearTimeout(timer);
191
+ resolve(version);
192
+ };
193
+ const capture = (chunk) => {
194
+ const remaining = MAX_VERSION_OUTPUT_CHARS - output.length;
195
+ if (remaining > 0)
196
+ output += chunk.toString('utf8').slice(0, remaining);
197
+ };
198
+ child.stdout.on('data', capture);
199
+ child.stderr.on('data', capture);
200
+ child.once('error', () => finish(null));
201
+ child.once('exit', code => finish(code === 0 ? normalizeCodexVersion(output) : null));
202
+ const timer = setTimeout(() => {
203
+ child.kill('SIGTERM');
204
+ finish(null);
205
+ }, PROBE_TIMEOUT_MS);
206
+ });
207
+ }
208
+ function codexModelInfo(value) {
209
+ if (typeof value !== 'string')
210
+ return undefined;
211
+ const id = value.trim().slice(0, MAX_MODEL_CHARS);
212
+ return id ? { id } : undefined;
213
+ }
214
+ function fullAccessWithApprovals() {
215
+ return {
216
+ sandbox: 'danger-full-access',
217
+ approvalPolicy: 'untrusted',
218
+ };
219
+ }
220
+ function timestampMs(value) {
221
+ if (typeof value === 'number' && Number.isFinite(value)) {
222
+ return value < 1_000_000_000_000 ? value * 1_000 : value;
223
+ }
224
+ if (typeof value === 'string') {
225
+ const parsed = Date.parse(value);
226
+ return Number.isNaN(parsed) ? undefined : parsed;
227
+ }
228
+ return undefined;
229
+ }
230
+ function codexSkillScope(value) {
231
+ if (value === 'user')
232
+ return 'global';
233
+ if (value === 'repo')
234
+ return 'project';
235
+ if (value === 'system' || value === 'admin')
236
+ return value;
237
+ return 'unknown';
238
+ }
239
+ function userMessageText(item) {
240
+ if (!Array.isArray(item?.content))
241
+ return '';
242
+ return item.content
243
+ .flatMap((part) => part?.type === 'text' && typeof part.text === 'string' ? [part.text] : [])
244
+ .join('\n');
245
+ }
246
+ function userMessageImageCount(item) {
247
+ if (!Array.isArray(item?.content))
248
+ return 0;
249
+ return item.content.filter((part) => part?.type === 'image' || part?.type === 'localImage')
250
+ .length;
251
+ }
252
+ function threadHistoryEvents(thread) {
253
+ if (!Array.isArray(thread?.turns))
254
+ return [];
255
+ const events = [];
256
+ for (const turn of thread.turns.slice(-100)) {
257
+ if (!turn || typeof turn.id !== 'string' || !Array.isArray(turn.items))
258
+ continue;
259
+ const timestamp = timestampMs(turn.completedAt ?? turn.startedAt);
260
+ for (const item of turn.items) {
261
+ if (!item || typeof item.id !== 'string' || typeof item.type !== 'string')
262
+ continue;
263
+ if (item.type === 'userMessage') {
264
+ const text = userMessageText(item);
265
+ const imageCount = userMessageImageCount(item);
266
+ if (text || imageCount > 0) {
267
+ events.push({
268
+ kind: 'lifecycle',
269
+ method: 'chat/userMessage',
270
+ params: { text, ...(imageCount > 0 ? { imageCount } : {}) },
271
+ ...(timestamp !== undefined ? { timestamp } : {}),
272
+ });
273
+ }
274
+ continue;
275
+ }
276
+ events.push({
277
+ kind: 'notification',
278
+ method: 'item/completed',
279
+ params: { turnId: turn.id, item },
280
+ ...(timestamp !== undefined ? { timestamp } : {}),
281
+ });
282
+ }
283
+ }
284
+ return events;
285
+ }
286
+ export function codexThreadUrl(providerSessionId) {
287
+ if (!/^[a-zA-Z0-9_-]+$/.test(providerSessionId)) {
288
+ throw new Error('Codex returned an invalid thread id');
289
+ }
290
+ return `codex://threads/${providerSessionId}`;
291
+ }
292
+ export function codexWorkspaceUrl(directory) {
293
+ return `codex://threads/new?path=${encodeURIComponent(directory)}`;
294
+ }
295
+ export function externalOpenCommand(url, platform = process.platform) {
296
+ if (platform === 'darwin')
297
+ return { command: 'open', args: [url] };
298
+ if (platform === 'win32') {
299
+ return { command: 'cmd.exe', args: ['/d', '/s', '/c', 'start', '', url] };
300
+ }
301
+ return { command: 'xdg-open', args: [url] };
302
+ }
303
+ function openExternalUrl(url) {
304
+ const { command, args } = externalOpenCommand(url);
305
+ return new Promise((resolve, reject) => {
306
+ const child = spawn(command, args, { stdio: 'ignore', windowsHide: true });
307
+ child.once('error', error => {
308
+ reject(new Error(`Unable to open Codex App: ${error.message}`));
309
+ });
310
+ child.once('close', code => {
311
+ if (code === 0)
312
+ resolve();
313
+ else
314
+ reject(new Error('Unable to open Codex App. Make sure it is installed.'));
315
+ });
316
+ });
317
+ }
318
+ export class CodexAgentProvider {
319
+ log;
320
+ clientVersion;
321
+ mearlSocketPath;
322
+ id = 'codex';
323
+ process = null;
324
+ initialized = false;
325
+ lifecycle = 0;
326
+ modelRuntime;
327
+ startPromise = null;
328
+ nextRequestId = 1;
329
+ pendingRequests = new Map();
330
+ listeners = new Set();
331
+ knownThreadIds = new Set();
332
+ sessionModels = new Map();
333
+ executableResolver;
334
+ constructor(log, clientVersion, mearlSocketPath) {
335
+ this.log = log;
336
+ this.clientVersion = clientVersion;
337
+ this.mearlSocketPath = mearlSocketPath;
338
+ this.executableResolver = new AgentExecutableResolver({
339
+ candidates: () => this.resolveExecutableCandidates(),
340
+ probe: executable => this.probeExecutable(executable),
341
+ getVersion: executable => this.readExecutableVersion(executable),
342
+ compareVersions: compareCodexVersions,
343
+ preferred: executable => executable === process.env.MEARL_CODEX_PATH,
344
+ notFoundError: CODEX_NOT_FOUND_ERROR,
345
+ unusableError: 'Codex candidates were found, but none supports app-server.',
346
+ onFailure: (executable, error) => {
347
+ this.log('[Codex] Skipping unusable runtime:', executable, error);
348
+ },
349
+ onSelection: (executable, version) => {
350
+ this.modelRuntime = {
351
+ executable,
352
+ ...(version ? { version } : {}),
353
+ selection: executable === process.env.MEARL_CODEX_PATH ? 'explicit' : 'automatic',
354
+ started: false,
355
+ };
356
+ this.modelDiagnostic({ runtime: this.modelRuntime });
357
+ this.log('[Codex] Selected runtime:', executable, version
358
+ ? `(version ${version})`
359
+ : executable === process.env.MEARL_CODEX_PATH
360
+ ? '(explicitly configured)'
361
+ : '(version unavailable)');
362
+ },
363
+ });
364
+ }
365
+ async getDescriptor() {
366
+ const { error } = await this.resolveExecutable();
367
+ return {
368
+ id: this.id,
369
+ name: 'Codex',
370
+ available: error === null,
371
+ capabilities: {
372
+ streaming: true,
373
+ resume: true,
374
+ listSessions: true,
375
+ refreshHistory: true,
376
+ imageInput: true,
377
+ interrupt: true,
378
+ requests: true,
379
+ },
380
+ ...(error ? { error } : {}),
381
+ setup: {
382
+ installCommand: 'npm install -g @openai/codex',
383
+ loginCommand: 'codex login',
384
+ docsUrl: 'https://developers.openai.com/codex/cli',
385
+ },
386
+ };
387
+ }
388
+ async prepare() {
389
+ await this.ensureStarted();
390
+ }
391
+ async listModels() {
392
+ await this.ensureStarted();
393
+ const models = new Map();
394
+ let cursor;
395
+ const cursors = new Set();
396
+ for (let page = 0; page < 4; page++) {
397
+ const result = await this.request('model/list', {
398
+ limit: 100,
399
+ ...(cursor ? { cursor } : {}),
400
+ });
401
+ if (!Array.isArray(result?.data))
402
+ throw new Error('Invalid Codex model list');
403
+ for (const value of result.data) {
404
+ const id = value?.model ?? value?.id;
405
+ if (value?.hidden || typeof id !== 'string' || !id || id.length > 256 || models.has(id))
406
+ continue;
407
+ const efforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
408
+ const reasoningEfforts = Array.isArray(value.supportedReasoningEfforts)
409
+ ? efforts.filter(effort => value.supportedReasoningEfforts.some((option) => option?.reasoningEffort === effort))
410
+ : undefined;
411
+ models.set(id, {
412
+ id,
413
+ displayName: typeof value.displayName === 'string' ? value.displayName.slice(0, 256) : id,
414
+ ...(typeof value.description === 'string'
415
+ ? { description: value.description.slice(0, 2000) }
416
+ : {}),
417
+ isDefault: value.isDefault === true,
418
+ ...(reasoningEfforts ? { reasoningEfforts } : {}),
419
+ ...(efforts.includes(value.defaultReasoningEffort)
420
+ ? { defaultReasoningEffort: value.defaultReasoningEffort }
421
+ : {}),
422
+ });
423
+ if (models.size >= 200)
424
+ break;
425
+ }
426
+ if (!result.nextCursor || models.size >= 200)
427
+ break;
428
+ if (typeof result.nextCursor !== 'string' || cursors.has(result.nextCursor))
429
+ throw new Error('Invalid Codex model cursor');
430
+ cursor = result.nextCursor;
431
+ cursors.add(result.nextCursor);
432
+ }
433
+ return [...models.values()];
434
+ }
435
+ onEvent(listener) {
436
+ this.listeners.add(listener);
437
+ return () => this.listeners.delete(listener);
438
+ }
439
+ async listSessions(options) {
440
+ await this.ensureStarted();
441
+ const result = await this.request('thread/list', {
442
+ cursor: options.cursor ?? null,
443
+ limit: options.limit,
444
+ sortKey: 'updated_at',
445
+ sortDirection: 'desc',
446
+ archived: false,
447
+ ...(options.cwd ? { cwd: options.cwd } : {}),
448
+ });
449
+ const sessions = Array.isArray(result?.data)
450
+ ? result.data.flatMap((thread) => {
451
+ if (!thread || typeof thread.id !== 'string')
452
+ return [];
453
+ const title = typeof thread.name === 'string' && thread.name.trim()
454
+ ? thread.name.trim()
455
+ : typeof thread.preview === 'string' && thread.preview.trim()
456
+ ? thread.preview.trim()
457
+ : undefined;
458
+ const cwd = typeof thread.cwd === 'string' ? thread.cwd : undefined;
459
+ const updatedAt = timestampMs(thread.recencyAt ?? thread.updatedAt);
460
+ const ephemeral = typeof thread.ephemeral === 'boolean' ? thread.ephemeral : undefined;
461
+ return [
462
+ {
463
+ providerSessionId: thread.id,
464
+ ...(title ? { title } : {}),
465
+ ...(cwd ? { cwd } : {}),
466
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
467
+ ...(ephemeral !== undefined ? { ephemeral } : {}),
468
+ },
469
+ ];
470
+ })
471
+ : [];
472
+ return {
473
+ sessions,
474
+ ...(typeof result?.nextCursor === 'string' ? { nextCursor: result.nextCursor } : {}),
475
+ };
476
+ }
477
+ async listSkills(cwd) {
478
+ await this.ensureStarted();
479
+ const result = await this.request('skills/list', {
480
+ cwds: [cwd || os.homedir()],
481
+ forceReload: true,
482
+ });
483
+ if (!Array.isArray(result?.data))
484
+ return [];
485
+ return result.data.flatMap((entry) => Array.isArray(entry?.skills)
486
+ ? entry.skills.flatMap((skill) => {
487
+ if (!skill || typeof skill.name !== 'string' || typeof skill.path !== 'string') {
488
+ return [];
489
+ }
490
+ return [
491
+ {
492
+ name: skill.name,
493
+ path: skill.path,
494
+ scope: codexSkillScope(skill.scope),
495
+ },
496
+ ];
497
+ })
498
+ : []);
499
+ }
500
+ async startSession(options) {
501
+ await this.ensureStarted();
502
+ const cwd = options.cwd || process.env.MEARL_AGENT_CWD || os.homedir();
503
+ const config = options.modelExecution ? await this.modelExecutionConfig() : undefined;
504
+ const result = await this.request('thread/start', {
505
+ ...(config ? { config } : {}),
506
+ cwd,
507
+ ...(options.modelId ? { model: options.modelId } : {}),
508
+ ...(options.readOnly
509
+ ? { sandbox: 'read-only', approvalPolicy: 'never' }
510
+ : fullAccessWithApprovals()),
511
+ ...(options.ephemeral !== undefined ? { ephemeral: options.ephemeral } : {}),
512
+ ...(options.developerInstructions
513
+ ? { developerInstructions: options.developerInstructions }
514
+ : {}),
515
+ });
516
+ const threadId = result?.thread?.id;
517
+ if (typeof threadId !== 'string' || !threadId) {
518
+ throw new Error('Codex did not return a thread id');
519
+ }
520
+ if (options.modelExecution)
521
+ this.modelDiagnostic({ threadRestrictionsAccepted: true });
522
+ this.knownThreadIds.add(threadId);
523
+ const model = codexModelInfo(result?.model);
524
+ if (model)
525
+ this.sessionModels.set(threadId, model);
526
+ return threadId;
527
+ }
528
+ async modelExecutionConfig() {
529
+ const result = await this.request('config/read', { includeLayers: false });
530
+ const config = result?.config;
531
+ if (!config || typeof config !== 'object')
532
+ throw new Error('Codex cannot report its effective execution configuration');
533
+ const home = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
534
+ const store = config.cli_auth_credentials_store;
535
+ this.modelDiagnostic({
536
+ configuration: {
537
+ source: process.env.CODEX_HOME ? 'environment' : 'default',
538
+ home,
539
+ filePresent: fs.existsSync(path.join(home, 'config.toml')),
540
+ ...(typeof config.model_provider === 'string'
541
+ ? { baseProvider: config.model_provider }
542
+ : {}),
543
+ ...(typeof store === 'string'
544
+ ? {
545
+ credentialStore: ['file', 'keyring', 'auto', 'ephemeral'].includes(store)
546
+ ? store
547
+ : 'unknown',
548
+ }
549
+ : {}),
550
+ },
551
+ });
552
+ const mcp = config.mcp_servers ?? {};
553
+ const apps = config.apps ?? {};
554
+ if (typeof mcp !== 'object' ||
555
+ Array.isArray(mcp) ||
556
+ typeof apps !== 'object' ||
557
+ Array.isArray(apps))
558
+ throw new Error('Invalid Codex model tool configuration');
559
+ return configOverride({
560
+ mcp_servers: Object.fromEntries(Object.keys(mcp).map(name => [name, { ...mcp[name], enabled: false }])),
561
+ apps: Object.fromEntries(['_default', ...Object.keys(apps)].map(name => [
562
+ name,
563
+ { ...(apps[name] ?? {}), enabled: false },
564
+ ])),
565
+ web_search: 'disabled',
566
+ features: {
567
+ apps: false,
568
+ plugins: false,
569
+ hooks: false,
570
+ codex_hooks: false,
571
+ plugin_hooks: false,
572
+ multi_agent: false,
573
+ multi_agent_v2: false,
574
+ collab: false,
575
+ computer_use: false,
576
+ browser_use: false,
577
+ image_generation: false,
578
+ imagegenext: false,
579
+ memory_tool: false,
580
+ memories: false,
581
+ skill_search: false,
582
+ },
583
+ });
584
+ }
585
+ async resumeSession(providerSessionId, options) {
586
+ await this.ensureStarted();
587
+ const result = await this.request('thread/resume', {
588
+ threadId: providerSessionId,
589
+ ...(options.cwd ? { cwd: options.cwd } : {}),
590
+ ...fullAccessWithApprovals(),
591
+ ...(options.developerInstructions
592
+ ? { developerInstructions: options.developerInstructions }
593
+ : {}),
594
+ });
595
+ const threadId = result?.thread?.id;
596
+ if (typeof threadId !== 'string' || !threadId) {
597
+ throw new Error('Codex did not return a resumed thread id');
598
+ }
599
+ this.knownThreadIds.add(threadId);
600
+ const model = codexModelInfo(result?.model);
601
+ if (model)
602
+ this.sessionModels.set(threadId, model);
603
+ return {
604
+ providerSessionId: threadId,
605
+ history: threadHistoryEvents(result.thread),
606
+ historyComplete: true,
607
+ };
608
+ }
609
+ getSessionModel(providerSessionId) {
610
+ const model = this.sessionModels.get(providerSessionId);
611
+ return model ? { ...model } : undefined;
612
+ }
613
+ async sendMessage(providerSessionId, message, images = [], options = {}) {
614
+ await this.ensureStarted();
615
+ const result = await this.request('turn/start', {
616
+ threadId: providerSessionId,
617
+ input: [
618
+ ...(message ? [{ type: 'text', text: message, text_elements: [] }] : []),
619
+ ...images.map(image => ({
620
+ type: 'image',
621
+ url: `data:${image.mimeType};base64,${image.data}`,
622
+ })),
623
+ ],
624
+ summary: options.reasoningSummary ?? 'detailed',
625
+ ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
626
+ ...(options.modelExecution
627
+ ? { approvalPolicy: 'never', sandboxPolicy: { type: 'readOnly', networkAccess: false } }
628
+ : {}),
629
+ ...(options.reasoningEffort ? { effort: options.reasoningEffort } : {}),
630
+ });
631
+ const turnId = result?.turn?.id;
632
+ if (typeof turnId !== 'string' || !turnId) {
633
+ throw new Error('Codex did not return a turn id');
634
+ }
635
+ if (options.modelExecution)
636
+ this.modelDiagnostic({ turnRestrictionsAccepted: true });
637
+ return { turnId };
638
+ }
639
+ async interrupt(providerSessionId, turnId) {
640
+ await this.ensureStarted();
641
+ await this.request('turn/interrupt', { threadId: providerSessionId, turnId });
642
+ }
643
+ async closeSession(providerSessionId) {
644
+ this.knownThreadIds.delete(providerSessionId);
645
+ this.sessionModels.delete(providerSessionId);
646
+ if (!this.initialized)
647
+ return;
648
+ void this.request('thread/unsubscribe', { threadId: providerSessionId }).catch(error => {
649
+ this.log('[Codex] Unable to unsubscribe thread:', String(error));
650
+ });
651
+ }
652
+ openSession(providerSessionId) {
653
+ return openExternalUrl(codexThreadUrl(providerSessionId));
654
+ }
655
+ openWorkspace(cwd) {
656
+ return openExternalUrl(codexWorkspaceUrl(cwd));
657
+ }
658
+ respondToRequest(requestId, result) {
659
+ this.write({ id: requestId, result });
660
+ }
661
+ dispose() {
662
+ this.lifecycle++;
663
+ this.rejectPending(new Error('Codex provider disposed'));
664
+ const child = this.process;
665
+ this.process = null;
666
+ this.initialized = false;
667
+ this.startPromise = null;
668
+ this.sessionModels.clear();
669
+ this.executableResolver.reset();
670
+ if (child && !child.killed) {
671
+ child.kill('SIGTERM');
672
+ }
673
+ }
674
+ async ensureStarted() {
675
+ if (this.process && !this.process.killed && this.initialized)
676
+ return;
677
+ if (this.startPromise)
678
+ return this.startPromise;
679
+ if (this.process && !this.process.killed) {
680
+ this.process.kill('SIGTERM');
681
+ this.process = null;
682
+ }
683
+ const pending = this.start();
684
+ this.startPromise = pending;
685
+ try {
686
+ await pending;
687
+ }
688
+ catch (error) {
689
+ if (this.startPromise === pending)
690
+ this.startPromise = null;
691
+ throw error;
692
+ }
693
+ }
694
+ async start() {
695
+ const lifecycle = this.lifecycle;
696
+ const resolution = await this.resolveExecutable();
697
+ if (lifecycle !== this.lifecycle)
698
+ throw new Error('Codex provider disposed during startup');
699
+ if (resolution.executable === null) {
700
+ throw new Error(resolution.error);
701
+ }
702
+ const { executable } = resolution;
703
+ const launch = agentSpawnCommand(executable, ['app-server', '--listen', 'stdio://']);
704
+ const childEnv = createAgentChildEnv();
705
+ if (this.mearlSocketPath)
706
+ childEnv.MEARL_SOCKET_PATH = this.mearlSocketPath;
707
+ const child = spawn(launch.command, launch.args, {
708
+ stdio: ['pipe', 'pipe', 'pipe'],
709
+ windowsHide: true,
710
+ env: childEnv,
711
+ });
712
+ this.process = child;
713
+ const lines = readline.createInterface({ input: child.stdout });
714
+ lines.on('line', line => this.handleLine(line));
715
+ child.stderr.on('data', chunk => {
716
+ const message = chunk.toString('utf8').trim();
717
+ if (message)
718
+ this.log('[Codex]', message);
719
+ });
720
+ child.once('error', error => this.handleExit(child, error));
721
+ child.once('exit', (code, signal) => {
722
+ this.handleExit(child, new Error(`Codex app-server exited (code=${String(code)}, signal=${String(signal)})`));
723
+ });
724
+ try {
725
+ await this.request('initialize', {
726
+ clientInfo: {
727
+ name: 'mearl',
728
+ title: 'Mearl',
729
+ version: this.clientVersion,
730
+ },
731
+ capabilities: { experimentalApi: true },
732
+ });
733
+ this.write({ method: 'initialized', params: {} });
734
+ this.initialized = true;
735
+ if (this.modelRuntime) {
736
+ this.modelRuntime = { ...this.modelRuntime, started: true };
737
+ this.modelDiagnostic({ runtime: this.modelRuntime });
738
+ }
739
+ }
740
+ catch (error) {
741
+ const startupError = error instanceof Error ? error : new Error(String(error));
742
+ this.handleExit(child, startupError);
743
+ if (!child.killed)
744
+ child.kill('SIGTERM');
745
+ throw startupError;
746
+ }
747
+ }
748
+ handleLine(line) {
749
+ let message;
750
+ try {
751
+ message = JSON.parse(line);
752
+ }
753
+ catch {
754
+ this.log('[Codex] Invalid JSONL message:', line);
755
+ return;
756
+ }
757
+ if (message.method) {
758
+ const isRequest = message.id !== undefined;
759
+ const providerSessionId = this.extractThreadId(message.params);
760
+ if (!isRequest && providerSessionId && message.method === 'thread/settings/updated') {
761
+ const model = codexModelInfo(message.params?.threadSettings?.model);
762
+ if (model) {
763
+ this.sessionModels.set(providerSessionId, model);
764
+ this.emit({
765
+ providerSessionId,
766
+ kind: 'notification',
767
+ method: 'session/model/updated',
768
+ params: { model },
769
+ });
770
+ }
771
+ }
772
+ else if (!isRequest && providerSessionId && message.method === 'model/rerouted') {
773
+ const model = codexModelInfo(message.params?.toModel);
774
+ if (model) {
775
+ this.emit({
776
+ providerSessionId,
777
+ kind: 'notification',
778
+ method: 'session/activeModel/updated',
779
+ params: { model },
780
+ });
781
+ }
782
+ }
783
+ this.emit({
784
+ providerSessionId,
785
+ kind: isRequest ? 'request' : message.method === 'error' ? 'error' : 'notification',
786
+ method: message.method,
787
+ params: message.params ?? {},
788
+ ...(isRequest ? { requestId: message.id } : {}),
789
+ });
790
+ return;
791
+ }
792
+ if (typeof message.id !== 'number')
793
+ return;
794
+ const pending = this.pendingRequests.get(message.id);
795
+ if (!pending)
796
+ return;
797
+ clearTimeout(pending.timer);
798
+ this.pendingRequests.delete(message.id);
799
+ if (message.error) {
800
+ pending.reject(new Error(message.error.message || `Codex error ${message.error.code ?? ''}`));
801
+ }
802
+ else {
803
+ pending.resolve(message.result);
804
+ }
805
+ }
806
+ request(method, params) {
807
+ const id = this.nextRequestId++;
808
+ return new Promise((resolve, reject) => {
809
+ const timer = setTimeout(() => {
810
+ this.pendingRequests.delete(id);
811
+ reject(new Error(`Codex request timed out: ${method}`));
812
+ }, REQUEST_TIMEOUT_MS);
813
+ this.pendingRequests.set(id, { resolve, reject, timer });
814
+ try {
815
+ this.write({ id, method, params });
816
+ }
817
+ catch (error) {
818
+ clearTimeout(timer);
819
+ this.pendingRequests.delete(id);
820
+ reject(error instanceof Error ? error : new Error(String(error)));
821
+ }
822
+ });
823
+ }
824
+ write(message) {
825
+ const child = this.process;
826
+ if (!child || child.killed || !child.stdin.writable) {
827
+ throw new Error('Codex app-server is not running');
828
+ }
829
+ child.stdin.write(`${JSON.stringify(message)}\n`);
830
+ }
831
+ handleExit(child, error) {
832
+ if (this.process !== child)
833
+ return;
834
+ this.process = null;
835
+ this.initialized = false;
836
+ this.startPromise = null;
837
+ this.executableResolver.reset();
838
+ this.rejectPending(error);
839
+ for (const providerSessionId of this.knownThreadIds) {
840
+ this.emit({
841
+ providerSessionId,
842
+ kind: 'error',
843
+ method: 'provider/exited',
844
+ params: { message: error.message },
845
+ });
846
+ }
847
+ }
848
+ rejectPending(error) {
849
+ for (const [id, pending] of this.pendingRequests) {
850
+ clearTimeout(pending.timer);
851
+ pending.reject(error);
852
+ this.pendingRequests.delete(id);
853
+ }
854
+ }
855
+ modelDiagnostic(metadata) {
856
+ this.emit({ kind: 'notification', method: 'provider/model/diagnostic', params: metadata });
857
+ }
858
+ emit(event) {
859
+ for (const listener of this.listeners)
860
+ listener(event);
861
+ }
862
+ extractThreadId(params) {
863
+ const candidates = [
864
+ params?.threadId,
865
+ params?.conversationId,
866
+ params?.thread?.id,
867
+ params?.turn?.threadId,
868
+ ];
869
+ return candidates.find(value => typeof value === 'string');
870
+ }
871
+ probeExecutable(executable) {
872
+ return probeAgentExecutable(executable, {
873
+ args: ['app-server', '--help'],
874
+ timeoutMs: PROBE_TIMEOUT_MS,
875
+ timeoutError: 'Timed out while checking Codex app-server availability.',
876
+ startError: error => `Failed to start Codex: ${error.message}`,
877
+ exitError: code => `Codex app-server check exited with code ${String(code)}`,
878
+ });
879
+ }
880
+ readExecutableVersion(executable) {
881
+ return codexExecutableVersion(executable);
882
+ }
883
+ resolveExecutable() {
884
+ return this.executableResolver.resolve();
885
+ }
886
+ resolveExecutableCandidates() {
887
+ return executableFileCandidates(codexExecutableCandidatePaths());
888
+ }
889
+ }