@mindexec/cli 0.2.436 → 0.2.438

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 (22) hide show
  1. package/codex-model-catalog.js +202 -0
  2. package/codex-runtime.js +5 -2
  3. package/package.json +6 -3
  4. package/scripts/codex-model-catalog-smoke.mjs +150 -0
  5. package/server.js +35 -69
  6. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +32 -28
  7. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +2 -2
  8. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +153 -233
  9. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +26 -5
  10. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -0
  11. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +40 -23
  12. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +10 -5
  13. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +10 -1
  14. package/wwwroot/_framework/{MindExecution.Plugins.Concept.7uhld08zkc.dll → MindExecution.Plugins.Concept.cvmglrxsjb.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.y1sj1ifw0z.dll → MindExecution.Plugins.PlanMaster.8hmc6ps1ux.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.ij0uk05x1o.dll → MindExecution.Plugins.YouTube.xb9sou9ox0.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Shared.1h72etbxec.dll → MindExecution.Shared.pqab76cjb6.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Web.18xe2efznc.dll → MindExecution.Web.mc24u4bzol.dll} +0 -0
  19. package/wwwroot/_framework/blazor.boot.json +11 -11
  20. package/wwwroot/index.html +3 -2
  21. package/wwwroot/service-worker-assets.js +24 -20
  22. package/wwwroot/service-worker.js +1 -1
@@ -0,0 +1,202 @@
1
+ import path from 'path';
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+
5
+ const execAsync = promisify(exec);
6
+
7
+ const DEFAULT_CACHE_TTL_MS = 15 * 60 * 1000;
8
+ const DEFAULT_FAILURE_CACHE_TTL_MS = 60 * 1000;
9
+ const DEFAULT_TIMEOUT_MS = 20 * 1000;
10
+ const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
11
+
12
+ function getPathEnvironmentKey(env) {
13
+ return Object.keys(env).find(key => key.toLowerCase() === 'path') || 'PATH';
14
+ }
15
+
16
+ function isPackageBinPath(value) {
17
+ const normalized = path.normalize(String(value || '').trim()).replace(/[\\/]+$/, '');
18
+ return /[\\/]node_modules[\\/]\.bin$/i.test(normalized);
19
+ }
20
+
21
+ /**
22
+ * The LocalBridge npm package carries a version-pinned Codex CLI through
23
+ * @openai/codex-sdk. That CLI is the execution runtime, but it must not shadow
24
+ * the user's newer installed Codex CLI when resolving the authoritative model
25
+ * catalog.
26
+ */
27
+ export function buildExternalCodexCatalogEnv(sourceEnv = process.env) {
28
+ const env = { ...sourceEnv };
29
+ const pathKey = getPathEnvironmentKey(env);
30
+ const pathValue = String(env[pathKey] || '');
31
+ env[pathKey] = pathValue
32
+ .split(path.delimiter)
33
+ .map(item => item.trim())
34
+ .filter(Boolean)
35
+ .filter(item => !isPackageBinPath(item))
36
+ .join(path.delimiter);
37
+ return env;
38
+ }
39
+
40
+ export function normalizeCodexDebugModel(item) {
41
+ if (!item || typeof item !== 'object') {
42
+ return null;
43
+ }
44
+
45
+ const id = String(item.slug || '').trim();
46
+ if (!id || String(item.visibility || '').toLowerCase() !== 'list') {
47
+ return null;
48
+ }
49
+
50
+ return {
51
+ id,
52
+ displayName: String(item.display_name || id).trim(),
53
+ description: String(item.description || '').trim(),
54
+ priority: Number.isFinite(Number(item.priority)) ? Number(item.priority) : 999,
55
+ defaultReasoningLevel: String(item.default_reasoning_level || '').trim(),
56
+ supportedReasoningLevels: Array.isArray(item.supported_reasoning_levels)
57
+ ? item.supported_reasoning_levels
58
+ .map(level => String(level?.effort || '').trim())
59
+ .filter(Boolean)
60
+ : [],
61
+ supportedInApi: Boolean(item.supported_in_api)
62
+ };
63
+ }
64
+
65
+ function parseCodexDebugModels(stdout) {
66
+ const parsed = JSON.parse(String(stdout || '{}'));
67
+ const models = Array.isArray(parsed.models)
68
+ ? parsed.models
69
+ .map(normalizeCodexDebugModel)
70
+ .filter(Boolean)
71
+ .sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id))
72
+ : [];
73
+
74
+ if (models.length === 0) {
75
+ throw new Error('Codex live model catalog returned no list-visible models.');
76
+ }
77
+
78
+ return models;
79
+ }
80
+
81
+ function formatAttemptError(error) {
82
+ const message = String(error?.stderr || error?.message || error || 'unknown-error').trim();
83
+ return message.length > 1200 ? `${message.slice(0, 1200)}...` : message;
84
+ }
85
+
86
+ /**
87
+ * Contract:
88
+ * - `forceRefresh` always bypasses the completed-result TTL.
89
+ * - Concurrent refreshes share one authoritative CLI request sequence.
90
+ * - The user's external Codex CLI is tried before the SDK-pinned package CLI.
91
+ * - A failed refresh never replaces a previously verified non-empty catalog.
92
+ */
93
+ export function createCodexModelCatalog(options = {}) {
94
+ const execute = options.execute || execAsync;
95
+ const sourceEnv = options.env || process.env;
96
+ const packageRoot = options.packageRoot || process.cwd();
97
+ const cacheTtlMs = Number(options.cacheTtlMs) > 0
98
+ ? Number(options.cacheTtlMs)
99
+ : DEFAULT_CACHE_TTL_MS;
100
+ const failureCacheTtlMs = Number(options.failureCacheTtlMs) > 0
101
+ ? Number(options.failureCacheTtlMs)
102
+ : DEFAULT_FAILURE_CACHE_TTL_MS;
103
+ const timeoutMs = Number(options.timeoutMs) > 0
104
+ ? Number(options.timeoutMs)
105
+ : DEFAULT_TIMEOUT_MS;
106
+ const now = typeof options.now === 'function' ? options.now : Date.now;
107
+
108
+ let cachedCatalog = null;
109
+ let cachedCatalogExpiresAt = 0;
110
+ let refreshPromise = null;
111
+
112
+ async function refresh() {
113
+ const attemptedAtMs = now();
114
+ const attemptedAt = new Date(attemptedAtMs).toISOString();
115
+ const attempts = [
116
+ {
117
+ commandSource: 'external',
118
+ command: 'codex debug models',
119
+ env: buildExternalCodexCatalogEnv(sourceEnv)
120
+ },
121
+ {
122
+ commandSource: 'package-local',
123
+ // Older SDK-pinned CLIs may not understand a newer configured
124
+ // reasoning level. The catalog itself is independent of this
125
+ // preference, so use a broadly supported value for fallback.
126
+ command: 'codex debug models --config model_reasoning_effort=high',
127
+ env: { ...sourceEnv }
128
+ }
129
+ ];
130
+ const attemptErrors = [];
131
+
132
+ for (const attempt of attempts) {
133
+ try {
134
+ const { stdout } = await execute(attempt.command, {
135
+ cwd: packageRoot,
136
+ env: attempt.env,
137
+ timeout: timeoutMs,
138
+ maxBuffer: DEFAULT_MAX_BUFFER,
139
+ windowsHide: true
140
+ });
141
+ const models = parseCodexDebugModels(stdout);
142
+ cachedCatalog = {
143
+ source: 'codex debug models',
144
+ commandSource: attempt.commandSource,
145
+ updatedAt: attemptedAt,
146
+ refreshAttemptedAt: attemptedAt,
147
+ stale: false,
148
+ error: null,
149
+ models
150
+ };
151
+ cachedCatalogExpiresAt = attemptedAtMs + cacheTtlMs;
152
+ return cachedCatalog;
153
+ } catch (error) {
154
+ attemptErrors.push(`${attempt.commandSource}: ${formatAttemptError(error)}`);
155
+ }
156
+ }
157
+
158
+ const error = attemptErrors.join(' | ');
159
+ cachedCatalogExpiresAt = attemptedAtMs + failureCacheTtlMs;
160
+ if (cachedCatalog?.models?.length > 0) {
161
+ cachedCatalog = {
162
+ ...cachedCatalog,
163
+ refreshAttemptedAt: attemptedAt,
164
+ stale: true,
165
+ error
166
+ };
167
+ return cachedCatalog;
168
+ }
169
+
170
+ cachedCatalog = {
171
+ source: 'codex debug models',
172
+ commandSource: null,
173
+ updatedAt: null,
174
+ refreshAttemptedAt: attemptedAt,
175
+ stale: true,
176
+ error,
177
+ models: []
178
+ };
179
+ return cachedCatalog;
180
+ }
181
+
182
+ async function getCatalog(requestOptions = {}) {
183
+ const forceRefresh = Boolean(requestOptions.forceRefresh);
184
+ if (!forceRefresh && cachedCatalog && cachedCatalogExpiresAt > now()) {
185
+ return cachedCatalog;
186
+ }
187
+
188
+ if (refreshPromise) {
189
+ return refreshPromise;
190
+ }
191
+
192
+ refreshPromise = refresh().finally(() => {
193
+ refreshPromise = null;
194
+ });
195
+ return refreshPromise;
196
+ }
197
+
198
+ return {
199
+ getCatalog,
200
+ peek: () => cachedCatalog
201
+ };
202
+ }
package/codex-runtime.js CHANGED
@@ -558,11 +558,11 @@ export function createCodexRuntime(options) {
558
558
  const threads = new Map();
559
559
  const activeTurns = new Map();
560
560
 
561
- async function getCapabilities() {
561
+ async function getCapabilities(options = {}) {
562
562
  const [sdk, legacy, modelCatalog] = await Promise.all([
563
563
  checkSdkAvailability(packageRoot),
564
564
  checkLegacyAvailability(),
565
- getModelCatalog()
565
+ getModelCatalog({ forceRefresh: Boolean(options.forceRefresh) })
566
566
  ]);
567
567
  const preferredProviderKind = sdk.available
568
568
  ? PROVIDER_KIND.typeScriptSdk
@@ -595,7 +595,10 @@ export function createCodexRuntime(options) {
595
595
  runtime: getCurrentRuntime(),
596
596
  models: modelCatalog.models || [],
597
597
  modelCatalogSource: modelCatalog.source,
598
+ modelCatalogCommandSource: modelCatalog.commandSource || null,
598
599
  modelCatalogUpdatedAt: modelCatalog.updatedAt,
600
+ modelCatalogRefreshAttemptedAt: modelCatalog.refreshAttemptedAt || null,
601
+ modelCatalogStale: modelCatalog.stale === true,
599
602
  modelCatalogError: modelCatalog.error || null
600
603
  };
601
604
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.436",
3
+ "version": "0.2.438",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "server.js",
12
12
  "remote-hub.js",
13
13
  "codex-runtime.js",
14
+ "codex-model-catalog.js",
14
15
  "port-guard.cjs",
15
16
  "remote-fast/",
16
17
  "wwwroot/",
@@ -21,7 +22,9 @@
21
22
  "scripts": {
22
23
  "start": "node launch-bridge.cjs",
23
24
  "dev": "node launch-bridge.cjs --watch",
24
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
25
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
26
+ "test:codex-model-catalog": "node scripts/codex-model-catalog-smoke.mjs",
27
+ "test:codex-model-catalog:live": "node scripts/codex-model-catalog-smoke.mjs --live",
25
28
  "test:auth": "node scripts/auth-session-smoke.mjs",
26
29
  "test:remote": "node scripts/remote-hub-smoke.mjs",
27
30
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
@@ -57,7 +60,7 @@
57
60
  },
58
61
  "dependencies": {
59
62
  "@mindexec/remote": "^0.1.17",
60
- "@openai/codex-sdk": "^0.137.0",
63
+ "@openai/codex-sdk": "^0.144.1",
61
64
  "chokidar": "^3.6.0",
62
65
  "cors": "^2.8.5",
63
66
  "express": "^4.18.2",
@@ -0,0 +1,150 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import {
5
+ buildExternalCodexCatalogEnv,
6
+ createCodexModelCatalog
7
+ } from '../codex-model-catalog.js';
8
+
9
+ function catalogJson(models) {
10
+ return JSON.stringify({ models });
11
+ }
12
+
13
+ function listed(slug, priority, displayName = slug) {
14
+ return {
15
+ slug,
16
+ display_name: displayName,
17
+ visibility: 'list',
18
+ priority,
19
+ supported_in_api: true,
20
+ supported_reasoning_levels: [{ effort: 'medium' }, { effort: 'high' }]
21
+ };
22
+ }
23
+
24
+ async function testExternalPathWinsAndForceRefreshBypassesCache() {
25
+ const packageBin = path.join('C:', 'tmp', 'package', 'node_modules', '.bin');
26
+ const globalBin = path.join('C:', 'tools', 'codex');
27
+ const env = { PATH: [packageBin, globalBin].join(path.delimiter) };
28
+ const externalEnv = buildExternalCodexCatalogEnv(env);
29
+ assert.equal(externalEnv.PATH.includes('node_modules'), false);
30
+ assert.equal(externalEnv.PATH.includes(globalBin), true);
31
+
32
+ let calls = 0;
33
+ const execute = async (command, options) => {
34
+ calls += 1;
35
+ assert.equal(command, 'codex debug models');
36
+ assert.equal(String(options.env.PATH).includes('node_modules'), false);
37
+ const current = calls === 1 ? 'gpt-current-a' : 'gpt-current-b';
38
+ return {
39
+ stdout: catalogJson([
40
+ listed('hidden-model', 0),
41
+ listed(current, 1, current.toUpperCase()),
42
+ { ...listed('not-listed', 2), visibility: 'hide' }
43
+ ]),
44
+ stderr: ''
45
+ };
46
+ };
47
+ const catalog = createCodexModelCatalog({ execute, env, packageRoot: process.cwd() });
48
+
49
+ const first = await catalog.getCatalog();
50
+ const cached = await catalog.getCatalog();
51
+ assert.equal(calls, 1);
52
+ assert.strictEqual(cached, first);
53
+ assert.deepEqual(first.models.map(model => model.id), ['hidden-model', 'gpt-current-a']);
54
+ assert.equal(first.commandSource, 'external');
55
+ assert.equal(first.stale, false);
56
+
57
+ const refreshed = await catalog.getCatalog({ forceRefresh: true });
58
+ assert.equal(calls, 2);
59
+ assert.deepEqual(refreshed.models.map(model => model.id), ['hidden-model', 'gpt-current-b']);
60
+ }
61
+
62
+ async function testConcurrentRefreshIsSingleFlight() {
63
+ let calls = 0;
64
+ let release;
65
+ const gate = new Promise(resolve => {
66
+ release = resolve;
67
+ });
68
+ const catalog = createCodexModelCatalog({
69
+ execute: async () => {
70
+ calls += 1;
71
+ await gate;
72
+ return { stdout: catalogJson([listed('gpt-concurrent', 1)]), stderr: '' };
73
+ }
74
+ });
75
+
76
+ const first = catalog.getCatalog({ forceRefresh: true });
77
+ const second = catalog.getCatalog({ forceRefresh: true });
78
+ await Promise.resolve();
79
+ assert.equal(calls, 1);
80
+ release();
81
+ const [firstResult, secondResult] = await Promise.all([first, second]);
82
+ assert.strictEqual(firstResult, secondResult);
83
+ assert.equal(calls, 1);
84
+ }
85
+
86
+ async function testPackageFallbackNormalizesNewerConfigPreference() {
87
+ const commands = [];
88
+ const catalog = createCodexModelCatalog({
89
+ execute: async command => {
90
+ commands.push(command);
91
+ if (commands.length === 1) {
92
+ throw new Error('external Codex CLI unavailable');
93
+ }
94
+
95
+ return { stdout: catalogJson([listed('gpt-package-fallback', 1)]), stderr: '' };
96
+ }
97
+ });
98
+
99
+ const result = await catalog.getCatalog({ forceRefresh: true });
100
+ assert.equal(commands.length, 2);
101
+ assert.match(commands[1], /model_reasoning_effort=high/);
102
+ assert.equal(result.commandSource, 'package-local');
103
+ assert.deepEqual(result.models.map(model => model.id), ['gpt-package-fallback']);
104
+ }
105
+
106
+ async function testFailedRefreshPreservesLastVerifiedCatalog() {
107
+ let shouldFail = false;
108
+ const catalog = createCodexModelCatalog({
109
+ execute: async () => {
110
+ if (shouldFail) {
111
+ throw new Error('temporary catalog failure');
112
+ }
113
+
114
+ return { stdout: catalogJson([listed('gpt-last-known-good', 1)]), stderr: '' };
115
+ }
116
+ });
117
+
118
+ const verified = await catalog.getCatalog({ forceRefresh: true });
119
+ shouldFail = true;
120
+ const stale = await catalog.getCatalog({ forceRefresh: true });
121
+ assert.deepEqual(stale.models, verified.models);
122
+ assert.equal(stale.updatedAt, verified.updatedAt);
123
+ assert.equal(stale.stale, true);
124
+ assert.match(stale.error, /external:.*temporary catalog failure/);
125
+ assert.match(stale.error, /package-local:.*temporary catalog failure/);
126
+ }
127
+
128
+ async function runDeterministicSmoke() {
129
+ await testExternalPathWinsAndForceRefreshBypassesCache();
130
+ await testConcurrentRefreshIsSingleFlight();
131
+ await testPackageFallbackNormalizesNewerConfigPreference();
132
+ await testFailedRefreshPreservesLastVerifiedCatalog();
133
+ console.log('[codex-model-catalog-smoke] deterministic catalog contracts passed');
134
+ }
135
+
136
+ async function runLiveSmoke() {
137
+ const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
138
+ const catalog = createCodexModelCatalog({ packageRoot: path.resolve(scriptDirectory, '..') });
139
+ const result = await catalog.getCatalog({ forceRefresh: true });
140
+ assert.ok(result.models.length > 0, result.error || 'expected a non-empty live Codex catalog');
141
+ assert.equal(result.commandSource, 'external');
142
+ assert.equal(result.stale, false);
143
+ console.log(`[codex-model-catalog-smoke] live ${result.source} (${result.commandSource}): ${result.models.map(model => model.id).join(', ')}`);
144
+ }
145
+
146
+ if (process.argv.includes('--live')) {
147
+ await runLiveSmoke();
148
+ } else {
149
+ await runDeterministicSmoke();
150
+ }
package/server.js CHANGED
@@ -22,6 +22,7 @@ import chokidar from 'chokidar';
22
22
  import Parser from 'web-tree-sitter';
23
23
  import { fileURLToPath } from 'url';
24
24
  import { createCodexRuntime } from './codex-runtime.js';
25
+ import { createCodexModelCatalog } from './codex-model-catalog.js';
25
26
  import { createRemoteHub } from './remote-hub.js';
26
27
  import portGuard from './port-guard.cjs';
27
28
 
@@ -3167,14 +3168,14 @@ function extractCodexOutputPath(command) {
3167
3168
  return match[2] || match[3] || match[4] || null;
3168
3169
  }
3169
3170
 
3170
- const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
3171
- const CODEX_MODEL_CATALOG_TTL_MS = 15 * 60 * 1000;
3172
- let cachedCodexConfig = null;
3173
- let cachedCodexConfigMtimeMs = -1;
3174
- let cachedCodexModelCatalog = null;
3175
- let cachedCodexModelCatalogExpiresAt = 0;
3171
+ const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
3172
+ let cachedCodexConfig = null;
3173
+ let cachedCodexConfigMtimeMs = -1;
3176
3174
  let lastCodexExecRuntime = null;
3177
3175
  let codexRuntime = null;
3176
+ const codexModelCatalog = createCodexModelCatalog({
3177
+ packageRoot: BRIDGE_ROOT
3178
+ });
3178
3179
 
3179
3180
  function readCodexConfig(options = {}) {
3180
3181
  const forceRefresh = Boolean(options.forceRefresh);
@@ -3281,68 +3282,17 @@ function formatCodexRuntime(runtime) {
3281
3282
  return parts.join(' ??');
3282
3283
  }
3283
3284
 
3284
- function normalizeCodexDebugModel(item) {
3285
- if (!item || typeof item !== 'object') {
3286
- return null;
3287
- }
3288
-
3289
- const id = String(item.slug || '').trim();
3290
- if (!id || String(item.visibility || '').toLowerCase() !== 'list') {
3291
- return null;
3292
- }
3293
-
3294
- return {
3295
- id,
3296
- displayName: String(item.display_name || id).trim(),
3297
- description: String(item.description || '').trim(),
3298
- priority: Number.isFinite(Number(item.priority)) ? Number(item.priority) : 999,
3299
- defaultReasoningLevel: String(item.default_reasoning_level || '').trim(),
3300
- supportedReasoningLevels: Array.isArray(item.supported_reasoning_levels)
3301
- ? item.supported_reasoning_levels
3302
- .map(level => String(level?.effort || '').trim())
3303
- .filter(Boolean)
3304
- : [],
3305
- supportedInApi: Boolean(item.supported_in_api)
3306
- };
3307
- }
3308
-
3309
3285
  async function getCodexModelCatalog(options = {}) {
3310
- const now = Date.now();
3311
- if (!options.forceRefresh && cachedCodexModelCatalog && cachedCodexModelCatalogExpiresAt > now) {
3312
- return cachedCodexModelCatalog;
3313
- }
3314
-
3315
- try {
3316
- const { stdout } = await execAsync('codex debug models', {
3317
- timeout: 10000,
3318
- maxBuffer: 16 * 1024 * 1024,
3319
- windowsHide: true
3320
- });
3321
- const parsed = JSON.parse(String(stdout || '{}'));
3322
- const models = Array.isArray(parsed.models)
3323
- ? parsed.models
3324
- .map(normalizeCodexDebugModel)
3325
- .filter(Boolean)
3326
- .sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id))
3327
- : [];
3328
-
3329
- cachedCodexModelCatalog = {
3330
- source: 'codex debug models',
3331
- updatedAt: new Date().toISOString(),
3332
- models
3333
- };
3334
- cachedCodexModelCatalogExpiresAt = now + CODEX_MODEL_CATALOG_TTL_MS;
3335
- return cachedCodexModelCatalog;
3336
- } catch (err) {
3337
- cachedCodexModelCatalog = {
3338
- source: 'codex debug models',
3339
- updatedAt: new Date().toISOString(),
3340
- error: err?.message || String(err),
3341
- models: []
3342
- };
3343
- cachedCodexModelCatalogExpiresAt = now + Math.min(CODEX_MODEL_CATALOG_TTL_MS, 60 * 1000);
3344
- return cachedCodexModelCatalog;
3286
+ return await codexModelCatalog.getCatalog(options);
3287
+ }
3288
+
3289
+ function shouldForceCodexCatalogRefresh(value, defaultValue = true) {
3290
+ const normalized = String(value ?? '').trim().toLowerCase();
3291
+ if (!normalized) {
3292
+ return defaultValue;
3345
3293
  }
3294
+
3295
+ return !['0', 'false', 'no', 'off', 'cache', 'cached'].includes(normalized);
3346
3296
  }
3347
3297
 
3348
3298
  function getCodexRuntime() {
@@ -12426,7 +12376,10 @@ app.get('/api/status', async (req, res) => {
12426
12376
  res.setHeader('Pragma', 'no-cache');
12427
12377
  res.setHeader('Expires', '0');
12428
12378
 
12429
- const forceRefresh = ['1', 'true', 'yes', 'force'].includes(String(req.query.refresh || '').trim().toLowerCase());
12379
+ // `/api/status` is the LocalBridge connection handshake used by browser and
12380
+ // app clients. Refresh the authoritative Codex catalog for every handshake;
12381
+ // callers doing a non-connection diagnostic poll can opt into `refresh=cache`.
12382
+ const forceRefresh = shouldForceCodexCatalogRefresh(req.query.refresh, true);
12430
12383
  const currentCodexRuntime = getCurrentCodexRuntime({
12431
12384
  forceRefresh,
12432
12385
  forceConfigRefresh: forceRefresh
@@ -12478,7 +12431,10 @@ app.get('/api/status', async (req, res) => {
12478
12431
  preferredProviderKind: codexCapabilities.preferredProviderKind,
12479
12432
  availableModels: codexModelCatalog.models,
12480
12433
  modelCatalogSource: codexModelCatalog.source,
12434
+ modelCatalogCommandSource: codexModelCatalog.commandSource,
12481
12435
  modelCatalogUpdatedAt: codexModelCatalog.updatedAt,
12436
+ modelCatalogRefreshAttemptedAt: codexModelCatalog.refreshAttemptedAt,
12437
+ modelCatalogStale: codexModelCatalog.stale === true,
12482
12438
  modelCatalogError: codexModelCatalog.error
12483
12439
  },
12484
12440
  codex: {
@@ -12865,7 +12821,9 @@ app.post('/api/remote/devices/:deviceId/live/stop', (req, res) => {
12865
12821
 
12866
12822
  app.get('/api/codex/capabilities', async (req, res) => {
12867
12823
  try {
12868
- res.json(await getCodexRuntime().getCapabilities());
12824
+ res.setHeader('Cache-Control', 'no-store, no-cache, max-age=0, must-revalidate');
12825
+ const forceRefresh = shouldForceCodexCatalogRefresh(req.query.refresh, true);
12826
+ res.json(await getCodexRuntime().getCapabilities({ forceRefresh }));
12869
12827
  } catch (err) {
12870
12828
  console.error('[Codex/Capabilities] Error:', err);
12871
12829
  res.status(500).json({
@@ -14285,13 +14243,21 @@ async function startBridgeServer() {
14285
14243
  logError('workspace', 'failed to initialize .mindexec layout.', layoutErr);
14286
14244
  }
14287
14245
 
14288
- const startupCodexRuntime = getCurrentCodexRuntime();
14246
+ const startupCodexRuntime = getCurrentCodexRuntime({
14247
+ forceRefresh: true,
14248
+ forceConfigRefresh: true
14249
+ });
14250
+ const startupCodexCatalog = await getCodexModelCatalog({ forceRefresh: true });
14251
+ if (startupCodexCatalog.error) {
14252
+ logWarn('codex', `model catalog refresh issue ${shortenText(startupCodexCatalog.error, 320)}`);
14253
+ }
14289
14254
  const remoteHubStatus = remoteHub.getStatus({ includeSecrets: true });
14290
14255
  logSection('MindExec Local Bridge', [
14291
14256
  formatKeyValue('port', tone(PORT, 'accent')),
14292
14257
  formatKeyValue('workspace', tone(normalizePathForClient(workspacePath), 'path')),
14293
14258
  formatKeyValue('status', tone('ready', 'success')),
14294
14259
  formatKeyValue('exec', tone(formatCodexRuntime(startupCodexRuntime), 'accent')),
14260
+ formatKeyValue('codexModels', startupCodexCatalog.models?.length || 0),
14295
14261
  formatKeyValue('remote', remoteHubStatus.started
14296
14262
  ? tone(`tcp://${remoteHubStatus.host}:${remoteHubStatus.port}`, 'accent')
14297
14263
  : tone(remoteHubStatus.enabled ? 'failed' : 'disabled', 'warn')),