@mindexec/cli 0.2.437 → 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.
@@ -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.437",
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')),
@@ -6058,10 +6058,6 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6058
6058
  (panEndedThisFrame && isPanSmoothingActive !== true);
6059
6059
  if (shouldFinalizeMotionThisFrame) {
6060
6060
  this._lastMotionEndAt = frameStart;
6061
- if (this._cursorDomDeferredForMotion === true) {
6062
- updateCursorDomElement(this);
6063
- this._cursorDomDeferredForMotion = false;
6064
- }
6065
6061
  if (zoomEndedThisFrame === true) {
6066
6062
  const shouldReconcileLocalSnapGridAfterZoom = !!(
6067
6063
  this._isLocalSnapGridThemeEnabled?.() === true &&
@@ -6847,14 +6843,10 @@ ${summaryLines.map(line => `<div>${escapeNodeFrameDebugHtml(line)}</div>`).join(
6847
6843
 
6848
6844
  // 5. Cursor DOM position
6849
6845
  // ----------------------------------------------------------------
6850
- if (isCameraNavigationOnlyInteractiveFrame === true ||
6851
- this.isPanning === true ||
6852
- this.isZooming === true ||
6853
- isCameraMoving === true) {
6854
- this._cursorDomDeferredForMotion = true;
6855
- } else {
6856
- updateCursorDomElement(this);
6857
- }
6846
+ // The creation cursor is a world-space anchor. Project it from
6847
+ // the canonical camera on every rendered motion frame so it
6848
+ // travels with the board while pan/zoom lerp is settling.
6849
+ updateCursorDomElement(this);
6858
6850
 
6859
6851
  // 6. Theme Animation
6860
6852
  // ----------------------------------------------------------------
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "mainAssemblyName": "MindExecution.Web",
3
3
  "resources": {
4
- "hash": "sha256-UWj0ijDRqn0AbF83B8IcEJMCKPtxWwQ27Ud2G/Zu03k=",
4
+ "hash": "sha256-cyYstxxu9/Yn3zgm26aHsm55DzzVHsZnB734jEONmCw=",
5
5
  "fingerprinting": {
6
6
  "Google.Protobuf.9h59ukbel7.dll": "Google.Protobuf.dll",
7
7
  "Markdig.d1j7v41cl1.dll": "Markdig.dll",
@@ -126,12 +126,12 @@
126
126
  "MindExecution.Kernel.75hxk08tt0.dll": "MindExecution.Kernel.dll",
127
127
  "MindExecution.Plugins.Admin.0zkmk8wahq.dll": "MindExecution.Plugins.Admin.dll",
128
128
  "MindExecution.Plugins.Business.vyw3769hrw.dll": "MindExecution.Plugins.Business.dll",
129
- "MindExecution.Plugins.Concept.vbnlpkxcoo.dll": "MindExecution.Plugins.Concept.dll",
129
+ "MindExecution.Plugins.Concept.cvmglrxsjb.dll": "MindExecution.Plugins.Concept.dll",
130
130
  "MindExecution.Plugins.Directory.jtw9q1dm9n.dll": "MindExecution.Plugins.Directory.dll",
131
- "MindExecution.Plugins.PlanMaster.k3xu6iqktj.dll": "MindExecution.Plugins.PlanMaster.dll",
132
- "MindExecution.Plugins.YouTube.dqlboez3y6.dll": "MindExecution.Plugins.YouTube.dll",
133
- "MindExecution.Shared.6xss0iwsn1.dll": "MindExecution.Shared.dll",
134
- "MindExecution.Web.22pax8j6l6.dll": "MindExecution.Web.dll",
131
+ "MindExecution.Plugins.PlanMaster.8hmc6ps1ux.dll": "MindExecution.Plugins.PlanMaster.dll",
132
+ "MindExecution.Plugins.YouTube.xb9sou9ox0.dll": "MindExecution.Plugins.YouTube.dll",
133
+ "MindExecution.Shared.pqab76cjb6.dll": "MindExecution.Shared.dll",
134
+ "MindExecution.Web.mc24u4bzol.dll": "MindExecution.Web.dll",
135
135
  "dotnet.native.566r55w3xu.js": "dotnet.native.js",
136
136
  "dotnet.native.x6q5aixc38.wasm": "dotnet.native.wasm",
137
137
  "dotnet.js": "dotnet.js",
@@ -279,15 +279,15 @@
279
279
  "MindExecution.Core.808jj176e1.dll": "sha256-uMmP6rEgsbuwnsZ6Cnxrpmzio4usBtoJERfkOvz25iY=",
280
280
  "MindExecution.Kernel.75hxk08tt0.dll": "sha256-9db04cSFp4z4iblswVb0UWgpjeluVqZUa7g27Oj3p40=",
281
281
  "MindExecution.Plugins.Business.vyw3769hrw.dll": "sha256-WSc1w7WNOhnmzD3DcIgg4PCYzHeeU5rc7KK7eLgsu+M=",
282
- "MindExecution.Plugins.Concept.vbnlpkxcoo.dll": "sha256-pf2GETS/+rzZTxBpviTD2zwBY8qAF6NP0sv1WB/pFx8=",
283
- "MindExecution.Plugins.PlanMaster.k3xu6iqktj.dll": "sha256-EPH8PepplQ1KhRkxCphNWRxoNuI7TjKkJJKSs7Xg7nU=",
284
- "MindExecution.Shared.6xss0iwsn1.dll": "sha256-lva/KIyQLLikvw2qN3MdYJ4feScF038KSxVvAfFYhXA=",
285
- "MindExecution.Web.22pax8j6l6.dll": "sha256-Wi5LDm1s08JJMwVLzG2qB4SDNXbvDpdhsHn8o61JQoc="
282
+ "MindExecution.Plugins.Concept.cvmglrxsjb.dll": "sha256-yGhEvwMzCTsmRB6D80lOSfOUWCegs7b/GRvXEdIss5s=",
283
+ "MindExecution.Plugins.PlanMaster.8hmc6ps1ux.dll": "sha256-NDlttv/8WOvjy9hud+07v5ul3ZJhf2n/cf2CgMfhBnA=",
284
+ "MindExecution.Shared.pqab76cjb6.dll": "sha256-3zUucQiJHN3pl4KBqsG0D0j054ojYss5D69/xCNTngY=",
285
+ "MindExecution.Web.mc24u4bzol.dll": "sha256-5sJydtlQZKVkPUzMIYPO5q2iWWpstxPNVn6Kyk3fBrk="
286
286
  },
287
287
  "lazyAssembly": {
288
288
  "MindExecution.Plugins.Admin.0zkmk8wahq.dll": "sha256-VLiwzLIr+tjKr6+yQfhMVQHq2Yx26QenAkC092+fEdw=",
289
289
  "MindExecution.Plugins.Directory.jtw9q1dm9n.dll": "sha256-Z1x+H7sTj3w9XjWxfqGsd3Jv+qlzwQxAur6pJIP4l20=",
290
- "MindExecution.Plugins.YouTube.dqlboez3y6.dll": "sha256-xUIofC6WDqpMhHa0TqKftVcU7NuZiTSMT25Hqy73U/k="
290
+ "MindExecution.Plugins.YouTube.xb9sou9ox0.dll": "sha256-Pf4IWNwd62loUPGvRnJ8ZgJ1X/h87tNDeBwS8ajlwyk="
291
291
  }
292
292
  },
293
293
  "cacheBootResources": true,
@@ -579,7 +579,7 @@
579
579
  }
580
580
 
581
581
  const base = '_content/MindExecution.Shared/js/';
582
- const scriptVersion = '20260713-normal-boundary-perf-v932';
582
+ const scriptVersion = '20260713-cursor-codex-catalog-v933';
583
583
  const scriptUrl = (script) => `${base}${script}?v=${scriptVersion}`;
584
584
  console.log(`[Script Loader] Shared JS version: ${scriptVersion}`);
585
585
  const criticalScripts = [
@@ -1,5 +1,5 @@
1
1
  self.assetsManifest = {
2
- "version": "664AG46v",
2
+ "version": "skSGAAoe",
3
3
  "assets": [
4
4
  {
5
5
  "hash": "sha256-+CSYMcqLNTsq3VnH11jgYyOCCdxvHzL74CBmo4sCmMU=",
@@ -78,7 +78,7 @@
78
78
  "url": "_content/MindExecution.Shared/js/marked.min.js"
79
79
  },
80
80
  {
81
- "hash": "sha256-EUZYGhkQlsm6gA4b0lgeJhCqmf+jX3HBJUuqdDmeMhU=",
81
+ "hash": "sha256-bBRq3Vtbchai8bpYEoThsNN2ONQiy3ktOmgeKe0U6lI=",
82
82
  "url": "_content/MindExecution.Shared/js/mind-map-core.js"
83
83
  },
84
84
  {
@@ -430,28 +430,28 @@
430
430
  "url": "_framework/MindExecution.Plugins.Business.vyw3769hrw.dll"
431
431
  },
432
432
  {
433
- "hash": "sha256-pf2GETS/+rzZTxBpviTD2zwBY8qAF6NP0sv1WB/pFx8=",
434
- "url": "_framework/MindExecution.Plugins.Concept.vbnlpkxcoo.dll"
433
+ "hash": "sha256-yGhEvwMzCTsmRB6D80lOSfOUWCegs7b/GRvXEdIss5s=",
434
+ "url": "_framework/MindExecution.Plugins.Concept.cvmglrxsjb.dll"
435
435
  },
436
436
  {
437
437
  "hash": "sha256-Z1x+H7sTj3w9XjWxfqGsd3Jv+qlzwQxAur6pJIP4l20=",
438
438
  "url": "_framework/MindExecution.Plugins.Directory.jtw9q1dm9n.dll"
439
439
  },
440
440
  {
441
- "hash": "sha256-EPH8PepplQ1KhRkxCphNWRxoNuI7TjKkJJKSs7Xg7nU=",
442
- "url": "_framework/MindExecution.Plugins.PlanMaster.k3xu6iqktj.dll"
441
+ "hash": "sha256-NDlttv/8WOvjy9hud+07v5ul3ZJhf2n/cf2CgMfhBnA=",
442
+ "url": "_framework/MindExecution.Plugins.PlanMaster.8hmc6ps1ux.dll"
443
443
  },
444
444
  {
445
- "hash": "sha256-xUIofC6WDqpMhHa0TqKftVcU7NuZiTSMT25Hqy73U/k=",
446
- "url": "_framework/MindExecution.Plugins.YouTube.dqlboez3y6.dll"
445
+ "hash": "sha256-Pf4IWNwd62loUPGvRnJ8ZgJ1X/h87tNDeBwS8ajlwyk=",
446
+ "url": "_framework/MindExecution.Plugins.YouTube.xb9sou9ox0.dll"
447
447
  },
448
448
  {
449
- "hash": "sha256-lva/KIyQLLikvw2qN3MdYJ4feScF038KSxVvAfFYhXA=",
450
- "url": "_framework/MindExecution.Shared.6xss0iwsn1.dll"
449
+ "hash": "sha256-3zUucQiJHN3pl4KBqsG0D0j054ojYss5D69/xCNTngY=",
450
+ "url": "_framework/MindExecution.Shared.pqab76cjb6.dll"
451
451
  },
452
452
  {
453
- "hash": "sha256-Wi5LDm1s08JJMwVLzG2qB4SDNXbvDpdhsHn8o61JQoc=",
454
- "url": "_framework/MindExecution.Web.22pax8j6l6.dll"
453
+ "hash": "sha256-5sJydtlQZKVkPUzMIYPO5q2iWWpstxPNVn6Kyk3fBrk=",
454
+ "url": "_framework/MindExecution.Web.mc24u4bzol.dll"
455
455
  },
456
456
  {
457
457
  "hash": "sha256-IsZJ91/OW+fHzNqIgEc7Y072ns8z9dGritiSyvR9Wgc=",
@@ -770,7 +770,7 @@
770
770
  "url": "_framework/Websocket.Client.vapounvmnl.dll"
771
771
  },
772
772
  {
773
- "hash": "sha256-1byhFXCDdf3da557X7XlShV9C91WRkUAJ4hQ5n1TnGE=",
773
+ "hash": "sha256-BhRb0qMTAHsNxt77FQUjETyjbzdhjkbZN5P0GDBi7vU=",
774
774
  "url": "_framework/blazor.boot.json"
775
775
  },
776
776
  {
@@ -830,7 +830,7 @@
830
830
  "url": "icon-512.png"
831
831
  },
832
832
  {
833
- "hash": "sha256-Wk8xAEvQkcT838xkX2NAoC2bypi1+S2OwW6plh/1V0k=",
833
+ "hash": "sha256-k4rHBfZbVwgAQvynTUrp2syVhaEZk19TtPsXKDxzfVI=",
834
834
  "url": "index.html"
835
835
  },
836
836
  {
@@ -1,4 +1,4 @@
1
- /* Manifest version: 664AG46v */
1
+ /* Manifest version: skSGAAoe */
2
2
  // Hosted deployments should prefer the network over stale offline caches.
3
3
  // This service worker immediately clears old Blazor offline caches and unregisters itself.
4
4