@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.
Files changed (27) hide show
  1. package/README.md +1 -0
  2. package/codex-runtime.js +521 -15
  3. package/package.json +35 -33
  4. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  5. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  6. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  7. package/scripts/board-render-ownership-smoke.mjs +85 -0
  8. package/scripts/codex-sdk-image-runtime-smoke.mjs +121 -0
  9. package/server.js +17 -0
  10. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -0
  11. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +85 -32
  12. package/wwwroot/_framework/MindExecution.Core.psrddqi3mi.dll +0 -0
  13. package/wwwroot/_framework/{MindExecution.Kernel.98wpeg0ntq.dll → MindExecution.Kernel.emus4f6jja.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Admin.y25y20jkiv.dll → MindExecution.Plugins.Admin.q3gz9b4wp8.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.Business.guko3zr1rb.dll → MindExecution.Plugins.Business.dr4m9mbnc5.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.Concept.qmin06apyt.dll → MindExecution.Plugins.Concept.xzrzi61p7e.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Plugins.Directory.6rw2l9utyr.dll → MindExecution.Plugins.Directory.hcbtf3n8w7.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.72ir717kho.dll → MindExecution.Plugins.PlanMaster.1w09epz56f.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.rkge49vbw6.dll → MindExecution.Plugins.YouTube.7vjbulgfo8.dll} +0 -0
  20. package/wwwroot/_framework/{MindExecution.Shared.q4dgev2e3z.dll → MindExecution.Shared.zj2n1axexl.dll} +0 -0
  21. package/wwwroot/_framework/MindExecution.Web.z1np8j1qu6.dll +0 -0
  22. package/wwwroot/_framework/blazor.boot.json +21 -21
  23. package/wwwroot/index.html +2 -1
  24. package/wwwroot/service-worker-assets.js +28 -24
  25. package/wwwroot/service-worker.js +1 -1
  26. package/wwwroot/_framework/MindExecution.Core.o29fdql6j5.dll +0 -0
  27. 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 loadCodexSdk();
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 loadCodexSdk();
625
- await ensureCodexRuntimeHome();
1011
+ const sdk = await sdkLoader();
1012
+ await prepareRuntimeHome();
626
1013
  const codex = new sdk.Codex({
627
- env: buildCodexChildEnv()
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 loadCodexSdk();
690
- await ensureCodexRuntimeHome();
1076
+ const sdk = await sdkLoader();
1077
+ await prepareRuntimeHome();
691
1078
  const codex = new sdk.Codex({
692
- env: buildCodexChildEnv()
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 ensureCodexRuntimeHome();
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: buildCodexChildEnv(),
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 loadCodexSdk();
1004
- await ensureCodexRuntimeHome();
1508
+ const sdk = await sdkLoader();
1509
+ await prepareRuntimeHome();
1005
1510
  const codex = new sdk.Codex({
1006
- env: buildCodexChildEnv()
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