@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.
- package/codex-model-catalog.js +202 -0
- package/codex-runtime.js +5 -2
- package/package.json +6 -3
- package/scripts/codex-model-catalog-smoke.mjs +150 -0
- package/server.js +35 -69
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +32 -28
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +2 -2
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +153 -233
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +26 -5
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +40 -23
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +10 -5
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +10 -1
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.7uhld08zkc.dll → MindExecution.Plugins.Concept.cvmglrxsjb.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.y1sj1ifw0z.dll → MindExecution.Plugins.PlanMaster.8hmc6ps1ux.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.ij0uk05x1o.dll → MindExecution.Plugins.YouTube.xb9sou9ox0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.1h72etbxec.dll → MindExecution.Shared.pqab76cjb6.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.18xe2efznc.dll → MindExecution.Web.mc24u4bzol.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +11 -11
- package/wwwroot/index.html +3 -2
- package/wwwroot/service-worker-assets.js +24 -20
- 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.
|
|
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.
|
|
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
|
-
|
|
3172
|
-
let
|
|
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
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
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
|
-
|
|
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.
|
|
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')),
|