@mindexec/cli 0.2.476 → 0.2.478

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7690 -7690
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  64. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  65. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  66. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  67. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  68. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  69. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  70. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  71. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  72. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  73. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  74. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  75. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10517
  76. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  77. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  78. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  79. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  80. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  81. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  82. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  100. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  101. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  102. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  103. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  104. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  105. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  106. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  107. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  108. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  109. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  110. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  111. package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
  112. package/wwwroot/_framework/{MindExecution.Core.cx2pre7qz2.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
  113. package/wwwroot/_framework/{MindExecution.Kernel.engfih12kr.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
  114. package/wwwroot/_framework/{MindExecution.Plugins.Admin.xfunemn9zi.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
  115. package/wwwroot/_framework/{MindExecution.Plugins.Business.ja94a5ze3i.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
  116. package/wwwroot/_framework/{MindExecution.Plugins.Concept.3q05vzviqb.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
  117. package/wwwroot/_framework/{MindExecution.Plugins.Directory.re4jjs91d1.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
  118. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.89etwfquz1.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
  119. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.aqf4vztjji.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
  120. package/wwwroot/_framework/{MindExecution.Shared.9b1rucdxvv.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
  121. package/wwwroot/_framework/{MindExecution.Web.ssp0n0xf1e.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
  122. package/wwwroot/_framework/blazor.boot.json +23 -23
  123. package/wwwroot/_headers +40 -40
  124. package/wwwroot/_redirects +1 -1
  125. package/wwwroot/index.html +772 -772
  126. package/wwwroot/js/marketing-tool.js +180 -180
  127. package/wwwroot/manifest.webmanifest +23 -23
  128. package/wwwroot/service-worker-assets.js +78 -106
  129. package/wwwroot/service-worker.js +33 -33
  130. package/tree-sitter-grammars/README.md +0 -18
  131. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  132. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  133. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  134. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  135. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  136. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  137. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  138. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  139. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  140. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  141. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  142. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
  143. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
  144. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
  145. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
package/codex-runtime.js CHANGED
@@ -1,1608 +1,1608 @@
1
- import { promises as fs, readFileSync } from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import { exec, spawn } from 'child_process';
5
- import { promisify } from 'util';
6
- import crypto from 'crypto';
7
-
8
- const execAsync = promisify(exec);
9
-
10
- const PROVIDER_KIND = Object.freeze({
11
- typeScriptSdk: 'TypeScriptSdk',
12
- appServerJsonRpc: 'AppServerJsonRpc',
13
- legacyExec: 'LegacyExec',
14
- unavailable: 'Unavailable'
15
- });
16
-
17
- const SDK_SANDBOX_BY_API_VALUE = Object.freeze({
18
- read_only: 'read-only',
19
- 'read-only': 'read-only',
20
- readonly: 'read-only',
21
- workspace_write: 'workspace-write',
22
- 'workspace-write': 'workspace-write',
23
- workspacewrite: 'workspace-write',
24
- full_access: 'danger-full-access',
25
- fullaccess: 'danger-full-access',
26
- danger_full_access: 'danger-full-access',
27
- 'danger-full-access': 'danger-full-access'
28
- });
29
-
30
- const API_SANDBOX_BY_SDK_VALUE = Object.freeze({
31
- 'read-only': 'read_only',
32
- 'workspace-write': 'workspace_write',
33
- 'danger-full-access': 'full_access'
34
- });
35
-
36
- const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
37
- const CODEX_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
38
- const MAX_LOG_CHARS = 4000;
39
- const MAX_EVENT_LOG = 120;
40
- const MAX_IMAGE_REFERENCE_COUNT = 8;
41
- const MAX_IMAGE_REFERENCE_BYTES = 40 * 1024 * 1024;
42
- const MAX_GENERATED_IMAGE_BYTES = 50 * 1024 * 1024;
43
- const TEMP_DIR = '.ai/codex';
44
- const CODEX_RUNTIME_HOME_ENV = 'MINDEXEC_CODEX_HOME';
45
- const CODEX_RUNTIME_CONFIG_MARKER = '# Generated by MindExec LocalBridge for isolated AI node runs.';
46
- const CODEX_TARGET_BY_PLATFORM = Object.freeze({
47
- 'win32:x64': {
48
- packageName: '@openai/codex-win32-x64',
49
- targetTriple: 'x86_64-pc-windows-msvc',
50
- binaryName: 'codex.exe'
51
- },
52
- 'win32:arm64': {
53
- packageName: '@openai/codex-win32-arm64',
54
- targetTriple: 'aarch64-pc-windows-msvc',
55
- binaryName: 'codex.exe'
56
- },
57
- 'darwin:x64': {
58
- packageName: '@openai/codex-darwin-x64',
59
- targetTriple: 'x86_64-apple-darwin',
60
- binaryName: 'codex'
61
- },
62
- 'darwin:arm64': {
63
- packageName: '@openai/codex-darwin-arm64',
64
- targetTriple: 'aarch64-apple-darwin',
65
- binaryName: 'codex'
66
- },
67
- 'linux:x64': {
68
- packageName: '@openai/codex-linux-x64',
69
- targetTriple: 'x86_64-unknown-linux-musl',
70
- binaryName: 'codex'
71
- },
72
- 'linux:arm64': {
73
- packageName: '@openai/codex-linux-arm64',
74
- targetTriple: 'aarch64-unknown-linux-musl',
75
- binaryName: 'codex'
76
- }
77
- });
78
-
79
- let cachedSdkModule = null;
80
- let cachedSdkLoadError = null;
81
- let cachedLegacyAvailability = null;
82
- let cachedLegacyAvailabilityExpiresAt = 0;
83
-
84
- function safeReadPackageVersion(packageRoot, packageName) {
85
- try {
86
- const packageJsonPath = path.join(packageRoot, 'node_modules', ...packageName.split('/'), 'package.json');
87
- const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
88
- return parsed.version || null;
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- async function loadCodexSdk() {
95
- if (cachedSdkModule) {
96
- return cachedSdkModule;
97
- }
98
-
99
- try {
100
- cachedSdkModule = await import('@openai/codex-sdk');
101
- cachedSdkLoadError = null;
102
- return cachedSdkModule;
103
- } catch (err) {
104
- cachedSdkLoadError = err?.message || String(err);
105
- return null;
106
- }
107
- }
108
-
109
- function normalizeProviderKind(value) {
110
- const normalized = String(value || '').trim().toLowerCase();
111
- if (!normalized || normalized === 'auto' || normalized === 'sdk') {
112
- return '';
113
- }
114
-
115
- if (normalized === 'typescriptsdk' || normalized === 'typescript-sdk' || normalized === 'type_script_sdk') {
116
- return PROVIDER_KIND.typeScriptSdk;
117
- }
118
-
119
- if (normalized === 'legacyexec' || normalized === 'legacy-exec' || normalized === 'exec') {
120
- return PROVIDER_KIND.legacyExec;
121
- }
122
-
123
- if (normalized === 'appserverjsonrpc' || normalized === 'app-server-json-rpc') {
124
- return PROVIDER_KIND.appServerJsonRpc;
125
- }
126
-
127
- return '';
128
- }
129
-
130
- function resolveUserCodexHome() {
131
- const configured = String(process.env.CODEX_HOME || '').trim();
132
- return path.resolve(configured || path.join(os.homedir(), '.codex'));
133
- }
134
-
135
- export function resolveCodexRuntimeHome() {
136
- const configured = String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim();
137
- return path.resolve(configured || resolveUserCodexHome());
138
- }
139
-
140
- function usesExplicitIsolatedCodexHome() {
141
- return String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim().length > 0;
142
- }
143
-
144
- function buildCodexSdkConfigOverrides(additional = {}) {
145
- return {
146
- mcp_servers: {},
147
- ...additional
148
- };
149
- }
150
-
151
- export async function ensureCodexRuntimeHome() {
152
- const runtimeHome = resolveCodexRuntimeHome();
153
- await fs.mkdir(runtimeHome, { recursive: true });
154
- await fs.mkdir(path.join(runtimeHome, 'sessions'), { recursive: true });
155
- await fs.mkdir(path.join(runtimeHome, 'generated_images'), { recursive: true });
156
-
157
- if (!usesExplicitIsolatedCodexHome()) {
158
- return runtimeHome;
159
- }
160
-
161
- const configPath = path.join(runtimeHome, 'config.toml');
162
- let shouldWriteConfig = true;
163
- try {
164
- const existing = await fs.readFile(configPath, 'utf8');
165
- shouldWriteConfig = existing.trim().length === 0 || existing.includes(CODEX_RUNTIME_CONFIG_MARKER);
166
- } catch {
167
- shouldWriteConfig = true;
168
- }
169
-
170
- if (shouldWriteConfig) {
171
- await fs.writeFile(
172
- configPath,
173
- `${CODEX_RUNTIME_CONFIG_MARKER}\n# User MCP server definitions are intentionally not inherited here.\n# LocalBridge passes model, sandbox, and reasoning options per run.\n`,
174
- 'utf8'
175
- );
176
- }
177
-
178
- return runtimeHome;
179
- }
180
-
181
- export function buildCodexChildEnv() {
182
- const env = {};
183
- for (const [key, value] of Object.entries(process.env)) {
184
- if (value !== undefined) {
185
- env[key] = value;
186
- }
187
- }
188
-
189
- env.CODEX_HOME = resolveCodexRuntimeHome();
190
- if (usesExplicitIsolatedCodexHome()) {
191
- env.MINDEXEC_CODEX_ISOLATED_HOME = '1';
192
- } else {
193
- delete env.MINDEXEC_CODEX_ISOLATED_HOME;
194
- }
195
- return env;
196
- }
197
-
198
- function resolveBundledCodexExecutable(packageRoot) {
199
- const target = CODEX_TARGET_BY_PLATFORM[`${process.platform}:${process.arch}`];
200
- if (!target) {
201
- throw new Error(`Unsupported Codex login platform: ${process.platform} (${process.arch}).`);
202
- }
203
-
204
- return path.join(
205
- packageRoot,
206
- 'node_modules',
207
- ...target.packageName.split('/'),
208
- 'vendor',
209
- target.targetTriple,
210
- 'bin',
211
- target.binaryName);
212
- }
213
-
214
- function runBundledCodexCommand(packageRoot, args, timeoutMs = CODEX_LOGIN_TIMEOUT_MS) {
215
- return new Promise((resolve) => {
216
- let executablePath;
217
- try {
218
- executablePath = resolveBundledCodexExecutable(packageRoot);
219
- } catch (err) {
220
- resolve({ success: false, exitCode: -1, stdout: '', stderr: '', error: err?.message || String(err) });
221
- return;
222
- }
223
-
224
- const child = spawn(executablePath, args, {
225
- env: buildCodexChildEnv(),
226
- windowsHide: true,
227
- stdio: ['ignore', 'pipe', 'pipe']
228
- });
229
- let stdout = '';
230
- let stderr = '';
231
- let settled = false;
232
- let timeout = null;
233
-
234
- const finish = (result) => {
235
- if (settled) {
236
- return;
237
- }
238
- settled = true;
239
- if (timeout) {
240
- clearTimeout(timeout);
241
- }
242
- resolve({
243
- success: result.exitCode === 0,
244
- exitCode: result.exitCode,
245
- stdout: truncateLog(stdout),
246
- stderr: truncateLog(stderr),
247
- error: result.error || ''
248
- });
249
- };
250
-
251
- child.stdout?.on('data', chunk => {
252
- stdout = truncateLog(`${stdout}${chunk}`, MAX_LOG_CHARS);
253
- });
254
- child.stderr?.on('data', chunk => {
255
- stderr = truncateLog(`${stderr}${chunk}`, MAX_LOG_CHARS);
256
- });
257
- child.once('error', err => finish({ exitCode: -1, error: err?.message || String(err) }));
258
- child.once('exit', code => finish({ exitCode: Number.isFinite(Number(code)) ? Number(code) : -1 }));
259
-
260
- timeout = setTimeout(() => {
261
- try {
262
- child.kill();
263
- } catch {
264
- // Ignore a process that already exited while the timeout fired.
265
- }
266
- finish({ exitCode: -1, error: 'Codex browser login timed out.' });
267
- }, timeoutMs);
268
- });
269
- }
270
-
271
- function normalizeReasoningEffort(value) {
272
- const normalized = String(value || '').trim().toLowerCase();
273
- return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(normalized)
274
- ? normalized
275
- : 'medium';
276
- }
277
-
278
- function inferSandboxFromIntent(intent, prompt) {
279
- const text = `${intent || ''}\n${prompt || ''}`.toLowerCase();
280
- if (/(full[_ -]?access|unrestricted|danger[_ -]?full)/.test(text)) {
281
- return 'workspace-write';
282
- }
283
-
284
- if (/(code[-_\s]?edit|project[-_\s]?fix|generate[-_\s]?files|implement|modify files|write files|patch|apply fix|refactor)/.test(text)) {
285
- return 'workspace-write';
286
- }
287
-
288
- if (/(research|review|analy[sz]e|summari[sz]e|diagnose|inspect|audit|compare|read[-_\s]?only)/.test(text)) {
289
- return 'read-only';
290
- }
291
-
292
- return 'workspace-write';
293
- }
294
-
295
- function normalizeSandbox(value, intent, prompt) {
296
- const normalized = String(value || '').trim().toLowerCase();
297
- if (normalized && SDK_SANDBOX_BY_API_VALUE[normalized]) {
298
- return SDK_SANDBOX_BY_API_VALUE[normalized];
299
- }
300
-
301
- return inferSandboxFromIntent(intent, prompt);
302
- }
303
-
304
- function normalizeApprovalPolicy(value, sandboxMode) {
305
- const normalized = String(value || '').trim().toLowerCase();
306
- if (['on-request', 'on-failure', 'untrusted'].includes(normalized)) {
307
- return normalized;
308
- }
309
-
310
- if (normalized === 'never' && sandboxMode !== 'danger-full-access') {
311
- return 'on-request';
312
- }
313
-
314
- return 'on-request';
315
- }
316
-
317
- function normalizeOutputSchema(value) {
318
- if (value == null) {
319
- return undefined;
320
- }
321
-
322
- if (typeof value === 'object') {
323
- return value;
324
- }
325
-
326
- const text = String(value || '').trim();
327
- if (!text) {
328
- return undefined;
329
- }
330
-
331
- return JSON.parse(text);
332
- }
333
-
334
- function truncateLog(value, maxChars = MAX_LOG_CHARS) {
335
- const text = String(value ?? '');
336
- if (text.length <= maxChars) {
337
- return text;
338
- }
339
-
340
- return `${text.slice(0, maxChars)}...`;
341
- }
342
-
343
- function makeEmptyResult(overrides = {}) {
344
- const now = new Date().toISOString();
345
- return {
346
- success: false,
347
- providerKind: PROVIDER_KIND.unavailable,
348
- threadId: '',
349
- turnId: '',
350
- finalResponse: '',
351
- summary: '',
352
- error: '',
353
- status: 'failed',
354
- startedAtUtc: now,
355
- completedAtUtc: now,
356
- sandboxMode: '',
357
- changedFiles: [],
358
- commandLogs: [],
359
- approvalEvents: [],
360
- toolEvents: [],
361
- rawEventCount: 0,
362
- ...overrides
363
- };
364
- }
365
-
366
- function upsertToolEvent(result, event) {
367
- const id = String(event?.id || '').trim();
368
- const type = String(event?.type || '').trim();
369
- const name = String(event?.name || '').trim();
370
- const key = id || `${type}:${name}`;
371
- const index = result.toolEvents.findIndex(existing =>
372
- (String(existing?.id || '').trim() || `${existing?.type || ''}:${existing?.name || ''}`) === key);
373
- if (index >= 0) {
374
- result.toolEvents[index] = event;
375
- return;
376
- }
377
-
378
- result.toolEvents.push(event);
379
- }
380
-
381
- function collectItem(result, item, eventType = '') {
382
- if (!item || typeof item !== 'object') {
383
- return;
384
- }
385
-
386
- if (item.type === 'agent_message' && typeof item.text === 'string') {
387
- result.finalResponse = item.text;
388
- return;
389
- }
390
-
391
- if (item.type === 'reasoning' && typeof item.text === 'string') {
392
- result.summary = result.summary
393
- ? `${result.summary}\n${item.text}`.trim()
394
- : item.text.trim();
395
- return;
396
- }
397
-
398
- if (item.type === 'file_change' && Array.isArray(item.changes)) {
399
- for (const change of item.changes) {
400
- const changePath = String(change?.path || '').trim();
401
- if (!changePath) {
402
- continue;
403
- }
404
-
405
- if (!result.changedFiles.some(existing => existing.path === changePath)) {
406
- result.changedFiles.push({
407
- path: changePath,
408
- kind: String(change?.kind || '').trim(),
409
- status: String(item.status || '').trim()
410
- });
411
- }
412
- }
413
- return;
414
- }
415
-
416
- if (item.type === 'command_execution') {
417
- result.commandLogs.push({
418
- id: String(item.id || ''),
419
- command: String(item.command || ''),
420
- status: String(item.status || ''),
421
- exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
422
- output: truncateLog(item.aggregated_output)
423
- });
424
- upsertToolEvent(result, {
425
- id: String(item.id || ''),
426
- type: 'command_execution',
427
- name: String(item.command || '').trim() || 'Shell',
428
- status: String(item.status || eventType || ''),
429
- summary: truncateLog(item.aggregated_output, 1200)
430
- });
431
- return;
432
- }
433
-
434
- if (item.type === 'web_search') {
435
- upsertToolEvent(result, {
436
- id: String(item.id || ''),
437
- type: 'web_search',
438
- name: 'WebSearch',
439
- status: eventType === 'item.completed' ? 'completed' : 'running',
440
- summary: truncateLog(item.query, 1200)
441
- });
442
- return;
443
- }
444
-
445
- if (item.type === 'mcp_tool_call') {
446
- const server = String(item.server || '').trim();
447
- const tool = String(item.tool || '').trim();
448
- upsertToolEvent(result, {
449
- id: String(item.id || ''),
450
- type: 'mcp_tool_call',
451
- name: [server, tool].filter(Boolean).join('.') || 'MCP',
452
- status: String(item.status || eventType || ''),
453
- summary: item.error?.message
454
- ? truncateLog(item.error.message, 1200)
455
- : truncateLog(JSON.stringify(item.arguments ?? {}), 1200)
456
- });
457
- return;
458
- }
459
-
460
- if (item.type === 'todo_list') {
461
- const items = Array.isArray(item.items) ? item.items : [];
462
- upsertToolEvent(result, {
463
- id: String(item.id || ''),
464
- type: 'todo_list',
465
- name: 'Plan',
466
- status: eventType === 'item.completed' ? 'completed' : 'running',
467
- summary: truncateLog(items.map(entry => `${entry?.completed ? '[x]' : '[ ]'} ${entry?.text || ''}`).join('\n'), 1200)
468
- });
469
- return;
470
- }
471
-
472
- if (String(item.type || '').toLowerCase().includes('approval')) {
473
- result.approvalEvents.push({
474
- id: String(item.id || ''),
475
- type: String(item.type || ''),
476
- status: String(item.status || ''),
477
- summary: truncateLog(JSON.stringify(item), 1200)
478
- });
479
- }
480
- }
481
-
482
- function extractLegacyEvents(stdout, stderr) {
483
- const result = {
484
- finalResponse: '',
485
- summary: '',
486
- error: '',
487
- threadId: '',
488
- changedFiles: [],
489
- commandLogs: [],
490
- approvalEvents: [],
491
- rawEventCount: 0
492
- };
493
-
494
- const lines = String(stdout || '')
495
- .replace(/\r\n/g, '\n')
496
- .replace(/\r/g, '\n')
497
- .split('\n')
498
- .filter(line => line.trim().length > 0);
499
-
500
- for (const line of lines) {
501
- let event;
502
- try {
503
- event = JSON.parse(line);
504
- } catch {
505
- continue;
506
- }
507
-
508
- result.rawEventCount += 1;
509
- if (event.type === 'thread.started' && event.thread_id) {
510
- result.threadId = String(event.thread_id);
511
- }
512
-
513
- if (event.type === 'turn.failed' && event.error?.message) {
514
- result.error = String(event.error.message);
515
- }
516
-
517
- if (event.type === 'error' && event.message) {
518
- result.error = String(event.message);
519
- }
520
-
521
- const item = event.item;
522
- if (item?.type === 'agent_message' && typeof item.text === 'string') {
523
- result.finalResponse = item.text;
524
- } else if (item?.type === 'reasoning' && typeof item.text === 'string') {
525
- result.summary = result.summary
526
- ? `${result.summary}\n${item.text}`.trim()
527
- : item.text.trim();
528
- } else if (item?.type === 'file_change' && Array.isArray(item.changes)) {
529
- for (const change of item.changes) {
530
- const changePath = String(change?.path || '').trim();
531
- if (changePath) {
532
- result.changedFiles.push({
533
- path: changePath,
534
- kind: String(change?.kind || ''),
535
- status: String(item.status || '')
536
- });
537
- }
538
- }
539
- } else if (item?.type === 'command_execution') {
540
- result.commandLogs.push({
541
- id: String(item.id || ''),
542
- command: String(item.command || ''),
543
- status: String(item.status || ''),
544
- exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
545
- output: truncateLog(item.aggregated_output)
546
- });
547
- }
548
- }
549
-
550
- if (!result.finalResponse && lines.length > 0) {
551
- result.finalResponse = String(stdout || '').trim();
552
- }
553
-
554
- if (!result.error && stderr) {
555
- result.error = String(stderr).trim();
556
- }
557
-
558
- return result;
559
- }
560
-
561
- function buildThreadOptions(body, workingDirectory) {
562
- const prompt = String(body?.instruction || body?.prompt || '').trim();
563
- const sandboxMode = normalizeSandbox(body?.sandbox || body?.sandboxMode, body?.intent || body?.nodeKind, prompt);
564
- const approvalPolicy = normalizeApprovalPolicy(body?.approvalPolicy, sandboxMode);
565
- const options = {
566
- workingDirectory,
567
- skipGitRepoCheck: true,
568
- sandboxMode,
569
- modelReasoningEffort: normalizeReasoningEffort(body?.reasoningEffort),
570
- approvalPolicy
571
- };
572
-
573
- const model = String(body?.model || '').trim();
574
- if (model) {
575
- options.model = model;
576
- }
577
-
578
- if (body?.networkAccessEnabled !== undefined) {
579
- options.networkAccessEnabled = Boolean(body.networkAccessEnabled);
580
- }
581
-
582
- const webSearchMode = String(body?.webSearchMode || '').trim().toLowerCase();
583
- if (['disabled', 'cached', 'live'].includes(webSearchMode)) {
584
- options.webSearchMode = webSearchMode;
585
- }
586
-
587
- return options;
588
- }
589
-
590
- function serializeThreadOptionsForStatus(options) {
591
- return {
592
- model: options?.model || null,
593
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[options?.sandboxMode] || options?.sandboxMode || null,
594
- reasoningEffort: options?.modelReasoningEffort || null,
595
- approvalPolicy: options?.approvalPolicy || null,
596
- networkAccessEnabled: options?.networkAccessEnabled ?? null,
597
- webSearchMode: options?.webSearchMode || null,
598
- workingDirectory: options?.workingDirectory || null
599
- };
600
- }
601
-
602
- async function checkSdkAvailability(packageRoot, sdkLoader = loadCodexSdk) {
603
- const sdk = await sdkLoader();
604
- return {
605
- kind: PROVIDER_KIND.typeScriptSdk,
606
- available: Boolean(sdk?.Codex),
607
- version: safeReadPackageVersion(packageRoot, '@openai/codex-sdk'),
608
- error: sdk?.Codex ? null : (cachedSdkLoadError || 'Codex SDK module did not export Codex.')
609
- };
610
- }
611
-
612
- async function checkLegacyAvailability() {
613
- const now = Date.now();
614
- if (cachedLegacyAvailability && cachedLegacyAvailabilityExpiresAt > now) {
615
- return cachedLegacyAvailability;
616
- }
617
-
618
- try {
619
- const { stdout, stderr } = await execAsync('codex --version', {
620
- timeout: 6000,
621
- windowsHide: true,
622
- maxBuffer: 512 * 1024
623
- });
624
- cachedLegacyAvailability = {
625
- kind: PROVIDER_KIND.legacyExec,
626
- available: true,
627
- version: String(stdout || stderr || '').trim(),
628
- error: null
629
- };
630
- } catch (err) {
631
- cachedLegacyAvailability = {
632
- kind: PROVIDER_KIND.legacyExec,
633
- available: false,
634
- version: null,
635
- error: err?.message || String(err)
636
- };
637
- }
638
-
639
- cachedLegacyAvailabilityExpiresAt = now + 30 * 1000;
640
- return cachedLegacyAvailability;
641
- }
642
-
643
- function extractPrompt(body) {
644
- const prompt = String(body?.instruction || body?.prompt || '').trim();
645
- if (!prompt) {
646
- const err = new Error('Codex prompt is required.');
647
- err.statusCode = 400;
648
- throw err;
649
- }
650
-
651
- return prompt;
652
- }
653
-
654
- function looksLikeCodexAuthenticationError(value) {
655
- const text = String(value || '').toLowerCase();
656
- return text.includes('invalid_refresh_token')
657
- || text.includes('invalid refresh token')
658
- || text.includes('login required')
659
- || text.includes('not logged in')
660
- || text.includes('please log out and sign in again')
661
- || text.includes('could not parse your authentication token')
662
- || text.includes('authrequired')
663
- || text.includes('401 unauthorized');
664
- }
665
-
666
- function normalizeImageMimeType(value, fallback = 'image/png') {
667
- const normalized = String(value || '').trim().toLowerCase();
668
- return /^image\/[a-z0-9.+-]+$/.test(normalized) ? normalized : fallback;
669
- }
670
-
671
- function inferImageMimeType(buffer, fallback = 'image/png') {
672
- if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
673
- return fallback;
674
- }
675
-
676
- if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
677
- return 'image/png';
678
- }
679
- if (buffer[0] === 0xff && buffer[1] === 0xd8) {
680
- return 'image/jpeg';
681
- }
682
- if (buffer.slice(0, 3).toString('ascii') === 'GIF') {
683
- return 'image/gif';
684
- }
685
- if (buffer.length >= 12
686
- && buffer.slice(0, 4).toString('ascii') === 'RIFF'
687
- && buffer.slice(8, 12).toString('ascii') === 'WEBP') {
688
- return 'image/webp';
689
- }
690
-
691
- return fallback;
692
- }
693
-
694
- function normalizeGeneratedImageBase64(value, hintedMimeType = '') {
695
- const text = String(value || '').trim();
696
- if (!text) {
697
- return null;
698
- }
699
-
700
- const dataUrlMatch = text.match(/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\r\n]+)$/i);
701
- const base64 = (dataUrlMatch ? dataUrlMatch[2] : text).replace(/\s+/g, '');
702
- if (base64.length < 32 || !/^[a-z0-9+/]+={0,2}$/i.test(base64)) {
703
- return null;
704
- }
705
-
706
- try {
707
- const bytes = Buffer.from(base64, 'base64');
708
- if (!bytes.length || bytes.length > MAX_GENERATED_IMAGE_BYTES) {
709
- return null;
710
- }
711
- const mimeType = inferImageMimeType(
712
- bytes,
713
- normalizeImageMimeType(dataUrlMatch?.[1] || hintedMimeType));
714
- return { imageBase64: bytes.toString('base64'), mimeType };
715
- } catch {
716
- return null;
717
- }
718
- }
719
-
720
- function extractGeneratedImageFromEvent(value, depth = 0, hintedMimeType = '') {
721
- if (value == null || depth > 10) {
722
- return null;
723
- }
724
-
725
- if (typeof value === 'string') {
726
- return value.startsWith('data:image/')
727
- ? normalizeGeneratedImageBase64(value, hintedMimeType)
728
- : null;
729
- }
730
-
731
- if (Array.isArray(value)) {
732
- for (const entry of value) {
733
- const found = extractGeneratedImageFromEvent(entry, depth + 1, hintedMimeType);
734
- if (found) {
735
- return found;
736
- }
737
- }
738
- return null;
739
- }
740
-
741
- if (typeof value !== 'object') {
742
- return null;
743
- }
744
-
745
- const localMimeType = normalizeImageMimeType(
746
- value.mime_type || value.mimeType || value.content_type || value.contentType,
747
- hintedMimeType || 'image/png');
748
- for (const key of ['b64_json', 'image_base64', 'imageBase64', 'base64']) {
749
- if (typeof value[key] === 'string') {
750
- const found = normalizeGeneratedImageBase64(value[key], localMimeType);
751
- if (found) {
752
- return found;
753
- }
754
- }
755
- }
756
-
757
- for (const key of ['result', 'output', 'image', 'images', 'data', 'payload', 'item']) {
758
- if (value[key] != null) {
759
- const found = extractGeneratedImageFromEvent(value[key], depth + 1, localMimeType);
760
- if (found) {
761
- return found;
762
- }
763
- }
764
- }
765
-
766
- return null;
767
- }
768
-
769
- function imageExtensionForReference(reference) {
770
- const fileNameExtension = path.extname(String(reference?.fileName || '')).toLowerCase();
771
- if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(fileNameExtension)) {
772
- return fileNameExtension === '.jpeg' ? '.jpg' : fileNameExtension;
773
- }
774
-
775
- const mimeType = normalizeImageMimeType(reference?.mimeType);
776
- if (mimeType === 'image/jpeg') return '.jpg';
777
- if (mimeType === 'image/webp') return '.webp';
778
- if (mimeType === 'image/gif') return '.gif';
779
- return '.png';
780
- }
781
-
782
- async function writeSdkImageReferences(runtimeHome, references) {
783
- const normalized = Array.isArray(references)
784
- ? references.slice(0, MAX_IMAGE_REFERENCE_COUNT)
785
- : [];
786
- if (!normalized.length) {
787
- return { directory: '', paths: [] };
788
- }
789
-
790
- const root = path.join(runtimeHome, 'image-inputs');
791
- const directory = path.join(root, crypto.randomUUID());
792
- await fs.mkdir(directory, { recursive: true });
793
- const paths = [];
794
- let totalBytes = 0;
795
-
796
- try {
797
- for (let index = 0; index < normalized.length; index += 1) {
798
- const reference = normalized[index];
799
- const bytes = Buffer.from(String(reference?.base64 || ''), 'base64');
800
- totalBytes += bytes.length;
801
- if (!bytes.length || totalBytes > MAX_IMAGE_REFERENCE_BYTES) {
802
- throw new Error('Codex SDK image references are empty or exceed the 40 MB request limit.');
803
- }
804
-
805
- const filePath = path.join(directory, `reference-${index + 1}${imageExtensionForReference(reference)}`);
806
- await fs.writeFile(filePath, bytes);
807
- paths.push(filePath);
808
- }
809
-
810
- return { directory, paths };
811
- } catch (err) {
812
- await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
813
- throw err;
814
- }
815
- }
816
-
817
- async function cleanupSdkImageReferences(runtimeHome, directory) {
818
- if (!directory) {
819
- return;
820
- }
821
-
822
- const root = path.resolve(runtimeHome, 'image-inputs');
823
- const target = path.resolve(directory);
824
- if (!target.startsWith(`${root}${path.sep}`)) {
825
- return;
826
- }
827
- await fs.rm(target, { recursive: true, force: true }).catch(() => {});
828
- }
829
-
830
- function buildSdkImagePrompt(body, referenceCount) {
831
- const prompt = extractPrompt(body);
832
- const action = String(body?.action || '').trim().toLowerCase() === 'edit' || referenceCount > 0
833
- ? 'edit'
834
- : 'generate';
835
- const size = String(body?.size || '').trim();
836
- const quality = String(body?.quality || '').trim();
837
- return [
838
- action === 'edit'
839
- ? `Use the image generation tool to edit the ${referenceCount} attached reference image(s).`
840
- : 'Use the image generation tool to create one new image.',
841
- prompt,
842
- size ? `Requested output size: ${size}.` : '',
843
- quality ? `Requested quality: ${quality}.` : '',
844
- 'Return exactly one generated image. Do not answer with instructions or prose.'
845
- ].filter(Boolean).join('\n\n');
846
- }
847
-
848
- async function listGeneratedImageCandidates(directory, minimumMtimeMs, depth = 0) {
849
- if (depth > 4) {
850
- return [];
851
- }
852
-
853
- const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
854
- const candidates = [];
855
- for (const entry of entries) {
856
- const fullPath = path.join(directory, entry.name);
857
- if (entry.isDirectory()) {
858
- candidates.push(...await listGeneratedImageCandidates(fullPath, minimumMtimeMs, depth + 1));
859
- continue;
860
- }
861
- if (!entry.isFile() || !/\.(png|jpe?g|webp|gif)$/i.test(entry.name)) {
862
- continue;
863
- }
864
-
865
- const stat = await fs.stat(fullPath).catch(() => null);
866
- if (stat && stat.size > 0 && stat.size <= MAX_GENERATED_IMAGE_BYTES && stat.mtimeMs >= minimumMtimeMs) {
867
- candidates.push({ fullPath, mtimeMs: stat.mtimeMs });
868
- }
869
- }
870
- return candidates;
871
- }
872
-
873
- async function recoverSdkGeneratedImage(runtimeHome, startedAtMs) {
874
- const generatedRoot = path.join(runtimeHome, 'generated_images');
875
- const minimumMtimeMs = startedAtMs - 15 * 1000;
876
- for (let attempt = 0; attempt < 8; attempt += 1) {
877
- const candidates = await listGeneratedImageCandidates(generatedRoot, minimumMtimeMs);
878
- candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
879
- if (candidates[0]) {
880
- const bytes = await fs.readFile(candidates[0].fullPath);
881
- return {
882
- imageBase64: bytes.toString('base64'),
883
- mimeType: inferImageMimeType(bytes)
884
- };
885
- }
886
- await new Promise(resolve => setTimeout(resolve, 350));
887
- }
888
- return null;
889
- }
890
-
891
- export function createCodexRuntime(options) {
892
- const packageRoot = options.packageRoot;
893
- const resolveWorkingDirectory = options.resolveWorkingDirectory;
894
- const getCurrentRuntime = options.getCurrentRuntime;
895
- const getModelCatalog = options.getModelCatalog;
896
- const log = typeof options.log === 'function' ? options.log : () => {};
897
- const sdkLoader = typeof options.loadCodexSdk === 'function' ? options.loadCodexSdk : loadCodexSdk;
898
- const prepareRuntimeHome = typeof options.ensureCodexRuntimeHome === 'function'
899
- ? options.ensureCodexRuntimeHome
900
- : ensureCodexRuntimeHome;
901
- const childEnvFactory = typeof options.buildCodexChildEnv === 'function'
902
- ? options.buildCodexChildEnv
903
- : buildCodexChildEnv;
904
-
905
- const threads = new Map();
906
- const activeTurns = new Map();
907
- let activeLoginPromise = null;
908
-
909
- async function loginWithBundledCodex() {
910
- if (typeof options.loginCodex === 'function') {
911
- return options.loginCodex();
912
- }
913
-
914
- if (activeLoginPromise) {
915
- return activeLoginPromise;
916
- }
917
-
918
- activeLoginPromise = (async () => {
919
- await prepareRuntimeHome();
920
- await runBundledCodexCommand(packageRoot, ['logout'], 15000).catch(() => null);
921
- log('Codex SDK browser login start', { runtimeHome: resolveCodexRuntimeHome() });
922
- const loginResult = await runBundledCodexCommand(packageRoot, ['login']);
923
- log('Codex SDK browser login finish', {
924
- success: loginResult.success,
925
- exitCode: loginResult.exitCode,
926
- error: loginResult.error || ''
927
- });
928
- return loginResult;
929
- })().finally(() => {
930
- activeLoginPromise = null;
931
- });
932
-
933
- return activeLoginPromise;
934
- }
935
-
936
- async function getCapabilities(options = {}) {
937
- const [sdk, legacy, modelCatalog] = await Promise.all([
938
- checkSdkAvailability(packageRoot, sdkLoader),
939
- checkLegacyAvailability(),
940
- getModelCatalog({ forceRefresh: Boolean(options.forceRefresh) })
941
- ]);
942
- const preferredProviderKind = sdk.available
943
- ? PROVIDER_KIND.typeScriptSdk
944
- : legacy.available
945
- ? PROVIDER_KIND.legacyExec
946
- : PROVIDER_KIND.unavailable;
947
-
948
- return {
949
- success: preferredProviderKind !== PROVIDER_KIND.unavailable,
950
- preferredProviderKind,
951
- providers: [
952
- sdk,
953
- {
954
- kind: PROVIDER_KIND.appServerJsonRpc,
955
- available: false,
956
- implemented: false,
957
- error: 'DTO scaffold only. Use TypeScriptSdk for LocalBridge execution.'
958
- },
959
- legacy
960
- ],
961
- sandboxModes: ['read_only', 'workspace_write', 'full_access'],
962
- endpoints: [
963
- 'GET /api/codex/capabilities',
964
- 'POST /api/codex/thread/start',
965
- 'POST /api/codex/thread/run',
966
- 'POST /api/codex/thread/resume',
967
- 'POST /api/codex/thread/cancel',
968
- 'POST /api/codex/image/run',
969
- 'GET /api/codex/thread/{threadId}/status'
970
- ],
971
- runtime: getCurrentRuntime(),
972
- models: modelCatalog.models || [],
973
- modelCatalogSource: modelCatalog.source,
974
- modelCatalogCommandSource: modelCatalog.commandSource || null,
975
- modelCatalogUpdatedAt: modelCatalog.updatedAt,
976
- modelCatalogRefreshAttemptedAt: modelCatalog.refreshAttemptedAt || null,
977
- modelCatalogStale: modelCatalog.stale === true,
978
- modelCatalogError: modelCatalog.error || null
979
- };
980
- }
981
-
982
- async function resolveProvider(requestedProviderKind) {
983
- const requested = normalizeProviderKind(requestedProviderKind);
984
- const sdk = await checkSdkAvailability(packageRoot, sdkLoader);
985
- const legacy = requested === PROVIDER_KIND.typeScriptSdk
986
- ? null
987
- : await checkLegacyAvailability();
988
-
989
- if ((!requested || requested === PROVIDER_KIND.typeScriptSdk) && sdk.available) {
990
- return PROVIDER_KIND.typeScriptSdk;
991
- }
992
-
993
- if ((!requested || requested === PROVIDER_KIND.legacyExec) && legacy?.available) {
994
- return PROVIDER_KIND.legacyExec;
995
- }
996
-
997
- return PROVIDER_KIND.unavailable;
998
- }
999
-
1000
- async function startThread(body = {}) {
1001
- const providerKind = await resolveProvider(body.providerKind || body.runtime);
1002
- const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1003
- const threadOptions = buildThreadOptions(body, workingDirectory);
1004
- const now = new Date().toISOString();
1005
-
1006
- if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1007
- const sdk = await sdkLoader();
1008
- await prepareRuntimeHome();
1009
- const codex = new sdk.Codex({
1010
- env: childEnvFactory(),
1011
- config: buildCodexSdkConfigOverrides()
1012
- });
1013
- const thread = codex.startThread(threadOptions);
1014
- const localThreadId = `local_${crypto.randomUUID()}`;
1015
- threads.set(localThreadId, {
1016
- providerKind,
1017
- thread,
1018
- createdAtUtc: now,
1019
- updatedAtUtc: now,
1020
- options: threadOptions,
1021
- officialThreadId: null,
1022
- lastStatus: 'created'
1023
- });
1024
-
1025
- return makeEmptyResult({
1026
- success: true,
1027
- providerKind,
1028
- threadId: localThreadId,
1029
- status: 'created',
1030
- startedAtUtc: now,
1031
- completedAtUtc: now,
1032
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1033
- });
1034
- }
1035
-
1036
- if (providerKind === PROVIDER_KIND.legacyExec) {
1037
- const localThreadId = `legacy_${crypto.randomUUID()}`;
1038
- threads.set(localThreadId, {
1039
- providerKind,
1040
- createdAtUtc: now,
1041
- updatedAtUtc: now,
1042
- options: threadOptions,
1043
- officialThreadId: localThreadId,
1044
- lastStatus: 'created'
1045
- });
1046
-
1047
- return makeEmptyResult({
1048
- success: true,
1049
- providerKind,
1050
- threadId: localThreadId,
1051
- status: 'created',
1052
- startedAtUtc: now,
1053
- completedAtUtc: now,
1054
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1055
- });
1056
- }
1057
-
1058
- return makeEmptyResult({
1059
- providerKind,
1060
- error: 'Codex local runtime is not available. Install LocalBridge dependencies or Codex CLI, then run codex login.'
1061
- });
1062
- }
1063
-
1064
- async function getSdkThread(body, threadOptions) {
1065
- const requestedThreadId = String(body?.threadId || '').trim();
1066
- if (requestedThreadId && threads.has(requestedThreadId)) {
1067
- return {
1068
- key: requestedThreadId,
1069
- record: threads.get(requestedThreadId)
1070
- };
1071
- }
1072
-
1073
- const sdk = await sdkLoader();
1074
- await prepareRuntimeHome();
1075
- const codex = new sdk.Codex({
1076
- env: childEnvFactory(),
1077
- config: buildCodexSdkConfigOverrides()
1078
- });
1079
- const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
1080
- ? requestedThreadId
1081
- : '';
1082
- const thread = officialId
1083
- ? codex.resumeThread(officialId, threadOptions)
1084
- : codex.startThread(threadOptions);
1085
- const key = officialId || `local_${crypto.randomUUID()}`;
1086
- const record = {
1087
- providerKind: PROVIDER_KIND.typeScriptSdk,
1088
- thread,
1089
- createdAtUtc: new Date().toISOString(),
1090
- updatedAtUtc: new Date().toISOString(),
1091
- options: threadOptions,
1092
- officialThreadId: officialId || null,
1093
- lastStatus: 'created'
1094
- };
1095
- threads.set(key, record);
1096
- return { key, record };
1097
- }
1098
-
1099
- async function runSdkThread(body, workingDirectory, prompt, startedAtUtc) {
1100
- const threadOptions = buildThreadOptions(body, workingDirectory);
1101
- const { key, record } = await getSdkThread(body, threadOptions);
1102
- const turnId = `turn_${crypto.randomUUID()}`;
1103
- const abortController = new AbortController();
1104
- const activeKey = `${key}:${turnId}`;
1105
- activeTurns.set(activeKey, {
1106
- providerKind: PROVIDER_KIND.typeScriptSdk,
1107
- threadId: key,
1108
- turnId,
1109
- abortController,
1110
- startedAtUtc
1111
- });
1112
-
1113
- const result = makeEmptyResult({
1114
- providerKind: PROVIDER_KIND.typeScriptSdk,
1115
- threadId: record.officialThreadId || key,
1116
- turnId,
1117
- status: 'running',
1118
- startedAtUtc,
1119
- completedAtUtc: '',
1120
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1121
- });
1122
- let timeout = null;
1123
-
1124
- try {
1125
- timeout = setTimeout(() => {
1126
- try {
1127
- abortController.abort();
1128
- } catch {
1129
- // Ignore duplicate aborts.
1130
- }
1131
- }, DEFAULT_TIMEOUT_MS);
1132
-
1133
- log('Codex SDK run start', {
1134
- threadId: result.threadId,
1135
- turnId,
1136
- sandbox: result.sandboxMode,
1137
- cwd: workingDirectory
1138
- });
1139
-
1140
- const outputSchema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
1141
- const { events } = await record.thread.runStreamed(prompt, {
1142
- outputSchema,
1143
- signal: abortController.signal
1144
- });
1145
-
1146
- for await (const event of events) {
1147
- result.rawEventCount += 1;
1148
- if (result.rawEventCount <= MAX_EVENT_LOG) {
1149
- record.lastEvent = event;
1150
- }
1151
-
1152
- if (event.type === 'thread.started' && event.thread_id) {
1153
- record.officialThreadId = String(event.thread_id);
1154
- result.threadId = record.officialThreadId;
1155
- threads.set(record.officialThreadId, record);
1156
- } else if (event.type === 'item.completed' || event.type === 'item.updated' || event.type === 'item.started') {
1157
- collectItem(result, event.item, event.type);
1158
- } else if (event.type === 'turn.failed') {
1159
- result.error = event.error?.message || 'Codex turn failed.';
1160
- result.status = 'failed';
1161
- } else if (event.type === 'error') {
1162
- result.error = event.message || 'Codex stream failed.';
1163
- result.status = 'failed';
1164
- } else if (event.type === 'turn.completed') {
1165
- result.status = 'completed';
1166
- }
1167
- }
1168
-
1169
- if (!result.error && result.status !== 'failed') {
1170
- result.success = true;
1171
- result.status = result.status === 'running' ? 'completed' : result.status;
1172
- }
1173
-
1174
- if (!result.finalResponse && result.success) {
1175
- result.finalResponse = result.summary;
1176
- }
1177
-
1178
- if (!result.finalResponse && !result.error) {
1179
- result.error = 'Codex returned an empty response.';
1180
- result.status = 'failed';
1181
- result.success = false;
1182
- }
1183
- } catch (err) {
1184
- const aborted = abortController.signal.aborted;
1185
- result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
1186
- result.status = aborted ? 'cancelled' : 'failed';
1187
- result.success = false;
1188
- } finally {
1189
- if (timeout) {
1190
- clearTimeout(timeout);
1191
- }
1192
- result.completedAtUtc = new Date().toISOString();
1193
- record.updatedAtUtc = result.completedAtUtc;
1194
- record.lastStatus = result.status;
1195
- record.lastResult = result;
1196
- activeTurns.delete(activeKey);
1197
- }
1198
-
1199
- return result;
1200
- }
1201
-
1202
- async function runLegacyExec(body, workingDirectory, prompt, startedAtUtc) {
1203
- const threadOptions = buildThreadOptions(body, workingDirectory);
1204
- const turnId = `turn_${crypto.randomUUID()}`;
1205
- const threadId = String(body.threadId || `legacy_${crypto.randomUUID()}`);
1206
- const abortController = new AbortController();
1207
- const activeKey = `${threadId}:${turnId}`;
1208
- activeTurns.set(activeKey, {
1209
- providerKind: PROVIDER_KIND.legacyExec,
1210
- threadId,
1211
- turnId,
1212
- abortController,
1213
- startedAtUtc
1214
- });
1215
-
1216
- const tempDir = path.join(workingDirectory, TEMP_DIR);
1217
- const schema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
1218
- let schemaPath = null;
1219
- await prepareRuntimeHome();
1220
- const args = [
1221
- 'exec',
1222
- '-',
1223
- '--skip-git-repo-check',
1224
- '--json',
1225
- '--disable',
1226
- 'plugins',
1227
- '--disable',
1228
- 'shell_snapshot',
1229
- '-s',
1230
- threadOptions.sandboxMode,
1231
- '--config',
1232
- `model_reasoning_effort=${threadOptions.modelReasoningEffort}`,
1233
- '--config',
1234
- 'mcp_servers={}'
1235
- ];
1236
- if (threadOptions.model) {
1237
- args.push('-m', threadOptions.model);
1238
- }
1239
-
1240
- if (schema) {
1241
- await fs.mkdir(tempDir, { recursive: true });
1242
- schemaPath = path.join(tempDir, `${crypto.randomUUID()}.schema.json`);
1243
- await fs.writeFile(schemaPath, JSON.stringify(schema), 'utf8');
1244
- args.push('--output-schema', schemaPath);
1245
- }
1246
-
1247
- const result = makeEmptyResult({
1248
- providerKind: PROVIDER_KIND.legacyExec,
1249
- threadId,
1250
- turnId,
1251
- status: 'running',
1252
- startedAtUtc,
1253
- completedAtUtc: '',
1254
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1255
- });
1256
-
1257
- try {
1258
- const childResult = await new Promise((resolve) => {
1259
- const child = spawn('codex', args, {
1260
- cwd: workingDirectory,
1261
- env: childEnvFactory(),
1262
- windowsHide: true,
1263
- signal: abortController.signal
1264
- });
1265
- let stdout = '';
1266
- let stderr = '';
1267
- let settled = false;
1268
- const timeout = setTimeout(() => {
1269
- try {
1270
- abortController.abort();
1271
- } catch {
1272
- // Ignore duplicate aborts.
1273
- }
1274
- try {
1275
- child.kill('SIGTERM');
1276
- } catch {
1277
- // Ignore if already exited.
1278
- }
1279
- }, DEFAULT_TIMEOUT_MS);
1280
-
1281
- const settle = (payload) => {
1282
- if (settled) return;
1283
- settled = true;
1284
- clearTimeout(timeout);
1285
- resolve(payload);
1286
- };
1287
-
1288
- child.stdout?.on('data', chunk => {
1289
- stdout += chunk.toString();
1290
- });
1291
- child.stderr?.on('data', chunk => {
1292
- stderr += chunk.toString();
1293
- });
1294
- child.on('error', err => {
1295
- settle({ exitCode: -1, stdout, stderr, error: err?.message || String(err) });
1296
- });
1297
- child.on('close', code => {
1298
- settle({ exitCode: Number.isFinite(code) ? code : -1, stdout, stderr, error: '' });
1299
- });
1300
- child.stdin?.write(prompt);
1301
- child.stdin?.end();
1302
- });
1303
-
1304
- const parsed = extractLegacyEvents(childResult.stdout, childResult.stderr);
1305
- result.threadId = parsed.threadId || threadId;
1306
- result.finalResponse = parsed.finalResponse;
1307
- result.summary = parsed.summary;
1308
- result.changedFiles = parsed.changedFiles;
1309
- result.commandLogs = parsed.commandLogs;
1310
- result.approvalEvents = parsed.approvalEvents;
1311
- result.rawEventCount = parsed.rawEventCount;
1312
- result.error = childResult.error || parsed.error;
1313
- result.success = childResult.exitCode === 0 && Boolean(result.finalResponse);
1314
- result.status = result.success ? 'completed' : 'failed';
1315
- if (!result.error && !result.success) {
1316
- result.error = parsed.error || 'Codex legacy exec returned an empty response.';
1317
- }
1318
- } catch (err) {
1319
- const aborted = abortController.signal.aborted;
1320
- result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
1321
- result.status = aborted ? 'cancelled' : 'failed';
1322
- result.success = false;
1323
- } finally {
1324
- result.completedAtUtc = new Date().toISOString();
1325
- activeTurns.delete(activeKey);
1326
- if (schemaPath) {
1327
- await fs.unlink(schemaPath).catch(() => {});
1328
- }
1329
- threads.set(result.threadId, {
1330
- providerKind: PROVIDER_KIND.legacyExec,
1331
- createdAtUtc: startedAtUtc,
1332
- updatedAtUtc: result.completedAtUtc,
1333
- options: threadOptions,
1334
- officialThreadId: result.threadId,
1335
- lastStatus: result.status,
1336
- lastResult: result
1337
- });
1338
- }
1339
-
1340
- return result;
1341
- }
1342
-
1343
- async function runThread(body = {}) {
1344
- const prompt = extractPrompt(body);
1345
- const startedAtUtc = new Date().toISOString();
1346
- const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1347
- const providerKind = await resolveProvider(body.providerKind || body.runtime);
1348
-
1349
- if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1350
- return runSdkThread(body, workingDirectory, prompt, startedAtUtc);
1351
- }
1352
-
1353
- if (providerKind === PROVIDER_KIND.legacyExec) {
1354
- return runLegacyExec(body, workingDirectory, prompt, startedAtUtc);
1355
- }
1356
-
1357
- return makeEmptyResult({
1358
- providerKind,
1359
- startedAtUtc,
1360
- completedAtUtc: new Date().toISOString(),
1361
- error: 'Codex local runtime is not available. TypeScript SDK and legacy codex exec are unavailable.'
1362
- });
1363
- }
1364
-
1365
- async function runSdkImageOnce(body = {}) {
1366
- const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1367
- const runtimeHome = await prepareRuntimeHome();
1368
- const sdk = await sdkLoader();
1369
- if (!sdk?.Codex) {
1370
- return {
1371
- success: false,
1372
- providerKind: PROVIDER_KIND.unavailable,
1373
- imageBase64: '',
1374
- mimeType: '',
1375
- error: cachedSdkLoadError || 'Codex TypeScript SDK is unavailable.',
1376
- threadId: '',
1377
- authenticationRequired: false,
1378
- loginOpened: false
1379
- };
1380
- }
1381
-
1382
- const references = await writeSdkImageReferences(runtimeHome, body.referenceImages);
1383
- const prompt = buildSdkImagePrompt(body, references.paths.length);
1384
- const threadOptions = buildThreadOptions({
1385
- ...body,
1386
- sandbox: 'read_only',
1387
- approvalPolicy: 'on-request',
1388
- reasoningEffort: 'low',
1389
- networkAccessEnabled: true,
1390
- webSearchMode: 'disabled'
1391
- }, workingDirectory);
1392
- const startedAtMs = Date.now();
1393
- let threadId = '';
1394
- let error = '';
1395
- let generatedImage = null;
1396
-
1397
- try {
1398
- const codex = new sdk.Codex({
1399
- env: childEnvFactory(),
1400
- config: buildCodexSdkConfigOverrides({
1401
- features: { image_generation: true }
1402
- })
1403
- });
1404
- const thread = codex.startThread(threadOptions);
1405
- const input = [
1406
- { type: 'text', text: prompt },
1407
- ...references.paths.map(referencePath => ({ type: 'local_image', path: referencePath }))
1408
- ];
1409
- const { events } = await thread.runStreamed(input);
1410
-
1411
- for await (const event of events) {
1412
- if (event?.type === 'thread.started' && event.thread_id) {
1413
- threadId = String(event.thread_id);
1414
- } else if (event?.type === 'turn.failed') {
1415
- error = event.error?.message || 'Codex SDK image turn failed.';
1416
- } else if (event?.type === 'error') {
1417
- error = event.message || 'Codex SDK image stream failed.';
1418
- }
1419
-
1420
- generatedImage ||= extractGeneratedImageFromEvent(event);
1421
- }
1422
- } catch (err) {
1423
- error = err?.message || String(err);
1424
- } finally {
1425
- await cleanupSdkImageReferences(runtimeHome, references.directory);
1426
- }
1427
-
1428
- const authenticationRequired = looksLikeCodexAuthenticationError(error);
1429
- if (!authenticationRequired) {
1430
- generatedImage ||= await recoverSdkGeneratedImage(runtimeHome, startedAtMs);
1431
- }
1432
- if (generatedImage) {
1433
- return {
1434
- success: true,
1435
- providerKind: PROVIDER_KIND.typeScriptSdk,
1436
- imageBase64: generatedImage.imageBase64,
1437
- mimeType: generatedImage.mimeType,
1438
- error: '',
1439
- threadId,
1440
- authenticationRequired: false,
1441
- loginOpened: false
1442
- };
1443
- }
1444
-
1445
- return {
1446
- success: false,
1447
- providerKind: PROVIDER_KIND.typeScriptSdk,
1448
- imageBase64: '',
1449
- mimeType: '',
1450
- error: error || 'Codex SDK completed without returning an image.',
1451
- threadId,
1452
- authenticationRequired,
1453
- loginOpened: false
1454
- };
1455
- }
1456
-
1457
- async function runImage(body = {}) {
1458
- extractPrompt(body);
1459
- const firstResult = await runSdkImageOnce(body);
1460
- if (!firstResult.authenticationRequired) {
1461
- return firstResult;
1462
- }
1463
-
1464
- const loginResult = await loginWithBundledCodex();
1465
- const loginSucceeded = loginResult === true || loginResult?.success === true;
1466
- if (!loginSucceeded) {
1467
- return {
1468
- ...firstResult,
1469
- loginOpened: true,
1470
- error: loginResult?.error
1471
- || 'Codex sign-in was not completed. Finish the browser login and retry.'
1472
- };
1473
- }
1474
-
1475
- const retryResult = await runSdkImageOnce(body);
1476
- return {
1477
- ...retryResult,
1478
- loginOpened: true,
1479
- authenticationRequired: retryResult.authenticationRequired
1480
- };
1481
- }
1482
-
1483
- async function resumeThread(body = {}) {
1484
- const providerKind = await resolveProvider(body.providerKind || body.runtime);
1485
- const threadId = String(body.threadId || '').trim();
1486
- if (!threadId) {
1487
- const err = new Error('threadId is required.');
1488
- err.statusCode = 400;
1489
- throw err;
1490
- }
1491
-
1492
- if (threads.has(threadId)) {
1493
- const record = threads.get(threadId);
1494
- record.lastStatus = 'resumed';
1495
- record.updatedAtUtc = new Date().toISOString();
1496
- return makeEmptyResult({
1497
- success: true,
1498
- providerKind: record.providerKind,
1499
- threadId,
1500
- status: 'resumed',
1501
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[record.options?.sandboxMode] || record.options?.sandboxMode || ''
1502
- });
1503
- }
1504
-
1505
- if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1506
- const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1507
- const threadOptions = buildThreadOptions(body, workingDirectory);
1508
- const sdk = await sdkLoader();
1509
- await prepareRuntimeHome();
1510
- const codex = new sdk.Codex({
1511
- env: childEnvFactory(),
1512
- config: buildCodexSdkConfigOverrides()
1513
- });
1514
- const thread = codex.resumeThread(threadId, threadOptions);
1515
- threads.set(threadId, {
1516
- providerKind,
1517
- thread,
1518
- createdAtUtc: new Date().toISOString(),
1519
- updatedAtUtc: new Date().toISOString(),
1520
- options: threadOptions,
1521
- officialThreadId: threadId,
1522
- lastStatus: 'resumed'
1523
- });
1524
- return makeEmptyResult({
1525
- success: true,
1526
- providerKind,
1527
- threadId,
1528
- status: 'resumed',
1529
- sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1530
- });
1531
- }
1532
-
1533
- return makeEmptyResult({
1534
- providerKind,
1535
- threadId,
1536
- error: 'Thread resume is only supported by the TypeScript SDK provider.'
1537
- });
1538
- }
1539
-
1540
- function cancelThread(body = {}) {
1541
- const threadId = String(body.threadId || '').trim();
1542
- const turnId = String(body.turnId || '').trim();
1543
- const matches = [];
1544
- for (const [key, turn] of activeTurns.entries()) {
1545
- if (threadId && turn.threadId !== threadId) {
1546
- continue;
1547
- }
1548
- if (turnId && turn.turnId !== turnId) {
1549
- continue;
1550
- }
1551
- matches.push([key, turn]);
1552
- }
1553
-
1554
- for (const [, turn] of matches) {
1555
- try {
1556
- turn.abortController.abort();
1557
- } catch {
1558
- // Ignore duplicate aborts.
1559
- }
1560
- }
1561
-
1562
- return {
1563
- success: matches.length > 0,
1564
- cancelled: matches.length,
1565
- threadId,
1566
- turnId,
1567
- status: matches.length > 0 ? 'cancel-requested' : 'not-found'
1568
- };
1569
- }
1570
-
1571
- function getThreadStatus(threadId) {
1572
- const id = String(threadId || '').trim();
1573
- const record = threads.get(id);
1574
- const active = Array.from(activeTurns.values())
1575
- .filter(turn => turn.threadId === id || record?.officialThreadId === turn.threadId)
1576
- .map(turn => ({
1577
- providerKind: turn.providerKind,
1578
- threadId: turn.threadId,
1579
- turnId: turn.turnId,
1580
- startedAtUtc: turn.startedAtUtc,
1581
- status: 'running'
1582
- }));
1583
-
1584
- return {
1585
- success: Boolean(record || active.length > 0),
1586
- threadId: id,
1587
- providerKind: record?.providerKind || active[0]?.providerKind || PROVIDER_KIND.unavailable,
1588
- officialThreadId: record?.officialThreadId || null,
1589
- status: active.length > 0 ? 'running' : (record?.lastStatus || 'not-found'),
1590
- createdAtUtc: record?.createdAtUtc || null,
1591
- updatedAtUtc: record?.updatedAtUtc || null,
1592
- options: serializeThreadOptionsForStatus(record?.options),
1593
- activeTurns: active,
1594
- lastResult: record?.lastResult || null
1595
- };
1596
- }
1597
-
1598
- return {
1599
- providerKinds: PROVIDER_KIND,
1600
- getCapabilities,
1601
- startThread,
1602
- runThread,
1603
- runImage,
1604
- resumeThread,
1605
- cancelThread,
1606
- getThreadStatus
1607
- };
1608
- }
1
+ import { promises as fs, readFileSync } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { exec, spawn } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import crypto from 'crypto';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ const PROVIDER_KIND = Object.freeze({
11
+ typeScriptSdk: 'TypeScriptSdk',
12
+ appServerJsonRpc: 'AppServerJsonRpc',
13
+ legacyExec: 'LegacyExec',
14
+ unavailable: 'Unavailable'
15
+ });
16
+
17
+ const SDK_SANDBOX_BY_API_VALUE = Object.freeze({
18
+ read_only: 'read-only',
19
+ 'read-only': 'read-only',
20
+ readonly: 'read-only',
21
+ workspace_write: 'workspace-write',
22
+ 'workspace-write': 'workspace-write',
23
+ workspacewrite: 'workspace-write',
24
+ full_access: 'danger-full-access',
25
+ fullaccess: 'danger-full-access',
26
+ danger_full_access: 'danger-full-access',
27
+ 'danger-full-access': 'danger-full-access'
28
+ });
29
+
30
+ const API_SANDBOX_BY_SDK_VALUE = Object.freeze({
31
+ 'read-only': 'read_only',
32
+ 'workspace-write': 'workspace_write',
33
+ 'danger-full-access': 'full_access'
34
+ });
35
+
36
+ const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
37
+ const CODEX_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
38
+ const MAX_LOG_CHARS = 4000;
39
+ const MAX_EVENT_LOG = 120;
40
+ const MAX_IMAGE_REFERENCE_COUNT = 8;
41
+ const MAX_IMAGE_REFERENCE_BYTES = 40 * 1024 * 1024;
42
+ const MAX_GENERATED_IMAGE_BYTES = 50 * 1024 * 1024;
43
+ const TEMP_DIR = '.ai/codex';
44
+ const CODEX_RUNTIME_HOME_ENV = 'MINDEXEC_CODEX_HOME';
45
+ const CODEX_RUNTIME_CONFIG_MARKER = '# Generated by MindExec LocalBridge for isolated AI node runs.';
46
+ const CODEX_TARGET_BY_PLATFORM = Object.freeze({
47
+ 'win32:x64': {
48
+ packageName: '@openai/codex-win32-x64',
49
+ targetTriple: 'x86_64-pc-windows-msvc',
50
+ binaryName: 'codex.exe'
51
+ },
52
+ 'win32:arm64': {
53
+ packageName: '@openai/codex-win32-arm64',
54
+ targetTriple: 'aarch64-pc-windows-msvc',
55
+ binaryName: 'codex.exe'
56
+ },
57
+ 'darwin:x64': {
58
+ packageName: '@openai/codex-darwin-x64',
59
+ targetTriple: 'x86_64-apple-darwin',
60
+ binaryName: 'codex'
61
+ },
62
+ 'darwin:arm64': {
63
+ packageName: '@openai/codex-darwin-arm64',
64
+ targetTriple: 'aarch64-apple-darwin',
65
+ binaryName: 'codex'
66
+ },
67
+ 'linux:x64': {
68
+ packageName: '@openai/codex-linux-x64',
69
+ targetTriple: 'x86_64-unknown-linux-musl',
70
+ binaryName: 'codex'
71
+ },
72
+ 'linux:arm64': {
73
+ packageName: '@openai/codex-linux-arm64',
74
+ targetTriple: 'aarch64-unknown-linux-musl',
75
+ binaryName: 'codex'
76
+ }
77
+ });
78
+
79
+ let cachedSdkModule = null;
80
+ let cachedSdkLoadError = null;
81
+ let cachedLegacyAvailability = null;
82
+ let cachedLegacyAvailabilityExpiresAt = 0;
83
+
84
+ function safeReadPackageVersion(packageRoot, packageName) {
85
+ try {
86
+ const packageJsonPath = path.join(packageRoot, 'node_modules', ...packageName.split('/'), 'package.json');
87
+ const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
88
+ return parsed.version || null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async function loadCodexSdk() {
95
+ if (cachedSdkModule) {
96
+ return cachedSdkModule;
97
+ }
98
+
99
+ try {
100
+ cachedSdkModule = await import('@openai/codex-sdk');
101
+ cachedSdkLoadError = null;
102
+ return cachedSdkModule;
103
+ } catch (err) {
104
+ cachedSdkLoadError = err?.message || String(err);
105
+ return null;
106
+ }
107
+ }
108
+
109
+ function normalizeProviderKind(value) {
110
+ const normalized = String(value || '').trim().toLowerCase();
111
+ if (!normalized || normalized === 'auto' || normalized === 'sdk') {
112
+ return '';
113
+ }
114
+
115
+ if (normalized === 'typescriptsdk' || normalized === 'typescript-sdk' || normalized === 'type_script_sdk') {
116
+ return PROVIDER_KIND.typeScriptSdk;
117
+ }
118
+
119
+ if (normalized === 'legacyexec' || normalized === 'legacy-exec' || normalized === 'exec') {
120
+ return PROVIDER_KIND.legacyExec;
121
+ }
122
+
123
+ if (normalized === 'appserverjsonrpc' || normalized === 'app-server-json-rpc') {
124
+ return PROVIDER_KIND.appServerJsonRpc;
125
+ }
126
+
127
+ return '';
128
+ }
129
+
130
+ function resolveUserCodexHome() {
131
+ const configured = String(process.env.CODEX_HOME || '').trim();
132
+ return path.resolve(configured || path.join(os.homedir(), '.codex'));
133
+ }
134
+
135
+ export function resolveCodexRuntimeHome() {
136
+ const configured = String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim();
137
+ return path.resolve(configured || resolveUserCodexHome());
138
+ }
139
+
140
+ function usesExplicitIsolatedCodexHome() {
141
+ return String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim().length > 0;
142
+ }
143
+
144
+ function buildCodexSdkConfigOverrides(additional = {}) {
145
+ return {
146
+ mcp_servers: {},
147
+ ...additional
148
+ };
149
+ }
150
+
151
+ export async function ensureCodexRuntimeHome() {
152
+ const runtimeHome = resolveCodexRuntimeHome();
153
+ await fs.mkdir(runtimeHome, { recursive: true });
154
+ await fs.mkdir(path.join(runtimeHome, 'sessions'), { recursive: true });
155
+ await fs.mkdir(path.join(runtimeHome, 'generated_images'), { recursive: true });
156
+
157
+ if (!usesExplicitIsolatedCodexHome()) {
158
+ return runtimeHome;
159
+ }
160
+
161
+ const configPath = path.join(runtimeHome, 'config.toml');
162
+ let shouldWriteConfig = true;
163
+ try {
164
+ const existing = await fs.readFile(configPath, 'utf8');
165
+ shouldWriteConfig = existing.trim().length === 0 || existing.includes(CODEX_RUNTIME_CONFIG_MARKER);
166
+ } catch {
167
+ shouldWriteConfig = true;
168
+ }
169
+
170
+ if (shouldWriteConfig) {
171
+ await fs.writeFile(
172
+ configPath,
173
+ `${CODEX_RUNTIME_CONFIG_MARKER}\n# User MCP server definitions are intentionally not inherited here.\n# LocalBridge passes model, sandbox, and reasoning options per run.\n`,
174
+ 'utf8'
175
+ );
176
+ }
177
+
178
+ return runtimeHome;
179
+ }
180
+
181
+ export function buildCodexChildEnv() {
182
+ const env = {};
183
+ for (const [key, value] of Object.entries(process.env)) {
184
+ if (value !== undefined) {
185
+ env[key] = value;
186
+ }
187
+ }
188
+
189
+ env.CODEX_HOME = resolveCodexRuntimeHome();
190
+ if (usesExplicitIsolatedCodexHome()) {
191
+ env.MINDEXEC_CODEX_ISOLATED_HOME = '1';
192
+ } else {
193
+ delete env.MINDEXEC_CODEX_ISOLATED_HOME;
194
+ }
195
+ return env;
196
+ }
197
+
198
+ function resolveBundledCodexExecutable(packageRoot) {
199
+ const target = CODEX_TARGET_BY_PLATFORM[`${process.platform}:${process.arch}`];
200
+ if (!target) {
201
+ throw new Error(`Unsupported Codex login platform: ${process.platform} (${process.arch}).`);
202
+ }
203
+
204
+ return path.join(
205
+ packageRoot,
206
+ 'node_modules',
207
+ ...target.packageName.split('/'),
208
+ 'vendor',
209
+ target.targetTriple,
210
+ 'bin',
211
+ target.binaryName);
212
+ }
213
+
214
+ function runBundledCodexCommand(packageRoot, args, timeoutMs = CODEX_LOGIN_TIMEOUT_MS) {
215
+ return new Promise((resolve) => {
216
+ let executablePath;
217
+ try {
218
+ executablePath = resolveBundledCodexExecutable(packageRoot);
219
+ } catch (err) {
220
+ resolve({ success: false, exitCode: -1, stdout: '', stderr: '', error: err?.message || String(err) });
221
+ return;
222
+ }
223
+
224
+ const child = spawn(executablePath, args, {
225
+ env: buildCodexChildEnv(),
226
+ windowsHide: true,
227
+ stdio: ['ignore', 'pipe', 'pipe']
228
+ });
229
+ let stdout = '';
230
+ let stderr = '';
231
+ let settled = false;
232
+ let timeout = null;
233
+
234
+ const finish = (result) => {
235
+ if (settled) {
236
+ return;
237
+ }
238
+ settled = true;
239
+ if (timeout) {
240
+ clearTimeout(timeout);
241
+ }
242
+ resolve({
243
+ success: result.exitCode === 0,
244
+ exitCode: result.exitCode,
245
+ stdout: truncateLog(stdout),
246
+ stderr: truncateLog(stderr),
247
+ error: result.error || ''
248
+ });
249
+ };
250
+
251
+ child.stdout?.on('data', chunk => {
252
+ stdout = truncateLog(`${stdout}${chunk}`, MAX_LOG_CHARS);
253
+ });
254
+ child.stderr?.on('data', chunk => {
255
+ stderr = truncateLog(`${stderr}${chunk}`, MAX_LOG_CHARS);
256
+ });
257
+ child.once('error', err => finish({ exitCode: -1, error: err?.message || String(err) }));
258
+ child.once('exit', code => finish({ exitCode: Number.isFinite(Number(code)) ? Number(code) : -1 }));
259
+
260
+ timeout = setTimeout(() => {
261
+ try {
262
+ child.kill();
263
+ } catch {
264
+ // Ignore a process that already exited while the timeout fired.
265
+ }
266
+ finish({ exitCode: -1, error: 'Codex browser login timed out.' });
267
+ }, timeoutMs);
268
+ });
269
+ }
270
+
271
+ function normalizeReasoningEffort(value) {
272
+ const normalized = String(value || '').trim().toLowerCase();
273
+ return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(normalized)
274
+ ? normalized
275
+ : 'medium';
276
+ }
277
+
278
+ function inferSandboxFromIntent(intent, prompt) {
279
+ const text = `${intent || ''}\n${prompt || ''}`.toLowerCase();
280
+ if (/(full[_ -]?access|unrestricted|danger[_ -]?full)/.test(text)) {
281
+ return 'workspace-write';
282
+ }
283
+
284
+ if (/(code[-_\s]?edit|project[-_\s]?fix|generate[-_\s]?files|implement|modify files|write files|patch|apply fix|refactor)/.test(text)) {
285
+ return 'workspace-write';
286
+ }
287
+
288
+ if (/(research|review|analy[sz]e|summari[sz]e|diagnose|inspect|audit|compare|read[-_\s]?only)/.test(text)) {
289
+ return 'read-only';
290
+ }
291
+
292
+ return 'workspace-write';
293
+ }
294
+
295
+ function normalizeSandbox(value, intent, prompt) {
296
+ const normalized = String(value || '').trim().toLowerCase();
297
+ if (normalized && SDK_SANDBOX_BY_API_VALUE[normalized]) {
298
+ return SDK_SANDBOX_BY_API_VALUE[normalized];
299
+ }
300
+
301
+ return inferSandboxFromIntent(intent, prompt);
302
+ }
303
+
304
+ function normalizeApprovalPolicy(value, sandboxMode) {
305
+ const normalized = String(value || '').trim().toLowerCase();
306
+ if (['on-request', 'on-failure', 'untrusted'].includes(normalized)) {
307
+ return normalized;
308
+ }
309
+
310
+ if (normalized === 'never' && sandboxMode !== 'danger-full-access') {
311
+ return 'on-request';
312
+ }
313
+
314
+ return 'on-request';
315
+ }
316
+
317
+ function normalizeOutputSchema(value) {
318
+ if (value == null) {
319
+ return undefined;
320
+ }
321
+
322
+ if (typeof value === 'object') {
323
+ return value;
324
+ }
325
+
326
+ const text = String(value || '').trim();
327
+ if (!text) {
328
+ return undefined;
329
+ }
330
+
331
+ return JSON.parse(text);
332
+ }
333
+
334
+ function truncateLog(value, maxChars = MAX_LOG_CHARS) {
335
+ const text = String(value ?? '');
336
+ if (text.length <= maxChars) {
337
+ return text;
338
+ }
339
+
340
+ return `${text.slice(0, maxChars)}...`;
341
+ }
342
+
343
+ function makeEmptyResult(overrides = {}) {
344
+ const now = new Date().toISOString();
345
+ return {
346
+ success: false,
347
+ providerKind: PROVIDER_KIND.unavailable,
348
+ threadId: '',
349
+ turnId: '',
350
+ finalResponse: '',
351
+ summary: '',
352
+ error: '',
353
+ status: 'failed',
354
+ startedAtUtc: now,
355
+ completedAtUtc: now,
356
+ sandboxMode: '',
357
+ changedFiles: [],
358
+ commandLogs: [],
359
+ approvalEvents: [],
360
+ toolEvents: [],
361
+ rawEventCount: 0,
362
+ ...overrides
363
+ };
364
+ }
365
+
366
+ function upsertToolEvent(result, event) {
367
+ const id = String(event?.id || '').trim();
368
+ const type = String(event?.type || '').trim();
369
+ const name = String(event?.name || '').trim();
370
+ const key = id || `${type}:${name}`;
371
+ const index = result.toolEvents.findIndex(existing =>
372
+ (String(existing?.id || '').trim() || `${existing?.type || ''}:${existing?.name || ''}`) === key);
373
+ if (index >= 0) {
374
+ result.toolEvents[index] = event;
375
+ return;
376
+ }
377
+
378
+ result.toolEvents.push(event);
379
+ }
380
+
381
+ function collectItem(result, item, eventType = '') {
382
+ if (!item || typeof item !== 'object') {
383
+ return;
384
+ }
385
+
386
+ if (item.type === 'agent_message' && typeof item.text === 'string') {
387
+ result.finalResponse = item.text;
388
+ return;
389
+ }
390
+
391
+ if (item.type === 'reasoning' && typeof item.text === 'string') {
392
+ result.summary = result.summary
393
+ ? `${result.summary}\n${item.text}`.trim()
394
+ : item.text.trim();
395
+ return;
396
+ }
397
+
398
+ if (item.type === 'file_change' && Array.isArray(item.changes)) {
399
+ for (const change of item.changes) {
400
+ const changePath = String(change?.path || '').trim();
401
+ if (!changePath) {
402
+ continue;
403
+ }
404
+
405
+ if (!result.changedFiles.some(existing => existing.path === changePath)) {
406
+ result.changedFiles.push({
407
+ path: changePath,
408
+ kind: String(change?.kind || '').trim(),
409
+ status: String(item.status || '').trim()
410
+ });
411
+ }
412
+ }
413
+ return;
414
+ }
415
+
416
+ if (item.type === 'command_execution') {
417
+ result.commandLogs.push({
418
+ id: String(item.id || ''),
419
+ command: String(item.command || ''),
420
+ status: String(item.status || ''),
421
+ exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
422
+ output: truncateLog(item.aggregated_output)
423
+ });
424
+ upsertToolEvent(result, {
425
+ id: String(item.id || ''),
426
+ type: 'command_execution',
427
+ name: String(item.command || '').trim() || 'Shell',
428
+ status: String(item.status || eventType || ''),
429
+ summary: truncateLog(item.aggregated_output, 1200)
430
+ });
431
+ return;
432
+ }
433
+
434
+ if (item.type === 'web_search') {
435
+ upsertToolEvent(result, {
436
+ id: String(item.id || ''),
437
+ type: 'web_search',
438
+ name: 'WebSearch',
439
+ status: eventType === 'item.completed' ? 'completed' : 'running',
440
+ summary: truncateLog(item.query, 1200)
441
+ });
442
+ return;
443
+ }
444
+
445
+ if (item.type === 'mcp_tool_call') {
446
+ const server = String(item.server || '').trim();
447
+ const tool = String(item.tool || '').trim();
448
+ upsertToolEvent(result, {
449
+ id: String(item.id || ''),
450
+ type: 'mcp_tool_call',
451
+ name: [server, tool].filter(Boolean).join('.') || 'MCP',
452
+ status: String(item.status || eventType || ''),
453
+ summary: item.error?.message
454
+ ? truncateLog(item.error.message, 1200)
455
+ : truncateLog(JSON.stringify(item.arguments ?? {}), 1200)
456
+ });
457
+ return;
458
+ }
459
+
460
+ if (item.type === 'todo_list') {
461
+ const items = Array.isArray(item.items) ? item.items : [];
462
+ upsertToolEvent(result, {
463
+ id: String(item.id || ''),
464
+ type: 'todo_list',
465
+ name: 'Plan',
466
+ status: eventType === 'item.completed' ? 'completed' : 'running',
467
+ summary: truncateLog(items.map(entry => `${entry?.completed ? '[x]' : '[ ]'} ${entry?.text || ''}`).join('\n'), 1200)
468
+ });
469
+ return;
470
+ }
471
+
472
+ if (String(item.type || '').toLowerCase().includes('approval')) {
473
+ result.approvalEvents.push({
474
+ id: String(item.id || ''),
475
+ type: String(item.type || ''),
476
+ status: String(item.status || ''),
477
+ summary: truncateLog(JSON.stringify(item), 1200)
478
+ });
479
+ }
480
+ }
481
+
482
+ function extractLegacyEvents(stdout, stderr) {
483
+ const result = {
484
+ finalResponse: '',
485
+ summary: '',
486
+ error: '',
487
+ threadId: '',
488
+ changedFiles: [],
489
+ commandLogs: [],
490
+ approvalEvents: [],
491
+ rawEventCount: 0
492
+ };
493
+
494
+ const lines = String(stdout || '')
495
+ .replace(/\r\n/g, '\n')
496
+ .replace(/\r/g, '\n')
497
+ .split('\n')
498
+ .filter(line => line.trim().length > 0);
499
+
500
+ for (const line of lines) {
501
+ let event;
502
+ try {
503
+ event = JSON.parse(line);
504
+ } catch {
505
+ continue;
506
+ }
507
+
508
+ result.rawEventCount += 1;
509
+ if (event.type === 'thread.started' && event.thread_id) {
510
+ result.threadId = String(event.thread_id);
511
+ }
512
+
513
+ if (event.type === 'turn.failed' && event.error?.message) {
514
+ result.error = String(event.error.message);
515
+ }
516
+
517
+ if (event.type === 'error' && event.message) {
518
+ result.error = String(event.message);
519
+ }
520
+
521
+ const item = event.item;
522
+ if (item?.type === 'agent_message' && typeof item.text === 'string') {
523
+ result.finalResponse = item.text;
524
+ } else if (item?.type === 'reasoning' && typeof item.text === 'string') {
525
+ result.summary = result.summary
526
+ ? `${result.summary}\n${item.text}`.trim()
527
+ : item.text.trim();
528
+ } else if (item?.type === 'file_change' && Array.isArray(item.changes)) {
529
+ for (const change of item.changes) {
530
+ const changePath = String(change?.path || '').trim();
531
+ if (changePath) {
532
+ result.changedFiles.push({
533
+ path: changePath,
534
+ kind: String(change?.kind || ''),
535
+ status: String(item.status || '')
536
+ });
537
+ }
538
+ }
539
+ } else if (item?.type === 'command_execution') {
540
+ result.commandLogs.push({
541
+ id: String(item.id || ''),
542
+ command: String(item.command || ''),
543
+ status: String(item.status || ''),
544
+ exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
545
+ output: truncateLog(item.aggregated_output)
546
+ });
547
+ }
548
+ }
549
+
550
+ if (!result.finalResponse && lines.length > 0) {
551
+ result.finalResponse = String(stdout || '').trim();
552
+ }
553
+
554
+ if (!result.error && stderr) {
555
+ result.error = String(stderr).trim();
556
+ }
557
+
558
+ return result;
559
+ }
560
+
561
+ function buildThreadOptions(body, workingDirectory) {
562
+ const prompt = String(body?.instruction || body?.prompt || '').trim();
563
+ const sandboxMode = normalizeSandbox(body?.sandbox || body?.sandboxMode, body?.intent || body?.nodeKind, prompt);
564
+ const approvalPolicy = normalizeApprovalPolicy(body?.approvalPolicy, sandboxMode);
565
+ const options = {
566
+ workingDirectory,
567
+ skipGitRepoCheck: true,
568
+ sandboxMode,
569
+ modelReasoningEffort: normalizeReasoningEffort(body?.reasoningEffort),
570
+ approvalPolicy
571
+ };
572
+
573
+ const model = String(body?.model || '').trim();
574
+ if (model) {
575
+ options.model = model;
576
+ }
577
+
578
+ if (body?.networkAccessEnabled !== undefined) {
579
+ options.networkAccessEnabled = Boolean(body.networkAccessEnabled);
580
+ }
581
+
582
+ const webSearchMode = String(body?.webSearchMode || '').trim().toLowerCase();
583
+ if (['disabled', 'cached', 'live'].includes(webSearchMode)) {
584
+ options.webSearchMode = webSearchMode;
585
+ }
586
+
587
+ return options;
588
+ }
589
+
590
+ function serializeThreadOptionsForStatus(options) {
591
+ return {
592
+ model: options?.model || null,
593
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[options?.sandboxMode] || options?.sandboxMode || null,
594
+ reasoningEffort: options?.modelReasoningEffort || null,
595
+ approvalPolicy: options?.approvalPolicy || null,
596
+ networkAccessEnabled: options?.networkAccessEnabled ?? null,
597
+ webSearchMode: options?.webSearchMode || null,
598
+ workingDirectory: options?.workingDirectory || null
599
+ };
600
+ }
601
+
602
+ async function checkSdkAvailability(packageRoot, sdkLoader = loadCodexSdk) {
603
+ const sdk = await sdkLoader();
604
+ return {
605
+ kind: PROVIDER_KIND.typeScriptSdk,
606
+ available: Boolean(sdk?.Codex),
607
+ version: safeReadPackageVersion(packageRoot, '@openai/codex-sdk'),
608
+ error: sdk?.Codex ? null : (cachedSdkLoadError || 'Codex SDK module did not export Codex.')
609
+ };
610
+ }
611
+
612
+ async function checkLegacyAvailability() {
613
+ const now = Date.now();
614
+ if (cachedLegacyAvailability && cachedLegacyAvailabilityExpiresAt > now) {
615
+ return cachedLegacyAvailability;
616
+ }
617
+
618
+ try {
619
+ const { stdout, stderr } = await execAsync('codex --version', {
620
+ timeout: 6000,
621
+ windowsHide: true,
622
+ maxBuffer: 512 * 1024
623
+ });
624
+ cachedLegacyAvailability = {
625
+ kind: PROVIDER_KIND.legacyExec,
626
+ available: true,
627
+ version: String(stdout || stderr || '').trim(),
628
+ error: null
629
+ };
630
+ } catch (err) {
631
+ cachedLegacyAvailability = {
632
+ kind: PROVIDER_KIND.legacyExec,
633
+ available: false,
634
+ version: null,
635
+ error: err?.message || String(err)
636
+ };
637
+ }
638
+
639
+ cachedLegacyAvailabilityExpiresAt = now + 30 * 1000;
640
+ return cachedLegacyAvailability;
641
+ }
642
+
643
+ function extractPrompt(body) {
644
+ const prompt = String(body?.instruction || body?.prompt || '').trim();
645
+ if (!prompt) {
646
+ const err = new Error('Codex prompt is required.');
647
+ err.statusCode = 400;
648
+ throw err;
649
+ }
650
+
651
+ return prompt;
652
+ }
653
+
654
+ function looksLikeCodexAuthenticationError(value) {
655
+ const text = String(value || '').toLowerCase();
656
+ return text.includes('invalid_refresh_token')
657
+ || text.includes('invalid refresh token')
658
+ || text.includes('login required')
659
+ || text.includes('not logged in')
660
+ || text.includes('please log out and sign in again')
661
+ || text.includes('could not parse your authentication token')
662
+ || text.includes('authrequired')
663
+ || text.includes('401 unauthorized');
664
+ }
665
+
666
+ function normalizeImageMimeType(value, fallback = 'image/png') {
667
+ const normalized = String(value || '').trim().toLowerCase();
668
+ return /^image\/[a-z0-9.+-]+$/.test(normalized) ? normalized : fallback;
669
+ }
670
+
671
+ function inferImageMimeType(buffer, fallback = 'image/png') {
672
+ if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
673
+ return fallback;
674
+ }
675
+
676
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
677
+ return 'image/png';
678
+ }
679
+ if (buffer[0] === 0xff && buffer[1] === 0xd8) {
680
+ return 'image/jpeg';
681
+ }
682
+ if (buffer.slice(0, 3).toString('ascii') === 'GIF') {
683
+ return 'image/gif';
684
+ }
685
+ if (buffer.length >= 12
686
+ && buffer.slice(0, 4).toString('ascii') === 'RIFF'
687
+ && buffer.slice(8, 12).toString('ascii') === 'WEBP') {
688
+ return 'image/webp';
689
+ }
690
+
691
+ return fallback;
692
+ }
693
+
694
+ function normalizeGeneratedImageBase64(value, hintedMimeType = '') {
695
+ const text = String(value || '').trim();
696
+ if (!text) {
697
+ return null;
698
+ }
699
+
700
+ const dataUrlMatch = text.match(/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\r\n]+)$/i);
701
+ const base64 = (dataUrlMatch ? dataUrlMatch[2] : text).replace(/\s+/g, '');
702
+ if (base64.length < 32 || !/^[a-z0-9+/]+={0,2}$/i.test(base64)) {
703
+ return null;
704
+ }
705
+
706
+ try {
707
+ const bytes = Buffer.from(base64, 'base64');
708
+ if (!bytes.length || bytes.length > MAX_GENERATED_IMAGE_BYTES) {
709
+ return null;
710
+ }
711
+ const mimeType = inferImageMimeType(
712
+ bytes,
713
+ normalizeImageMimeType(dataUrlMatch?.[1] || hintedMimeType));
714
+ return { imageBase64: bytes.toString('base64'), mimeType };
715
+ } catch {
716
+ return null;
717
+ }
718
+ }
719
+
720
+ function extractGeneratedImageFromEvent(value, depth = 0, hintedMimeType = '') {
721
+ if (value == null || depth > 10) {
722
+ return null;
723
+ }
724
+
725
+ if (typeof value === 'string') {
726
+ return value.startsWith('data:image/')
727
+ ? normalizeGeneratedImageBase64(value, hintedMimeType)
728
+ : null;
729
+ }
730
+
731
+ if (Array.isArray(value)) {
732
+ for (const entry of value) {
733
+ const found = extractGeneratedImageFromEvent(entry, depth + 1, hintedMimeType);
734
+ if (found) {
735
+ return found;
736
+ }
737
+ }
738
+ return null;
739
+ }
740
+
741
+ if (typeof value !== 'object') {
742
+ return null;
743
+ }
744
+
745
+ const localMimeType = normalizeImageMimeType(
746
+ value.mime_type || value.mimeType || value.content_type || value.contentType,
747
+ hintedMimeType || 'image/png');
748
+ for (const key of ['b64_json', 'image_base64', 'imageBase64', 'base64']) {
749
+ if (typeof value[key] === 'string') {
750
+ const found = normalizeGeneratedImageBase64(value[key], localMimeType);
751
+ if (found) {
752
+ return found;
753
+ }
754
+ }
755
+ }
756
+
757
+ for (const key of ['result', 'output', 'image', 'images', 'data', 'payload', 'item']) {
758
+ if (value[key] != null) {
759
+ const found = extractGeneratedImageFromEvent(value[key], depth + 1, localMimeType);
760
+ if (found) {
761
+ return found;
762
+ }
763
+ }
764
+ }
765
+
766
+ return null;
767
+ }
768
+
769
+ function imageExtensionForReference(reference) {
770
+ const fileNameExtension = path.extname(String(reference?.fileName || '')).toLowerCase();
771
+ if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(fileNameExtension)) {
772
+ return fileNameExtension === '.jpeg' ? '.jpg' : fileNameExtension;
773
+ }
774
+
775
+ const mimeType = normalizeImageMimeType(reference?.mimeType);
776
+ if (mimeType === 'image/jpeg') return '.jpg';
777
+ if (mimeType === 'image/webp') return '.webp';
778
+ if (mimeType === 'image/gif') return '.gif';
779
+ return '.png';
780
+ }
781
+
782
+ async function writeSdkImageReferences(runtimeHome, references) {
783
+ const normalized = Array.isArray(references)
784
+ ? references.slice(0, MAX_IMAGE_REFERENCE_COUNT)
785
+ : [];
786
+ if (!normalized.length) {
787
+ return { directory: '', paths: [] };
788
+ }
789
+
790
+ const root = path.join(runtimeHome, 'image-inputs');
791
+ const directory = path.join(root, crypto.randomUUID());
792
+ await fs.mkdir(directory, { recursive: true });
793
+ const paths = [];
794
+ let totalBytes = 0;
795
+
796
+ try {
797
+ for (let index = 0; index < normalized.length; index += 1) {
798
+ const reference = normalized[index];
799
+ const bytes = Buffer.from(String(reference?.base64 || ''), 'base64');
800
+ totalBytes += bytes.length;
801
+ if (!bytes.length || totalBytes > MAX_IMAGE_REFERENCE_BYTES) {
802
+ throw new Error('Codex SDK image references are empty or exceed the 40 MB request limit.');
803
+ }
804
+
805
+ const filePath = path.join(directory, `reference-${index + 1}${imageExtensionForReference(reference)}`);
806
+ await fs.writeFile(filePath, bytes);
807
+ paths.push(filePath);
808
+ }
809
+
810
+ return { directory, paths };
811
+ } catch (err) {
812
+ await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
813
+ throw err;
814
+ }
815
+ }
816
+
817
+ async function cleanupSdkImageReferences(runtimeHome, directory) {
818
+ if (!directory) {
819
+ return;
820
+ }
821
+
822
+ const root = path.resolve(runtimeHome, 'image-inputs');
823
+ const target = path.resolve(directory);
824
+ if (!target.startsWith(`${root}${path.sep}`)) {
825
+ return;
826
+ }
827
+ await fs.rm(target, { recursive: true, force: true }).catch(() => {});
828
+ }
829
+
830
+ function buildSdkImagePrompt(body, referenceCount) {
831
+ const prompt = extractPrompt(body);
832
+ const action = String(body?.action || '').trim().toLowerCase() === 'edit' || referenceCount > 0
833
+ ? 'edit'
834
+ : 'generate';
835
+ const size = String(body?.size || '').trim();
836
+ const quality = String(body?.quality || '').trim();
837
+ return [
838
+ action === 'edit'
839
+ ? `Use the image generation tool to edit the ${referenceCount} attached reference image(s).`
840
+ : 'Use the image generation tool to create one new image.',
841
+ prompt,
842
+ size ? `Requested output size: ${size}.` : '',
843
+ quality ? `Requested quality: ${quality}.` : '',
844
+ 'Return exactly one generated image. Do not answer with instructions or prose.'
845
+ ].filter(Boolean).join('\n\n');
846
+ }
847
+
848
+ async function listGeneratedImageCandidates(directory, minimumMtimeMs, depth = 0) {
849
+ if (depth > 4) {
850
+ return [];
851
+ }
852
+
853
+ const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
854
+ const candidates = [];
855
+ for (const entry of entries) {
856
+ const fullPath = path.join(directory, entry.name);
857
+ if (entry.isDirectory()) {
858
+ candidates.push(...await listGeneratedImageCandidates(fullPath, minimumMtimeMs, depth + 1));
859
+ continue;
860
+ }
861
+ if (!entry.isFile() || !/\.(png|jpe?g|webp|gif)$/i.test(entry.name)) {
862
+ continue;
863
+ }
864
+
865
+ const stat = await fs.stat(fullPath).catch(() => null);
866
+ if (stat && stat.size > 0 && stat.size <= MAX_GENERATED_IMAGE_BYTES && stat.mtimeMs >= minimumMtimeMs) {
867
+ candidates.push({ fullPath, mtimeMs: stat.mtimeMs });
868
+ }
869
+ }
870
+ return candidates;
871
+ }
872
+
873
+ async function recoverSdkGeneratedImage(runtimeHome, startedAtMs) {
874
+ const generatedRoot = path.join(runtimeHome, 'generated_images');
875
+ const minimumMtimeMs = startedAtMs - 15 * 1000;
876
+ for (let attempt = 0; attempt < 8; attempt += 1) {
877
+ const candidates = await listGeneratedImageCandidates(generatedRoot, minimumMtimeMs);
878
+ candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
879
+ if (candidates[0]) {
880
+ const bytes = await fs.readFile(candidates[0].fullPath);
881
+ return {
882
+ imageBase64: bytes.toString('base64'),
883
+ mimeType: inferImageMimeType(bytes)
884
+ };
885
+ }
886
+ await new Promise(resolve => setTimeout(resolve, 350));
887
+ }
888
+ return null;
889
+ }
890
+
891
+ export function createCodexRuntime(options) {
892
+ const packageRoot = options.packageRoot;
893
+ const resolveWorkingDirectory = options.resolveWorkingDirectory;
894
+ const getCurrentRuntime = options.getCurrentRuntime;
895
+ const getModelCatalog = options.getModelCatalog;
896
+ const log = typeof options.log === 'function' ? options.log : () => {};
897
+ const sdkLoader = typeof options.loadCodexSdk === 'function' ? options.loadCodexSdk : loadCodexSdk;
898
+ const prepareRuntimeHome = typeof options.ensureCodexRuntimeHome === 'function'
899
+ ? options.ensureCodexRuntimeHome
900
+ : ensureCodexRuntimeHome;
901
+ const childEnvFactory = typeof options.buildCodexChildEnv === 'function'
902
+ ? options.buildCodexChildEnv
903
+ : buildCodexChildEnv;
904
+
905
+ const threads = new Map();
906
+ const activeTurns = new Map();
907
+ let activeLoginPromise = null;
908
+
909
+ async function loginWithBundledCodex() {
910
+ if (typeof options.loginCodex === 'function') {
911
+ return options.loginCodex();
912
+ }
913
+
914
+ if (activeLoginPromise) {
915
+ return activeLoginPromise;
916
+ }
917
+
918
+ activeLoginPromise = (async () => {
919
+ await prepareRuntimeHome();
920
+ await runBundledCodexCommand(packageRoot, ['logout'], 15000).catch(() => null);
921
+ log('Codex SDK browser login start', { runtimeHome: resolveCodexRuntimeHome() });
922
+ const loginResult = await runBundledCodexCommand(packageRoot, ['login']);
923
+ log('Codex SDK browser login finish', {
924
+ success: loginResult.success,
925
+ exitCode: loginResult.exitCode,
926
+ error: loginResult.error || ''
927
+ });
928
+ return loginResult;
929
+ })().finally(() => {
930
+ activeLoginPromise = null;
931
+ });
932
+
933
+ return activeLoginPromise;
934
+ }
935
+
936
+ async function getCapabilities(options = {}) {
937
+ const [sdk, legacy, modelCatalog] = await Promise.all([
938
+ checkSdkAvailability(packageRoot, sdkLoader),
939
+ checkLegacyAvailability(),
940
+ getModelCatalog({ forceRefresh: Boolean(options.forceRefresh) })
941
+ ]);
942
+ const preferredProviderKind = sdk.available
943
+ ? PROVIDER_KIND.typeScriptSdk
944
+ : legacy.available
945
+ ? PROVIDER_KIND.legacyExec
946
+ : PROVIDER_KIND.unavailable;
947
+
948
+ return {
949
+ success: preferredProviderKind !== PROVIDER_KIND.unavailable,
950
+ preferredProviderKind,
951
+ providers: [
952
+ sdk,
953
+ {
954
+ kind: PROVIDER_KIND.appServerJsonRpc,
955
+ available: false,
956
+ implemented: false,
957
+ error: 'DTO scaffold only. Use TypeScriptSdk for LocalBridge execution.'
958
+ },
959
+ legacy
960
+ ],
961
+ sandboxModes: ['read_only', 'workspace_write', 'full_access'],
962
+ endpoints: [
963
+ 'GET /api/codex/capabilities',
964
+ 'POST /api/codex/thread/start',
965
+ 'POST /api/codex/thread/run',
966
+ 'POST /api/codex/thread/resume',
967
+ 'POST /api/codex/thread/cancel',
968
+ 'POST /api/codex/image/run',
969
+ 'GET /api/codex/thread/{threadId}/status'
970
+ ],
971
+ runtime: getCurrentRuntime(),
972
+ models: modelCatalog.models || [],
973
+ modelCatalogSource: modelCatalog.source,
974
+ modelCatalogCommandSource: modelCatalog.commandSource || null,
975
+ modelCatalogUpdatedAt: modelCatalog.updatedAt,
976
+ modelCatalogRefreshAttemptedAt: modelCatalog.refreshAttemptedAt || null,
977
+ modelCatalogStale: modelCatalog.stale === true,
978
+ modelCatalogError: modelCatalog.error || null
979
+ };
980
+ }
981
+
982
+ async function resolveProvider(requestedProviderKind) {
983
+ const requested = normalizeProviderKind(requestedProviderKind);
984
+ const sdk = await checkSdkAvailability(packageRoot, sdkLoader);
985
+ const legacy = requested === PROVIDER_KIND.typeScriptSdk
986
+ ? null
987
+ : await checkLegacyAvailability();
988
+
989
+ if ((!requested || requested === PROVIDER_KIND.typeScriptSdk) && sdk.available) {
990
+ return PROVIDER_KIND.typeScriptSdk;
991
+ }
992
+
993
+ if ((!requested || requested === PROVIDER_KIND.legacyExec) && legacy?.available) {
994
+ return PROVIDER_KIND.legacyExec;
995
+ }
996
+
997
+ return PROVIDER_KIND.unavailable;
998
+ }
999
+
1000
+ async function startThread(body = {}) {
1001
+ const providerKind = await resolveProvider(body.providerKind || body.runtime);
1002
+ const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1003
+ const threadOptions = buildThreadOptions(body, workingDirectory);
1004
+ const now = new Date().toISOString();
1005
+
1006
+ if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1007
+ const sdk = await sdkLoader();
1008
+ await prepareRuntimeHome();
1009
+ const codex = new sdk.Codex({
1010
+ env: childEnvFactory(),
1011
+ config: buildCodexSdkConfigOverrides()
1012
+ });
1013
+ const thread = codex.startThread(threadOptions);
1014
+ const localThreadId = `local_${crypto.randomUUID()}`;
1015
+ threads.set(localThreadId, {
1016
+ providerKind,
1017
+ thread,
1018
+ createdAtUtc: now,
1019
+ updatedAtUtc: now,
1020
+ options: threadOptions,
1021
+ officialThreadId: null,
1022
+ lastStatus: 'created'
1023
+ });
1024
+
1025
+ return makeEmptyResult({
1026
+ success: true,
1027
+ providerKind,
1028
+ threadId: localThreadId,
1029
+ status: 'created',
1030
+ startedAtUtc: now,
1031
+ completedAtUtc: now,
1032
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1033
+ });
1034
+ }
1035
+
1036
+ if (providerKind === PROVIDER_KIND.legacyExec) {
1037
+ const localThreadId = `legacy_${crypto.randomUUID()}`;
1038
+ threads.set(localThreadId, {
1039
+ providerKind,
1040
+ createdAtUtc: now,
1041
+ updatedAtUtc: now,
1042
+ options: threadOptions,
1043
+ officialThreadId: localThreadId,
1044
+ lastStatus: 'created'
1045
+ });
1046
+
1047
+ return makeEmptyResult({
1048
+ success: true,
1049
+ providerKind,
1050
+ threadId: localThreadId,
1051
+ status: 'created',
1052
+ startedAtUtc: now,
1053
+ completedAtUtc: now,
1054
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1055
+ });
1056
+ }
1057
+
1058
+ return makeEmptyResult({
1059
+ providerKind,
1060
+ error: 'Codex local runtime is not available. Install LocalBridge dependencies or Codex CLI, then run codex login.'
1061
+ });
1062
+ }
1063
+
1064
+ async function getSdkThread(body, threadOptions) {
1065
+ const requestedThreadId = String(body?.threadId || '').trim();
1066
+ if (requestedThreadId && threads.has(requestedThreadId)) {
1067
+ return {
1068
+ key: requestedThreadId,
1069
+ record: threads.get(requestedThreadId)
1070
+ };
1071
+ }
1072
+
1073
+ const sdk = await sdkLoader();
1074
+ await prepareRuntimeHome();
1075
+ const codex = new sdk.Codex({
1076
+ env: childEnvFactory(),
1077
+ config: buildCodexSdkConfigOverrides()
1078
+ });
1079
+ const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
1080
+ ? requestedThreadId
1081
+ : '';
1082
+ const thread = officialId
1083
+ ? codex.resumeThread(officialId, threadOptions)
1084
+ : codex.startThread(threadOptions);
1085
+ const key = officialId || `local_${crypto.randomUUID()}`;
1086
+ const record = {
1087
+ providerKind: PROVIDER_KIND.typeScriptSdk,
1088
+ thread,
1089
+ createdAtUtc: new Date().toISOString(),
1090
+ updatedAtUtc: new Date().toISOString(),
1091
+ options: threadOptions,
1092
+ officialThreadId: officialId || null,
1093
+ lastStatus: 'created'
1094
+ };
1095
+ threads.set(key, record);
1096
+ return { key, record };
1097
+ }
1098
+
1099
+ async function runSdkThread(body, workingDirectory, prompt, startedAtUtc) {
1100
+ const threadOptions = buildThreadOptions(body, workingDirectory);
1101
+ const { key, record } = await getSdkThread(body, threadOptions);
1102
+ const turnId = `turn_${crypto.randomUUID()}`;
1103
+ const abortController = new AbortController();
1104
+ const activeKey = `${key}:${turnId}`;
1105
+ activeTurns.set(activeKey, {
1106
+ providerKind: PROVIDER_KIND.typeScriptSdk,
1107
+ threadId: key,
1108
+ turnId,
1109
+ abortController,
1110
+ startedAtUtc
1111
+ });
1112
+
1113
+ const result = makeEmptyResult({
1114
+ providerKind: PROVIDER_KIND.typeScriptSdk,
1115
+ threadId: record.officialThreadId || key,
1116
+ turnId,
1117
+ status: 'running',
1118
+ startedAtUtc,
1119
+ completedAtUtc: '',
1120
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1121
+ });
1122
+ let timeout = null;
1123
+
1124
+ try {
1125
+ timeout = setTimeout(() => {
1126
+ try {
1127
+ abortController.abort();
1128
+ } catch {
1129
+ // Ignore duplicate aborts.
1130
+ }
1131
+ }, DEFAULT_TIMEOUT_MS);
1132
+
1133
+ log('Codex SDK run start', {
1134
+ threadId: result.threadId,
1135
+ turnId,
1136
+ sandbox: result.sandboxMode,
1137
+ cwd: workingDirectory
1138
+ });
1139
+
1140
+ const outputSchema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
1141
+ const { events } = await record.thread.runStreamed(prompt, {
1142
+ outputSchema,
1143
+ signal: abortController.signal
1144
+ });
1145
+
1146
+ for await (const event of events) {
1147
+ result.rawEventCount += 1;
1148
+ if (result.rawEventCount <= MAX_EVENT_LOG) {
1149
+ record.lastEvent = event;
1150
+ }
1151
+
1152
+ if (event.type === 'thread.started' && event.thread_id) {
1153
+ record.officialThreadId = String(event.thread_id);
1154
+ result.threadId = record.officialThreadId;
1155
+ threads.set(record.officialThreadId, record);
1156
+ } else if (event.type === 'item.completed' || event.type === 'item.updated' || event.type === 'item.started') {
1157
+ collectItem(result, event.item, event.type);
1158
+ } else if (event.type === 'turn.failed') {
1159
+ result.error = event.error?.message || 'Codex turn failed.';
1160
+ result.status = 'failed';
1161
+ } else if (event.type === 'error') {
1162
+ result.error = event.message || 'Codex stream failed.';
1163
+ result.status = 'failed';
1164
+ } else if (event.type === 'turn.completed') {
1165
+ result.status = 'completed';
1166
+ }
1167
+ }
1168
+
1169
+ if (!result.error && result.status !== 'failed') {
1170
+ result.success = true;
1171
+ result.status = result.status === 'running' ? 'completed' : result.status;
1172
+ }
1173
+
1174
+ if (!result.finalResponse && result.success) {
1175
+ result.finalResponse = result.summary;
1176
+ }
1177
+
1178
+ if (!result.finalResponse && !result.error) {
1179
+ result.error = 'Codex returned an empty response.';
1180
+ result.status = 'failed';
1181
+ result.success = false;
1182
+ }
1183
+ } catch (err) {
1184
+ const aborted = abortController.signal.aborted;
1185
+ result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
1186
+ result.status = aborted ? 'cancelled' : 'failed';
1187
+ result.success = false;
1188
+ } finally {
1189
+ if (timeout) {
1190
+ clearTimeout(timeout);
1191
+ }
1192
+ result.completedAtUtc = new Date().toISOString();
1193
+ record.updatedAtUtc = result.completedAtUtc;
1194
+ record.lastStatus = result.status;
1195
+ record.lastResult = result;
1196
+ activeTurns.delete(activeKey);
1197
+ }
1198
+
1199
+ return result;
1200
+ }
1201
+
1202
+ async function runLegacyExec(body, workingDirectory, prompt, startedAtUtc) {
1203
+ const threadOptions = buildThreadOptions(body, workingDirectory);
1204
+ const turnId = `turn_${crypto.randomUUID()}`;
1205
+ const threadId = String(body.threadId || `legacy_${crypto.randomUUID()}`);
1206
+ const abortController = new AbortController();
1207
+ const activeKey = `${threadId}:${turnId}`;
1208
+ activeTurns.set(activeKey, {
1209
+ providerKind: PROVIDER_KIND.legacyExec,
1210
+ threadId,
1211
+ turnId,
1212
+ abortController,
1213
+ startedAtUtc
1214
+ });
1215
+
1216
+ const tempDir = path.join(workingDirectory, TEMP_DIR);
1217
+ const schema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
1218
+ let schemaPath = null;
1219
+ await prepareRuntimeHome();
1220
+ const args = [
1221
+ 'exec',
1222
+ '-',
1223
+ '--skip-git-repo-check',
1224
+ '--json',
1225
+ '--disable',
1226
+ 'plugins',
1227
+ '--disable',
1228
+ 'shell_snapshot',
1229
+ '-s',
1230
+ threadOptions.sandboxMode,
1231
+ '--config',
1232
+ `model_reasoning_effort=${threadOptions.modelReasoningEffort}`,
1233
+ '--config',
1234
+ 'mcp_servers={}'
1235
+ ];
1236
+ if (threadOptions.model) {
1237
+ args.push('-m', threadOptions.model);
1238
+ }
1239
+
1240
+ if (schema) {
1241
+ await fs.mkdir(tempDir, { recursive: true });
1242
+ schemaPath = path.join(tempDir, `${crypto.randomUUID()}.schema.json`);
1243
+ await fs.writeFile(schemaPath, JSON.stringify(schema), 'utf8');
1244
+ args.push('--output-schema', schemaPath);
1245
+ }
1246
+
1247
+ const result = makeEmptyResult({
1248
+ providerKind: PROVIDER_KIND.legacyExec,
1249
+ threadId,
1250
+ turnId,
1251
+ status: 'running',
1252
+ startedAtUtc,
1253
+ completedAtUtc: '',
1254
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1255
+ });
1256
+
1257
+ try {
1258
+ const childResult = await new Promise((resolve) => {
1259
+ const child = spawn('codex', args, {
1260
+ cwd: workingDirectory,
1261
+ env: childEnvFactory(),
1262
+ windowsHide: true,
1263
+ signal: abortController.signal
1264
+ });
1265
+ let stdout = '';
1266
+ let stderr = '';
1267
+ let settled = false;
1268
+ const timeout = setTimeout(() => {
1269
+ try {
1270
+ abortController.abort();
1271
+ } catch {
1272
+ // Ignore duplicate aborts.
1273
+ }
1274
+ try {
1275
+ child.kill('SIGTERM');
1276
+ } catch {
1277
+ // Ignore if already exited.
1278
+ }
1279
+ }, DEFAULT_TIMEOUT_MS);
1280
+
1281
+ const settle = (payload) => {
1282
+ if (settled) return;
1283
+ settled = true;
1284
+ clearTimeout(timeout);
1285
+ resolve(payload);
1286
+ };
1287
+
1288
+ child.stdout?.on('data', chunk => {
1289
+ stdout += chunk.toString();
1290
+ });
1291
+ child.stderr?.on('data', chunk => {
1292
+ stderr += chunk.toString();
1293
+ });
1294
+ child.on('error', err => {
1295
+ settle({ exitCode: -1, stdout, stderr, error: err?.message || String(err) });
1296
+ });
1297
+ child.on('close', code => {
1298
+ settle({ exitCode: Number.isFinite(code) ? code : -1, stdout, stderr, error: '' });
1299
+ });
1300
+ child.stdin?.write(prompt);
1301
+ child.stdin?.end();
1302
+ });
1303
+
1304
+ const parsed = extractLegacyEvents(childResult.stdout, childResult.stderr);
1305
+ result.threadId = parsed.threadId || threadId;
1306
+ result.finalResponse = parsed.finalResponse;
1307
+ result.summary = parsed.summary;
1308
+ result.changedFiles = parsed.changedFiles;
1309
+ result.commandLogs = parsed.commandLogs;
1310
+ result.approvalEvents = parsed.approvalEvents;
1311
+ result.rawEventCount = parsed.rawEventCount;
1312
+ result.error = childResult.error || parsed.error;
1313
+ result.success = childResult.exitCode === 0 && Boolean(result.finalResponse);
1314
+ result.status = result.success ? 'completed' : 'failed';
1315
+ if (!result.error && !result.success) {
1316
+ result.error = parsed.error || 'Codex legacy exec returned an empty response.';
1317
+ }
1318
+ } catch (err) {
1319
+ const aborted = abortController.signal.aborted;
1320
+ result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
1321
+ result.status = aborted ? 'cancelled' : 'failed';
1322
+ result.success = false;
1323
+ } finally {
1324
+ result.completedAtUtc = new Date().toISOString();
1325
+ activeTurns.delete(activeKey);
1326
+ if (schemaPath) {
1327
+ await fs.unlink(schemaPath).catch(() => {});
1328
+ }
1329
+ threads.set(result.threadId, {
1330
+ providerKind: PROVIDER_KIND.legacyExec,
1331
+ createdAtUtc: startedAtUtc,
1332
+ updatedAtUtc: result.completedAtUtc,
1333
+ options: threadOptions,
1334
+ officialThreadId: result.threadId,
1335
+ lastStatus: result.status,
1336
+ lastResult: result
1337
+ });
1338
+ }
1339
+
1340
+ return result;
1341
+ }
1342
+
1343
+ async function runThread(body = {}) {
1344
+ const prompt = extractPrompt(body);
1345
+ const startedAtUtc = new Date().toISOString();
1346
+ const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1347
+ const providerKind = await resolveProvider(body.providerKind || body.runtime);
1348
+
1349
+ if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1350
+ return runSdkThread(body, workingDirectory, prompt, startedAtUtc);
1351
+ }
1352
+
1353
+ if (providerKind === PROVIDER_KIND.legacyExec) {
1354
+ return runLegacyExec(body, workingDirectory, prompt, startedAtUtc);
1355
+ }
1356
+
1357
+ return makeEmptyResult({
1358
+ providerKind,
1359
+ startedAtUtc,
1360
+ completedAtUtc: new Date().toISOString(),
1361
+ error: 'Codex local runtime is not available. TypeScript SDK and legacy codex exec are unavailable.'
1362
+ });
1363
+ }
1364
+
1365
+ async function runSdkImageOnce(body = {}) {
1366
+ const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1367
+ const runtimeHome = await prepareRuntimeHome();
1368
+ const sdk = await sdkLoader();
1369
+ if (!sdk?.Codex) {
1370
+ return {
1371
+ success: false,
1372
+ providerKind: PROVIDER_KIND.unavailable,
1373
+ imageBase64: '',
1374
+ mimeType: '',
1375
+ error: cachedSdkLoadError || 'Codex TypeScript SDK is unavailable.',
1376
+ threadId: '',
1377
+ authenticationRequired: false,
1378
+ loginOpened: false
1379
+ };
1380
+ }
1381
+
1382
+ const references = await writeSdkImageReferences(runtimeHome, body.referenceImages);
1383
+ const prompt = buildSdkImagePrompt(body, references.paths.length);
1384
+ const threadOptions = buildThreadOptions({
1385
+ ...body,
1386
+ sandbox: 'read_only',
1387
+ approvalPolicy: 'on-request',
1388
+ reasoningEffort: 'low',
1389
+ networkAccessEnabled: true,
1390
+ webSearchMode: 'disabled'
1391
+ }, workingDirectory);
1392
+ const startedAtMs = Date.now();
1393
+ let threadId = '';
1394
+ let error = '';
1395
+ let generatedImage = null;
1396
+
1397
+ try {
1398
+ const codex = new sdk.Codex({
1399
+ env: childEnvFactory(),
1400
+ config: buildCodexSdkConfigOverrides({
1401
+ features: { image_generation: true }
1402
+ })
1403
+ });
1404
+ const thread = codex.startThread(threadOptions);
1405
+ const input = [
1406
+ { type: 'text', text: prompt },
1407
+ ...references.paths.map(referencePath => ({ type: 'local_image', path: referencePath }))
1408
+ ];
1409
+ const { events } = await thread.runStreamed(input);
1410
+
1411
+ for await (const event of events) {
1412
+ if (event?.type === 'thread.started' && event.thread_id) {
1413
+ threadId = String(event.thread_id);
1414
+ } else if (event?.type === 'turn.failed') {
1415
+ error = event.error?.message || 'Codex SDK image turn failed.';
1416
+ } else if (event?.type === 'error') {
1417
+ error = event.message || 'Codex SDK image stream failed.';
1418
+ }
1419
+
1420
+ generatedImage ||= extractGeneratedImageFromEvent(event);
1421
+ }
1422
+ } catch (err) {
1423
+ error = err?.message || String(err);
1424
+ } finally {
1425
+ await cleanupSdkImageReferences(runtimeHome, references.directory);
1426
+ }
1427
+
1428
+ const authenticationRequired = looksLikeCodexAuthenticationError(error);
1429
+ if (!authenticationRequired) {
1430
+ generatedImage ||= await recoverSdkGeneratedImage(runtimeHome, startedAtMs);
1431
+ }
1432
+ if (generatedImage) {
1433
+ return {
1434
+ success: true,
1435
+ providerKind: PROVIDER_KIND.typeScriptSdk,
1436
+ imageBase64: generatedImage.imageBase64,
1437
+ mimeType: generatedImage.mimeType,
1438
+ error: '',
1439
+ threadId,
1440
+ authenticationRequired: false,
1441
+ loginOpened: false
1442
+ };
1443
+ }
1444
+
1445
+ return {
1446
+ success: false,
1447
+ providerKind: PROVIDER_KIND.typeScriptSdk,
1448
+ imageBase64: '',
1449
+ mimeType: '',
1450
+ error: error || 'Codex SDK completed without returning an image.',
1451
+ threadId,
1452
+ authenticationRequired,
1453
+ loginOpened: false
1454
+ };
1455
+ }
1456
+
1457
+ async function runImage(body = {}) {
1458
+ extractPrompt(body);
1459
+ const firstResult = await runSdkImageOnce(body);
1460
+ if (!firstResult.authenticationRequired) {
1461
+ return firstResult;
1462
+ }
1463
+
1464
+ const loginResult = await loginWithBundledCodex();
1465
+ const loginSucceeded = loginResult === true || loginResult?.success === true;
1466
+ if (!loginSucceeded) {
1467
+ return {
1468
+ ...firstResult,
1469
+ loginOpened: true,
1470
+ error: loginResult?.error
1471
+ || 'Codex sign-in was not completed. Finish the browser login and retry.'
1472
+ };
1473
+ }
1474
+
1475
+ const retryResult = await runSdkImageOnce(body);
1476
+ return {
1477
+ ...retryResult,
1478
+ loginOpened: true,
1479
+ authenticationRequired: retryResult.authenticationRequired
1480
+ };
1481
+ }
1482
+
1483
+ async function resumeThread(body = {}) {
1484
+ const providerKind = await resolveProvider(body.providerKind || body.runtime);
1485
+ const threadId = String(body.threadId || '').trim();
1486
+ if (!threadId) {
1487
+ const err = new Error('threadId is required.');
1488
+ err.statusCode = 400;
1489
+ throw err;
1490
+ }
1491
+
1492
+ if (threads.has(threadId)) {
1493
+ const record = threads.get(threadId);
1494
+ record.lastStatus = 'resumed';
1495
+ record.updatedAtUtc = new Date().toISOString();
1496
+ return makeEmptyResult({
1497
+ success: true,
1498
+ providerKind: record.providerKind,
1499
+ threadId,
1500
+ status: 'resumed',
1501
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[record.options?.sandboxMode] || record.options?.sandboxMode || ''
1502
+ });
1503
+ }
1504
+
1505
+ if (providerKind === PROVIDER_KIND.typeScriptSdk) {
1506
+ const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
1507
+ const threadOptions = buildThreadOptions(body, workingDirectory);
1508
+ const sdk = await sdkLoader();
1509
+ await prepareRuntimeHome();
1510
+ const codex = new sdk.Codex({
1511
+ env: childEnvFactory(),
1512
+ config: buildCodexSdkConfigOverrides()
1513
+ });
1514
+ const thread = codex.resumeThread(threadId, threadOptions);
1515
+ threads.set(threadId, {
1516
+ providerKind,
1517
+ thread,
1518
+ createdAtUtc: new Date().toISOString(),
1519
+ updatedAtUtc: new Date().toISOString(),
1520
+ options: threadOptions,
1521
+ officialThreadId: threadId,
1522
+ lastStatus: 'resumed'
1523
+ });
1524
+ return makeEmptyResult({
1525
+ success: true,
1526
+ providerKind,
1527
+ threadId,
1528
+ status: 'resumed',
1529
+ sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
1530
+ });
1531
+ }
1532
+
1533
+ return makeEmptyResult({
1534
+ providerKind,
1535
+ threadId,
1536
+ error: 'Thread resume is only supported by the TypeScript SDK provider.'
1537
+ });
1538
+ }
1539
+
1540
+ function cancelThread(body = {}) {
1541
+ const threadId = String(body.threadId || '').trim();
1542
+ const turnId = String(body.turnId || '').trim();
1543
+ const matches = [];
1544
+ for (const [key, turn] of activeTurns.entries()) {
1545
+ if (threadId && turn.threadId !== threadId) {
1546
+ continue;
1547
+ }
1548
+ if (turnId && turn.turnId !== turnId) {
1549
+ continue;
1550
+ }
1551
+ matches.push([key, turn]);
1552
+ }
1553
+
1554
+ for (const [, turn] of matches) {
1555
+ try {
1556
+ turn.abortController.abort();
1557
+ } catch {
1558
+ // Ignore duplicate aborts.
1559
+ }
1560
+ }
1561
+
1562
+ return {
1563
+ success: matches.length > 0,
1564
+ cancelled: matches.length,
1565
+ threadId,
1566
+ turnId,
1567
+ status: matches.length > 0 ? 'cancel-requested' : 'not-found'
1568
+ };
1569
+ }
1570
+
1571
+ function getThreadStatus(threadId) {
1572
+ const id = String(threadId || '').trim();
1573
+ const record = threads.get(id);
1574
+ const active = Array.from(activeTurns.values())
1575
+ .filter(turn => turn.threadId === id || record?.officialThreadId === turn.threadId)
1576
+ .map(turn => ({
1577
+ providerKind: turn.providerKind,
1578
+ threadId: turn.threadId,
1579
+ turnId: turn.turnId,
1580
+ startedAtUtc: turn.startedAtUtc,
1581
+ status: 'running'
1582
+ }));
1583
+
1584
+ return {
1585
+ success: Boolean(record || active.length > 0),
1586
+ threadId: id,
1587
+ providerKind: record?.providerKind || active[0]?.providerKind || PROVIDER_KIND.unavailable,
1588
+ officialThreadId: record?.officialThreadId || null,
1589
+ status: active.length > 0 ? 'running' : (record?.lastStatus || 'not-found'),
1590
+ createdAtUtc: record?.createdAtUtc || null,
1591
+ updatedAtUtc: record?.updatedAtUtc || null,
1592
+ options: serializeThreadOptionsForStatus(record?.options),
1593
+ activeTurns: active,
1594
+ lastResult: record?.lastResult || null
1595
+ };
1596
+ }
1597
+
1598
+ return {
1599
+ providerKinds: PROVIDER_KIND,
1600
+ getCapabilities,
1601
+ startThread,
1602
+ runThread,
1603
+ runImage,
1604
+ resumeThread,
1605
+ cancelThread,
1606
+ getThreadStatus
1607
+ };
1608
+ }