@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.
- package/README.md +323 -323
- package/codex-model-catalog.js +202 -202
- package/codex-runtime.js +1608 -1608
- package/desktop-workspace-state.cjs +120 -120
- package/electron/main.cjs +517 -517
- package/electron/preload.cjs +17 -17
- package/electron/recurring-update-owner-smoke.mjs +55 -55
- package/electron/recurring-update-owner.cjs +97 -97
- package/electron/source-smoke.mjs +108 -108
- package/electron/update-feed-resolver.cjs +79 -79
- package/electron/update-feed.json +10 -10
- package/electron/update-manager-smoke.mjs +146 -146
- package/electron/update-manager.cjs +297 -297
- package/electron/windows-package-smoke.mjs +156 -156
- package/launch-bridge.cjs +236 -236
- package/lzo1x.js +110 -110
- package/mode4-atlas-worker.js +440 -440
- package/mode4-atlas.js +130 -130
- package/package.json +1 -1
- package/port-guard.cjs +285 -285
- package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-hub.js +3698 -3698
- package/scripts/auth-session-smoke.mjs +262 -262
- package/scripts/board-render-ownership-smoke.mjs +85 -85
- package/scripts/codex-model-catalog-smoke.mjs +150 -150
- package/scripts/codex-runtime-home-smoke.mjs +58 -58
- package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
- package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
- package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
- package/scripts/desktop-update-publisher-smoke.mjs +89 -89
- package/scripts/desktop-update-worker-smoke.mjs +72 -72
- package/scripts/desktop-workspace-state-smoke.mjs +81 -81
- package/scripts/drive-installed-update-smoke.mjs +67 -67
- package/scripts/npm-fresh-install-smoke.mjs +216 -216
- package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
- package/scripts/remote-agent-managed-smoke.mjs +318 -318
- package/scripts/remote-agent-package-smoke.mjs +64 -64
- package/scripts/remote-agent-ws-smoke.mjs +269 -269
- package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
- package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
- package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
- package/scripts/remote-frame-ws-smoke.mjs +234 -234
- package/scripts/remote-http-smoke.mjs +592 -592
- package/scripts/remote-hub-identity-smoke.mjs +146 -146
- package/scripts/remote-hub-scale-smoke.mjs +124 -124
- package/scripts/remote-hub-smoke.mjs +699 -699
- package/scripts/remote-input-ws-smoke.mjs +263 -263
- package/scripts/remote-registry-follower-smoke.mjs +815 -815
- package/scripts/setup-tree-sitter-grammars.mjs +59 -59
- package/server.js +7690 -7690
- package/start-bridge.bat +32 -32
- package/start-bridge.sh +81 -81
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10517
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
- package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Core.cx2pre7qz2.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.engfih12kr.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.xfunemn9zi.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.ja94a5ze3i.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.3q05vzviqb.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.re4jjs91d1.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.89etwfquz1.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.aqf4vztjji.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.9b1rucdxvv.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.ssp0n0xf1e.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +23 -23
- package/wwwroot/_headers +40 -40
- package/wwwroot/_redirects +1 -1
- package/wwwroot/index.html +772 -772
- package/wwwroot/js/marketing-tool.js +180 -180
- package/wwwroot/manifest.webmanifest +23 -23
- package/wwwroot/service-worker-assets.js +78 -106
- package/wwwroot/service-worker.js +33 -33
- package/tree-sitter-grammars/README.md +0 -18
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
- 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
|
+
}
|