@mindexec/cli 0.2.465 → 0.2.466
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 +1 -0
- package/codex-runtime.js +521 -15
- package/package.json +35 -33
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/scripts/board-render-ownership-smoke.mjs +85 -0
- package/scripts/codex-sdk-image-runtime-smoke.mjs +121 -0
- package/server.js +17 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +85 -32
- package/wwwroot/_framework/MindExecution.Core.psrddqi3mi.dll +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.98wpeg0ntq.dll → MindExecution.Kernel.emus4f6jja.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.y25y20jkiv.dll → MindExecution.Plugins.Admin.q3gz9b4wp8.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.guko3zr1rb.dll → MindExecution.Plugins.Business.dr4m9mbnc5.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.qmin06apyt.dll → MindExecution.Plugins.Concept.xzrzi61p7e.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.6rw2l9utyr.dll → MindExecution.Plugins.Directory.hcbtf3n8w7.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.72ir717kho.dll → MindExecution.Plugins.PlanMaster.1w09epz56f.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.rkge49vbw6.dll → MindExecution.Plugins.YouTube.7vjbulgfo8.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.q4dgev2e3z.dll → MindExecution.Shared.zj2n1axexl.dll} +0 -0
- package/wwwroot/_framework/MindExecution.Web.z1np8j1qu6.dll +0 -0
- package/wwwroot/_framework/blazor.boot.json +21 -21
- package/wwwroot/index.html +2 -1
- package/wwwroot/service-worker-assets.js +28 -24
- package/wwwroot/service-worker.js +1 -1
- package/wwwroot/_framework/MindExecution.Core.o29fdql6j5.dll +0 -0
- package/wwwroot/_framework/MindExecution.Web.ckdrqv28k2.dll +0 -0
package/README.md
CHANGED
|
@@ -267,6 +267,7 @@ Shell/Codex job ?대깽??
|
|
|
267
267
|
- `GET /api/codex/capabilities` - TypeScript SDK / App Server scaffold / Legacy Exec provider availability
|
|
268
268
|
- `POST /api/codex/thread/start` - start a local Codex thread
|
|
269
269
|
- `POST /api/codex/thread/run` - run a Codex turn. Uses `@openai/codex-sdk` first and falls back to legacy `codex exec`
|
|
270
|
+
- `POST /api/codex/image/run` - generate or edit one image through `@openai/codex-sdk`; invalid login opens the bundled browser sign-in flow and retries once
|
|
270
271
|
- `POST /api/codex/thread/resume` - resume a stored SDK thread by id
|
|
271
272
|
- `POST /api/codex/thread/cancel` - cancel an active turn
|
|
272
273
|
- `GET /api/codex/thread/:threadId/status` - inspect local thread status and compact run metadata
|
package/codex-runtime.js
CHANGED
|
@@ -34,14 +34,50 @@ const API_SANDBOX_BY_SDK_VALUE = Object.freeze({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
|
|
37
|
+
const CODEX_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
38
|
const MAX_LOG_CHARS = 4000;
|
|
38
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;
|
|
39
43
|
const TEMP_DIR = '.ai/codex';
|
|
40
44
|
const CODEX_SOURCE_HOME = path.join(os.homedir(), '.codex');
|
|
41
45
|
const CODEX_RUNTIME_HOME_ENV = 'MINDEXEC_CODEX_HOME';
|
|
42
46
|
const DEFAULT_CODEX_RUNTIME_HOME = path.join(os.homedir(), '.mindexec', 'codex-runtime');
|
|
43
47
|
const CODEX_RUNTIME_CONFIG_MARKER = '# Generated by MindExec LocalBridge for isolated AI node runs.';
|
|
44
48
|
const CODEX_RUNTIME_AUTH_FILES = ['auth.json'];
|
|
49
|
+
const CODEX_TARGET_BY_PLATFORM = Object.freeze({
|
|
50
|
+
'win32:x64': {
|
|
51
|
+
packageName: '@openai/codex-win32-x64',
|
|
52
|
+
targetTriple: 'x86_64-pc-windows-msvc',
|
|
53
|
+
binaryName: 'codex.exe'
|
|
54
|
+
},
|
|
55
|
+
'win32:arm64': {
|
|
56
|
+
packageName: '@openai/codex-win32-arm64',
|
|
57
|
+
targetTriple: 'aarch64-pc-windows-msvc',
|
|
58
|
+
binaryName: 'codex.exe'
|
|
59
|
+
},
|
|
60
|
+
'darwin:x64': {
|
|
61
|
+
packageName: '@openai/codex-darwin-x64',
|
|
62
|
+
targetTriple: 'x86_64-apple-darwin',
|
|
63
|
+
binaryName: 'codex'
|
|
64
|
+
},
|
|
65
|
+
'darwin:arm64': {
|
|
66
|
+
packageName: '@openai/codex-darwin-arm64',
|
|
67
|
+
targetTriple: 'aarch64-apple-darwin',
|
|
68
|
+
binaryName: 'codex'
|
|
69
|
+
},
|
|
70
|
+
'linux:x64': {
|
|
71
|
+
packageName: '@openai/codex-linux-x64',
|
|
72
|
+
targetTriple: 'x86_64-unknown-linux-musl',
|
|
73
|
+
binaryName: 'codex'
|
|
74
|
+
},
|
|
75
|
+
'linux:arm64': {
|
|
76
|
+
packageName: '@openai/codex-linux-arm64',
|
|
77
|
+
targetTriple: 'aarch64-unknown-linux-musl',
|
|
78
|
+
binaryName: 'codex'
|
|
79
|
+
}
|
|
80
|
+
});
|
|
45
81
|
|
|
46
82
|
let cachedSdkModule = null;
|
|
47
83
|
let cachedSdkLoadError = null;
|
|
@@ -103,6 +139,11 @@ async function copyCodexRuntimeFileIfPresent(fileName, runtimeHome) {
|
|
|
103
139
|
const source = path.join(CODEX_SOURCE_HOME, fileName);
|
|
104
140
|
const target = path.join(runtimeHome, fileName);
|
|
105
141
|
try {
|
|
142
|
+
const targetStat = await fs.stat(target).catch(() => null);
|
|
143
|
+
if (targetStat?.isFile()) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
106
147
|
const sourceStat = await fs.stat(source);
|
|
107
148
|
if (!sourceStat.isFile()) {
|
|
108
149
|
return false;
|
|
@@ -158,6 +199,79 @@ function buildCodexChildEnv() {
|
|
|
158
199
|
return env;
|
|
159
200
|
}
|
|
160
201
|
|
|
202
|
+
function resolveBundledCodexExecutable(packageRoot) {
|
|
203
|
+
const target = CODEX_TARGET_BY_PLATFORM[`${process.platform}:${process.arch}`];
|
|
204
|
+
if (!target) {
|
|
205
|
+
throw new Error(`Unsupported Codex login platform: ${process.platform} (${process.arch}).`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return path.join(
|
|
209
|
+
packageRoot,
|
|
210
|
+
'node_modules',
|
|
211
|
+
...target.packageName.split('/'),
|
|
212
|
+
'vendor',
|
|
213
|
+
target.targetTriple,
|
|
214
|
+
'bin',
|
|
215
|
+
target.binaryName);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runBundledCodexCommand(packageRoot, args, timeoutMs = CODEX_LOGIN_TIMEOUT_MS) {
|
|
219
|
+
return new Promise((resolve) => {
|
|
220
|
+
let executablePath;
|
|
221
|
+
try {
|
|
222
|
+
executablePath = resolveBundledCodexExecutable(packageRoot);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
resolve({ success: false, exitCode: -1, stdout: '', stderr: '', error: err?.message || String(err) });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const child = spawn(executablePath, args, {
|
|
229
|
+
env: buildCodexChildEnv(),
|
|
230
|
+
windowsHide: true,
|
|
231
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
232
|
+
});
|
|
233
|
+
let stdout = '';
|
|
234
|
+
let stderr = '';
|
|
235
|
+
let settled = false;
|
|
236
|
+
let timeout = null;
|
|
237
|
+
|
|
238
|
+
const finish = (result) => {
|
|
239
|
+
if (settled) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
settled = true;
|
|
243
|
+
if (timeout) {
|
|
244
|
+
clearTimeout(timeout);
|
|
245
|
+
}
|
|
246
|
+
resolve({
|
|
247
|
+
success: result.exitCode === 0,
|
|
248
|
+
exitCode: result.exitCode,
|
|
249
|
+
stdout: truncateLog(stdout),
|
|
250
|
+
stderr: truncateLog(stderr),
|
|
251
|
+
error: result.error || ''
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
child.stdout?.on('data', chunk => {
|
|
256
|
+
stdout = truncateLog(`${stdout}${chunk}`, MAX_LOG_CHARS);
|
|
257
|
+
});
|
|
258
|
+
child.stderr?.on('data', chunk => {
|
|
259
|
+
stderr = truncateLog(`${stderr}${chunk}`, MAX_LOG_CHARS);
|
|
260
|
+
});
|
|
261
|
+
child.once('error', err => finish({ exitCode: -1, error: err?.message || String(err) }));
|
|
262
|
+
child.once('exit', code => finish({ exitCode: Number.isFinite(Number(code)) ? Number(code) : -1 }));
|
|
263
|
+
|
|
264
|
+
timeout = setTimeout(() => {
|
|
265
|
+
try {
|
|
266
|
+
child.kill();
|
|
267
|
+
} catch {
|
|
268
|
+
// Ignore a process that already exited while the timeout fired.
|
|
269
|
+
}
|
|
270
|
+
finish({ exitCode: -1, error: 'Codex browser login timed out.' });
|
|
271
|
+
}, timeoutMs);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
161
275
|
function normalizeReasoningEffort(value) {
|
|
162
276
|
const normalized = String(value || '').trim().toLowerCase();
|
|
163
277
|
return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(normalized)
|
|
@@ -489,8 +603,8 @@ function serializeThreadOptionsForStatus(options) {
|
|
|
489
603
|
};
|
|
490
604
|
}
|
|
491
605
|
|
|
492
|
-
async function checkSdkAvailability(packageRoot) {
|
|
493
|
-
const sdk = await
|
|
606
|
+
async function checkSdkAvailability(packageRoot, sdkLoader = loadCodexSdk) {
|
|
607
|
+
const sdk = await sdkLoader();
|
|
494
608
|
return {
|
|
495
609
|
kind: PROVIDER_KIND.typeScriptSdk,
|
|
496
610
|
available: Boolean(sdk?.Codex),
|
|
@@ -541,19 +655,291 @@ function extractPrompt(body) {
|
|
|
541
655
|
return prompt;
|
|
542
656
|
}
|
|
543
657
|
|
|
658
|
+
function looksLikeCodexAuthenticationError(value) {
|
|
659
|
+
const text = String(value || '').toLowerCase();
|
|
660
|
+
return text.includes('invalid_refresh_token')
|
|
661
|
+
|| text.includes('invalid refresh token')
|
|
662
|
+
|| text.includes('login required')
|
|
663
|
+
|| text.includes('not logged in')
|
|
664
|
+
|| text.includes('please log out and sign in again')
|
|
665
|
+
|| text.includes('could not parse your authentication token')
|
|
666
|
+
|| text.includes('authrequired')
|
|
667
|
+
|| text.includes('401 unauthorized');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function normalizeImageMimeType(value, fallback = 'image/png') {
|
|
671
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
672
|
+
return /^image\/[a-z0-9.+-]+$/.test(normalized) ? normalized : fallback;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function inferImageMimeType(buffer, fallback = 'image/png') {
|
|
676
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
677
|
+
return fallback;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
|
681
|
+
return 'image/png';
|
|
682
|
+
}
|
|
683
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
684
|
+
return 'image/jpeg';
|
|
685
|
+
}
|
|
686
|
+
if (buffer.slice(0, 3).toString('ascii') === 'GIF') {
|
|
687
|
+
return 'image/gif';
|
|
688
|
+
}
|
|
689
|
+
if (buffer.length >= 12
|
|
690
|
+
&& buffer.slice(0, 4).toString('ascii') === 'RIFF'
|
|
691
|
+
&& buffer.slice(8, 12).toString('ascii') === 'WEBP') {
|
|
692
|
+
return 'image/webp';
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return fallback;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeGeneratedImageBase64(value, hintedMimeType = '') {
|
|
699
|
+
const text = String(value || '').trim();
|
|
700
|
+
if (!text) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const dataUrlMatch = text.match(/^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\r\n]+)$/i);
|
|
705
|
+
const base64 = (dataUrlMatch ? dataUrlMatch[2] : text).replace(/\s+/g, '');
|
|
706
|
+
if (base64.length < 32 || !/^[a-z0-9+/]+={0,2}$/i.test(base64)) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
712
|
+
if (!bytes.length || bytes.length > MAX_GENERATED_IMAGE_BYTES) {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
const mimeType = inferImageMimeType(
|
|
716
|
+
bytes,
|
|
717
|
+
normalizeImageMimeType(dataUrlMatch?.[1] || hintedMimeType));
|
|
718
|
+
return { imageBase64: bytes.toString('base64'), mimeType };
|
|
719
|
+
} catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function extractGeneratedImageFromEvent(value, depth = 0, hintedMimeType = '') {
|
|
725
|
+
if (value == null || depth > 10) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (typeof value === 'string') {
|
|
730
|
+
return value.startsWith('data:image/')
|
|
731
|
+
? normalizeGeneratedImageBase64(value, hintedMimeType)
|
|
732
|
+
: null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (Array.isArray(value)) {
|
|
736
|
+
for (const entry of value) {
|
|
737
|
+
const found = extractGeneratedImageFromEvent(entry, depth + 1, hintedMimeType);
|
|
738
|
+
if (found) {
|
|
739
|
+
return found;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (typeof value !== 'object') {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const localMimeType = normalizeImageMimeType(
|
|
750
|
+
value.mime_type || value.mimeType || value.content_type || value.contentType,
|
|
751
|
+
hintedMimeType || 'image/png');
|
|
752
|
+
for (const key of ['b64_json', 'image_base64', 'imageBase64', 'base64']) {
|
|
753
|
+
if (typeof value[key] === 'string') {
|
|
754
|
+
const found = normalizeGeneratedImageBase64(value[key], localMimeType);
|
|
755
|
+
if (found) {
|
|
756
|
+
return found;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
for (const key of ['result', 'output', 'image', 'images', 'data', 'payload', 'item']) {
|
|
762
|
+
if (value[key] != null) {
|
|
763
|
+
const found = extractGeneratedImageFromEvent(value[key], depth + 1, localMimeType);
|
|
764
|
+
if (found) {
|
|
765
|
+
return found;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function imageExtensionForReference(reference) {
|
|
774
|
+
const fileNameExtension = path.extname(String(reference?.fileName || '')).toLowerCase();
|
|
775
|
+
if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(fileNameExtension)) {
|
|
776
|
+
return fileNameExtension === '.jpeg' ? '.jpg' : fileNameExtension;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const mimeType = normalizeImageMimeType(reference?.mimeType);
|
|
780
|
+
if (mimeType === 'image/jpeg') return '.jpg';
|
|
781
|
+
if (mimeType === 'image/webp') return '.webp';
|
|
782
|
+
if (mimeType === 'image/gif') return '.gif';
|
|
783
|
+
return '.png';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function writeSdkImageReferences(runtimeHome, references) {
|
|
787
|
+
const normalized = Array.isArray(references)
|
|
788
|
+
? references.slice(0, MAX_IMAGE_REFERENCE_COUNT)
|
|
789
|
+
: [];
|
|
790
|
+
if (!normalized.length) {
|
|
791
|
+
return { directory: '', paths: [] };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const root = path.join(runtimeHome, 'image-inputs');
|
|
795
|
+
const directory = path.join(root, crypto.randomUUID());
|
|
796
|
+
await fs.mkdir(directory, { recursive: true });
|
|
797
|
+
const paths = [];
|
|
798
|
+
let totalBytes = 0;
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
802
|
+
const reference = normalized[index];
|
|
803
|
+
const bytes = Buffer.from(String(reference?.base64 || ''), 'base64');
|
|
804
|
+
totalBytes += bytes.length;
|
|
805
|
+
if (!bytes.length || totalBytes > MAX_IMAGE_REFERENCE_BYTES) {
|
|
806
|
+
throw new Error('Codex SDK image references are empty or exceed the 40 MB request limit.');
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const filePath = path.join(directory, `reference-${index + 1}${imageExtensionForReference(reference)}`);
|
|
810
|
+
await fs.writeFile(filePath, bytes);
|
|
811
|
+
paths.push(filePath);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return { directory, paths };
|
|
815
|
+
} catch (err) {
|
|
816
|
+
await fs.rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
817
|
+
throw err;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function cleanupSdkImageReferences(runtimeHome, directory) {
|
|
822
|
+
if (!directory) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const root = path.resolve(runtimeHome, 'image-inputs');
|
|
827
|
+
const target = path.resolve(directory);
|
|
828
|
+
if (!target.startsWith(`${root}${path.sep}`)) {
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
await fs.rm(target, { recursive: true, force: true }).catch(() => {});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function buildSdkImagePrompt(body, referenceCount) {
|
|
835
|
+
const prompt = extractPrompt(body);
|
|
836
|
+
const action = String(body?.action || '').trim().toLowerCase() === 'edit' || referenceCount > 0
|
|
837
|
+
? 'edit'
|
|
838
|
+
: 'generate';
|
|
839
|
+
const size = String(body?.size || '').trim();
|
|
840
|
+
const quality = String(body?.quality || '').trim();
|
|
841
|
+
return [
|
|
842
|
+
action === 'edit'
|
|
843
|
+
? `Use the image generation tool to edit the ${referenceCount} attached reference image(s).`
|
|
844
|
+
: 'Use the image generation tool to create one new image.',
|
|
845
|
+
prompt,
|
|
846
|
+
size ? `Requested output size: ${size}.` : '',
|
|
847
|
+
quality ? `Requested quality: ${quality}.` : '',
|
|
848
|
+
'Return exactly one generated image. Do not answer with instructions or prose.'
|
|
849
|
+
].filter(Boolean).join('\n\n');
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function listGeneratedImageCandidates(directory, minimumMtimeMs, depth = 0) {
|
|
853
|
+
if (depth > 4) {
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const entries = await fs.readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
858
|
+
const candidates = [];
|
|
859
|
+
for (const entry of entries) {
|
|
860
|
+
const fullPath = path.join(directory, entry.name);
|
|
861
|
+
if (entry.isDirectory()) {
|
|
862
|
+
candidates.push(...await listGeneratedImageCandidates(fullPath, minimumMtimeMs, depth + 1));
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (!entry.isFile() || !/\.(png|jpe?g|webp|gif)$/i.test(entry.name)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
870
|
+
if (stat && stat.size > 0 && stat.size <= MAX_GENERATED_IMAGE_BYTES && stat.mtimeMs >= minimumMtimeMs) {
|
|
871
|
+
candidates.push({ fullPath, mtimeMs: stat.mtimeMs });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return candidates;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function recoverSdkGeneratedImage(runtimeHome, startedAtMs) {
|
|
878
|
+
const generatedRoot = path.join(runtimeHome, 'generated_images');
|
|
879
|
+
const minimumMtimeMs = startedAtMs - 15 * 1000;
|
|
880
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
881
|
+
const candidates = await listGeneratedImageCandidates(generatedRoot, minimumMtimeMs);
|
|
882
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
883
|
+
if (candidates[0]) {
|
|
884
|
+
const bytes = await fs.readFile(candidates[0].fullPath);
|
|
885
|
+
return {
|
|
886
|
+
imageBase64: bytes.toString('base64'),
|
|
887
|
+
mimeType: inferImageMimeType(bytes)
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
await new Promise(resolve => setTimeout(resolve, 350));
|
|
891
|
+
}
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
|
|
544
895
|
export function createCodexRuntime(options) {
|
|
545
896
|
const packageRoot = options.packageRoot;
|
|
546
897
|
const resolveWorkingDirectory = options.resolveWorkingDirectory;
|
|
547
898
|
const getCurrentRuntime = options.getCurrentRuntime;
|
|
548
899
|
const getModelCatalog = options.getModelCatalog;
|
|
549
900
|
const log = typeof options.log === 'function' ? options.log : () => {};
|
|
901
|
+
const sdkLoader = typeof options.loadCodexSdk === 'function' ? options.loadCodexSdk : loadCodexSdk;
|
|
902
|
+
const prepareRuntimeHome = typeof options.ensureCodexRuntimeHome === 'function'
|
|
903
|
+
? options.ensureCodexRuntimeHome
|
|
904
|
+
: ensureCodexRuntimeHome;
|
|
905
|
+
const childEnvFactory = typeof options.buildCodexChildEnv === 'function'
|
|
906
|
+
? options.buildCodexChildEnv
|
|
907
|
+
: buildCodexChildEnv;
|
|
550
908
|
|
|
551
909
|
const threads = new Map();
|
|
552
910
|
const activeTurns = new Map();
|
|
911
|
+
let activeLoginPromise = null;
|
|
912
|
+
|
|
913
|
+
async function loginWithBundledCodex() {
|
|
914
|
+
if (typeof options.loginCodex === 'function') {
|
|
915
|
+
return options.loginCodex();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (activeLoginPromise) {
|
|
919
|
+
return activeLoginPromise;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
activeLoginPromise = (async () => {
|
|
923
|
+
await prepareRuntimeHome();
|
|
924
|
+
await runBundledCodexCommand(packageRoot, ['logout'], 15000).catch(() => null);
|
|
925
|
+
log('Codex SDK browser login start', { runtimeHome: resolveCodexRuntimeHome() });
|
|
926
|
+
const loginResult = await runBundledCodexCommand(packageRoot, ['login']);
|
|
927
|
+
log('Codex SDK browser login finish', {
|
|
928
|
+
success: loginResult.success,
|
|
929
|
+
exitCode: loginResult.exitCode,
|
|
930
|
+
error: loginResult.error || ''
|
|
931
|
+
});
|
|
932
|
+
return loginResult;
|
|
933
|
+
})().finally(() => {
|
|
934
|
+
activeLoginPromise = null;
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
return activeLoginPromise;
|
|
938
|
+
}
|
|
553
939
|
|
|
554
940
|
async function getCapabilities(options = {}) {
|
|
555
941
|
const [sdk, legacy, modelCatalog] = await Promise.all([
|
|
556
|
-
checkSdkAvailability(packageRoot),
|
|
942
|
+
checkSdkAvailability(packageRoot, sdkLoader),
|
|
557
943
|
checkLegacyAvailability(),
|
|
558
944
|
getModelCatalog({ forceRefresh: Boolean(options.forceRefresh) })
|
|
559
945
|
]);
|
|
@@ -583,6 +969,7 @@ export function createCodexRuntime(options) {
|
|
|
583
969
|
'POST /api/codex/thread/run',
|
|
584
970
|
'POST /api/codex/thread/resume',
|
|
585
971
|
'POST /api/codex/thread/cancel',
|
|
972
|
+
'POST /api/codex/image/run',
|
|
586
973
|
'GET /api/codex/thread/{threadId}/status'
|
|
587
974
|
],
|
|
588
975
|
runtime: getCurrentRuntime(),
|
|
@@ -598,7 +985,7 @@ export function createCodexRuntime(options) {
|
|
|
598
985
|
|
|
599
986
|
async function resolveProvider(requestedProviderKind) {
|
|
600
987
|
const requested = normalizeProviderKind(requestedProviderKind);
|
|
601
|
-
const sdk = await checkSdkAvailability(packageRoot);
|
|
988
|
+
const sdk = await checkSdkAvailability(packageRoot, sdkLoader);
|
|
602
989
|
const legacy = requested === PROVIDER_KIND.typeScriptSdk
|
|
603
990
|
? null
|
|
604
991
|
: await checkLegacyAvailability();
|
|
@@ -621,10 +1008,10 @@ export function createCodexRuntime(options) {
|
|
|
621
1008
|
const now = new Date().toISOString();
|
|
622
1009
|
|
|
623
1010
|
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
624
|
-
const sdk = await
|
|
625
|
-
await
|
|
1011
|
+
const sdk = await sdkLoader();
|
|
1012
|
+
await prepareRuntimeHome();
|
|
626
1013
|
const codex = new sdk.Codex({
|
|
627
|
-
env:
|
|
1014
|
+
env: childEnvFactory()
|
|
628
1015
|
});
|
|
629
1016
|
const thread = codex.startThread(threadOptions);
|
|
630
1017
|
const localThreadId = `local_${crypto.randomUUID()}`;
|
|
@@ -686,10 +1073,10 @@ export function createCodexRuntime(options) {
|
|
|
686
1073
|
};
|
|
687
1074
|
}
|
|
688
1075
|
|
|
689
|
-
const sdk = await
|
|
690
|
-
await
|
|
1076
|
+
const sdk = await sdkLoader();
|
|
1077
|
+
await prepareRuntimeHome();
|
|
691
1078
|
const codex = new sdk.Codex({
|
|
692
|
-
env:
|
|
1079
|
+
env: childEnvFactory()
|
|
693
1080
|
});
|
|
694
1081
|
const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
|
|
695
1082
|
? requestedThreadId
|
|
@@ -831,7 +1218,7 @@ export function createCodexRuntime(options) {
|
|
|
831
1218
|
const tempDir = path.join(workingDirectory, TEMP_DIR);
|
|
832
1219
|
const schema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
|
|
833
1220
|
let schemaPath = null;
|
|
834
|
-
await
|
|
1221
|
+
await prepareRuntimeHome();
|
|
835
1222
|
const args = [
|
|
836
1223
|
'exec',
|
|
837
1224
|
'-',
|
|
@@ -871,7 +1258,7 @@ export function createCodexRuntime(options) {
|
|
|
871
1258
|
const childResult = await new Promise((resolve) => {
|
|
872
1259
|
const child = spawn('codex', args, {
|
|
873
1260
|
cwd: workingDirectory,
|
|
874
|
-
env:
|
|
1261
|
+
env: childEnvFactory(),
|
|
875
1262
|
windowsHide: true,
|
|
876
1263
|
signal: abortController.signal
|
|
877
1264
|
});
|
|
@@ -975,6 +1362,124 @@ export function createCodexRuntime(options) {
|
|
|
975
1362
|
});
|
|
976
1363
|
}
|
|
977
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: {
|
|
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
|
+
|
|
978
1483
|
async function resumeThread(body = {}) {
|
|
979
1484
|
const providerKind = await resolveProvider(body.providerKind || body.runtime);
|
|
980
1485
|
const threadId = String(body.threadId || '').trim();
|
|
@@ -1000,10 +1505,10 @@ export function createCodexRuntime(options) {
|
|
|
1000
1505
|
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
1001
1506
|
const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
|
|
1002
1507
|
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
1003
|
-
const sdk = await
|
|
1004
|
-
await
|
|
1508
|
+
const sdk = await sdkLoader();
|
|
1509
|
+
await prepareRuntimeHome();
|
|
1005
1510
|
const codex = new sdk.Codex({
|
|
1006
|
-
env:
|
|
1511
|
+
env: childEnvFactory()
|
|
1007
1512
|
});
|
|
1008
1513
|
const thread = codex.resumeThread(threadId, threadOptions);
|
|
1009
1514
|
threads.set(threadId, {
|
|
@@ -1094,6 +1599,7 @@ export function createCodexRuntime(options) {
|
|
|
1094
1599
|
getCapabilities,
|
|
1095
1600
|
startThread,
|
|
1096
1601
|
runThread,
|
|
1602
|
+
runImage,
|
|
1097
1603
|
resumeThread,
|
|
1098
1604
|
cancelThread,
|
|
1099
1605
|
getThreadStatus
|