@bitkyc08/opencodex 2.7.43 → 2.8.2-preview.20260731
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/bin/ocx.mjs +34 -8
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/assets/index-GC0Vlu1Z.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/adapters/openai-chat.ts +55 -4
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +129 -102
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +3 -3
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +21 -3
- package/src/lib/provider-outbound.ts +8 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +129 -9
- package/src/oauth/kiro.ts +15 -3
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/index.ts +3 -3
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +82 -8
- package/src/server/management/config-routes.ts +24 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +61 -14
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +18 -5
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/proxy-liveness.ts +9 -2
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +395 -31
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +86 -13
- package/src/types.ts +16 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
package/src/cli/init.ts
CHANGED
|
@@ -8,11 +8,28 @@ import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
|
8
8
|
|
|
9
9
|
function createPrompt(): { ask(question: string): Promise<string>; close(): void } {
|
|
10
10
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
let closed = false;
|
|
12
|
+
rl.on("close", () => { closed = true; });
|
|
11
13
|
return {
|
|
12
14
|
ask(question: string): Promise<string> {
|
|
13
|
-
return new Promise(resolve =>
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
if (closed) {
|
|
17
|
+
reject(new Error("stdin closed before the prompt could be answered"));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const onClose = () => {
|
|
21
|
+
reject(new Error("stdin reached EOF while waiting for input"));
|
|
22
|
+
};
|
|
23
|
+
rl.once("close", onClose);
|
|
24
|
+
rl.question(question, answer => {
|
|
25
|
+
rl.off("close", onClose);
|
|
26
|
+
resolve(answer);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
close() {
|
|
31
|
+
if (!closed) rl.close();
|
|
14
32
|
},
|
|
15
|
-
close() { rl.close(); },
|
|
16
33
|
};
|
|
17
34
|
}
|
|
18
35
|
|
|
@@ -83,115 +100,125 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
|
|
|
83
100
|
|
|
84
101
|
export async function runInit(): Promise<void> {
|
|
85
102
|
const prompt = createPrompt();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
try {
|
|
104
|
+
console.log("\n🔧 opencodex (ocx) setup\n");
|
|
105
|
+
|
|
106
|
+
const providers = buildInitProviders();
|
|
107
|
+
printMenu(providers);
|
|
108
|
+
|
|
109
|
+
const choice = await prompt.ask("\nSelect default provider (number): ");
|
|
110
|
+
const idx = parseInt(choice, 10) - 1;
|
|
111
|
+
|
|
112
|
+
let providerName: string;
|
|
113
|
+
let providerConfig: OcxProviderConfig;
|
|
114
|
+
let oauthHint = false;
|
|
115
|
+
|
|
116
|
+
if (idx >= 0 && idx < providers.length) {
|
|
117
|
+
const p = providers[idx];
|
|
118
|
+
providerName = p.id;
|
|
119
|
+
console.log(`\n📡 ${p.label}`);
|
|
120
|
+
console.log(` Base URL: ${p.baseUrl}`);
|
|
121
|
+
|
|
122
|
+
if (p.kind === "forward") {
|
|
123
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "forward" };
|
|
124
|
+
console.log(" No API key needed — forwards your existing `codex login`.");
|
|
125
|
+
} else if (p.kind === "oauth") {
|
|
126
|
+
providerConfig = { adapter: p.adapter, baseUrl: p.baseUrl, authMode: "oauth", ...(p.defaultModel ? { defaultModel: p.defaultModel } : {}) };
|
|
127
|
+
oauthHint = true;
|
|
128
|
+
} else {
|
|
129
|
+
// key + local: collect a key (local usually blank).
|
|
130
|
+
if (p.dashboardUrl) console.log(` 🔑 Get your key: ${p.dashboardUrl}`);
|
|
131
|
+
// Template URL with placeholders (e.g. Cloudflare's {account_id}) needs a resolved value.
|
|
132
|
+
let baseUrl = p.baseUrl;
|
|
133
|
+
if (/\{[^}]*\}/.test(baseUrl)) {
|
|
134
|
+
const resolved = (await prompt.ask(` Your endpoint URL (${baseUrl}): `)).trim();
|
|
135
|
+
if (!resolved) {
|
|
136
|
+
console.error(" A resolved URL is required — replace the {placeholder} with your actual value.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
baseUrl = resolved;
|
|
120
140
|
}
|
|
121
|
-
|
|
141
|
+
const env = envKeyFor(p.id);
|
|
142
|
+
const hint = p.kind === "local" ? "API key (usually blank — press Enter): " : `API key (paste, or env var $${env}): `;
|
|
143
|
+
const apiKey = (await prompt.ask(`\n${hint}`)).trim();
|
|
144
|
+
const modelChoice = (await prompt.ask(`Default model${p.defaultModel ? ` [${p.defaultModel}]` : " (optional)"}: `)).trim();
|
|
145
|
+
const defaultModel = modelChoice || p.defaultModel;
|
|
146
|
+
providerConfig = {
|
|
147
|
+
adapter: p.adapter,
|
|
148
|
+
baseUrl,
|
|
149
|
+
...(p.kind === "key" ? { apiKey: apiKey || `\${${env}}` } : apiKey ? { apiKey } : {}),
|
|
150
|
+
...(defaultModel ? { defaultModel } : {}),
|
|
151
|
+
};
|
|
152
|
+
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
153
|
+
enrichProviderFromCatalog(p.id, providerConfig);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
157
|
+
if (!isValidProviderName(providerName)) {
|
|
158
|
+
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
159
|
+
process.exit(1);
|
|
122
160
|
}
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const apiKey =
|
|
126
|
-
const
|
|
127
|
-
const defaultModel = modelChoice || p.defaultModel;
|
|
161
|
+
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
162
|
+
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
163
|
+
const apiKey = await prompt.ask("API key (optional): ");
|
|
164
|
+
const defaultModel = await prompt.ask("Default model: ");
|
|
128
165
|
providerConfig = {
|
|
129
|
-
adapter:
|
|
130
|
-
baseUrl,
|
|
131
|
-
...(
|
|
132
|
-
...(defaultModel ? { defaultModel } : {}),
|
|
166
|
+
adapter: adapter.trim(),
|
|
167
|
+
baseUrl: baseUrl.trim(),
|
|
168
|
+
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
169
|
+
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
133
170
|
};
|
|
134
|
-
// Apply the catalog's models / vision classification (same enrichment as the GUI).
|
|
135
|
-
enrichProviderFromCatalog(p.id, providerConfig);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
providerName = (await prompt.ask("Provider name: ")).trim();
|
|
139
|
-
if (!isValidProviderName(providerName)) {
|
|
140
|
-
console.error("Provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key.");
|
|
141
|
-
prompt.close();
|
|
142
|
-
process.exit(1);
|
|
143
171
|
}
|
|
144
|
-
const baseUrl = await prompt.ask("Base URL (e.g. http://localhost:11434/v1): ");
|
|
145
|
-
const adapter = await prompt.ask("Adapter [openai-chat]: ") || "openai-chat";
|
|
146
|
-
const apiKey = await prompt.ask("API key (optional): ");
|
|
147
|
-
const defaultModel = await prompt.ask("Default model: ");
|
|
148
|
-
providerConfig = {
|
|
149
|
-
adapter: adapter.trim(),
|
|
150
|
-
baseUrl: baseUrl.trim(),
|
|
151
|
-
...(apiKey.trim() ? { apiKey: apiKey.trim() } : {}),
|
|
152
|
-
...(defaultModel.trim() ? { defaultModel: defaultModel.trim() } : {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
172
|
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
const portStr = await prompt.ask("\nProxy port [10100]: ");
|
|
174
|
+
const port = parseInt(portStr, 10) || 10100;
|
|
158
175
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const config: OcxConfig = {
|
|
177
|
+
...getDefaultConfig(),
|
|
178
|
+
port,
|
|
179
|
+
providers: { [providerName]: providerConfig },
|
|
180
|
+
defaultProvider: providerName,
|
|
181
|
+
};
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
saveConfig(config);
|
|
184
|
+
// Init writes a fresh config, so a stale pre-migration backup from a previous
|
|
185
|
+
// installation would make the next `ocx start` crash on a stale-backup
|
|
186
|
+
// collision (issue #257). But only a STALE backup (unparseable, or already a
|
|
187
|
+
// post-migration v2 snapshot) may be deleted; a backup that still parses as a
|
|
188
|
+
// valid pre-migration (v1) config is a user-intentional rollback point and is
|
|
189
|
+
// preserved by renaming it out of the collision path (sol review 260722).
|
|
190
|
+
cleanupOpenAiTierBackupAfterInit();
|
|
191
|
+
console.log(`\n✅ Config saved to ~/.opencodex/config.json`);
|
|
192
|
+
if (oauthHint) console.log(`🔐 Authenticate this provider with: ocx login ${providerName}`);
|
|
193
|
+
|
|
194
|
+
const injectAnswer = await prompt.ask("Inject into Codex config.toml? [Y/n]: ");
|
|
195
|
+
if (injectAnswer.trim().toLowerCase() !== "n") {
|
|
196
|
+
console.log("Fetching available models from provider...");
|
|
197
|
+
const result = await injectCodexConfig(port, config);
|
|
198
|
+
console.log(result.success ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
199
|
+
}
|
|
183
200
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
const shimAnswer = await prompt.ask("Install Codex autostart shim? [Y/n]: ");
|
|
202
|
+
if (shimAnswer.trim().toLowerCase() !== "n") {
|
|
203
|
+
try {
|
|
204
|
+
const { installCodexShim } = await import("../codex/shim");
|
|
205
|
+
const result = installCodexShim();
|
|
206
|
+
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(`⚠️ Codex autostart shim skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
209
|
+
}
|
|
192
210
|
}
|
|
193
|
-
}
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
console.log(`\n🚀 Setup complete! Run 'ocx start' to start the proxy.`);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
+
if (/stdin (closed|reached EOF)/i.test(message)) {
|
|
216
|
+
console.error(`\n❌ ${message}. Re-run \`ocx init\` in an interactive terminal.`);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
} finally {
|
|
222
|
+
prompt.close();
|
|
223
|
+
}
|
|
197
224
|
}
|
|
@@ -25,7 +25,11 @@ export interface InteractiveConfirmOptions {
|
|
|
25
25
|
output?: NodeJS.WriteStream;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// The highlight sets an explicit black-on-white pair rather than bare reverse
|
|
29
|
+
// video (\x1b[7m). Reverse alone inherits whatever foreground colour is in
|
|
30
|
+
// effect, so on some themes the selected label rendered as black text on a black
|
|
31
|
+
// block and the choice became invisible.
|
|
32
|
+
const REVERSE = "\x1b[30;47m";
|
|
29
33
|
const DIM = "\x1b[2m";
|
|
30
34
|
const RESET = "\x1b[0m";
|
|
31
35
|
const CLEAR_LINE = "\r\x1b[K";
|
package/src/cli/star-prompt.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { getConfigDir } from "../config";
|
|
5
5
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
6
|
+
import { commandInvocation } from "../lib/win-exec";
|
|
6
7
|
import { isAgentDriven } from "./agent-driven";
|
|
7
8
|
import { interactiveConfirm } from "./interactive-confirm";
|
|
8
9
|
|
|
@@ -29,16 +30,37 @@ export function hasStarPromptRun(): boolean {
|
|
|
29
30
|
* that case the prompt stays silent instead of asking for something it would
|
|
30
31
|
* then fail to do.
|
|
31
32
|
*/
|
|
33
|
+
/**
|
|
34
|
+
* On Windows `gh` is a `.cmd` shim; a shell-less spawn of the bare name skips
|
|
35
|
+
* PATHEXT and refuses `.cmd` targets, so it stalls until the timeout instead of
|
|
36
|
+
* failing fast. Route every call through the launcher the rest of the CLI uses.
|
|
37
|
+
*/
|
|
38
|
+
/** Resolve `gh` once; callers keep their own spawnSync overload. */
|
|
39
|
+
function ghInvocation(args: string[]) {
|
|
40
|
+
const invocation = commandInvocation("gh", args);
|
|
41
|
+
return {
|
|
42
|
+
file: invocation.file,
|
|
43
|
+
args: invocation.args,
|
|
44
|
+
verbatim: invocation.options.windowsVerbatimArguments === true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
function ghAvailable(): boolean {
|
|
33
|
-
const
|
|
49
|
+
const v = ghInvocation(["--version"]);
|
|
50
|
+
const version = spawnSync(v.file, v.args,
|
|
51
|
+
{ stdio: "ignore", timeout: 3000, windowsHide: true, windowsVerbatimArguments: v.verbatim });
|
|
34
52
|
if (version.error || version.status !== 0) return false;
|
|
35
|
-
const
|
|
53
|
+
const a = ghInvocation(["auth", "status"]);
|
|
54
|
+
const auth = spawnSync(a.file, a.args,
|
|
55
|
+
{ stdio: "ignore", timeout: 5000, windowsHide: true, windowsVerbatimArguments: a.verbatim });
|
|
36
56
|
return !auth.error && auth.status === 0;
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
function starRepo(): { ok: boolean; error?: string } {
|
|
40
|
-
const
|
|
41
|
-
|
|
60
|
+
const star = ghInvocation(["api", "-X", "PUT", `/user/starred/${REPO}`]);
|
|
61
|
+
const r = spawnSync(star.file, star.args,
|
|
62
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000, windowsHide: true,
|
|
63
|
+
windowsVerbatimArguments: star.verbatim });
|
|
42
64
|
if (r.error) return { ok: false, error: r.error.message };
|
|
43
65
|
if (r.status !== 0) return { ok: false, error: (r.stderr || r.stdout || "").trim() || `gh exited ${r.status}` };
|
|
44
66
|
return { ok: true };
|
package/src/cli/v2.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - nothing in the catalog build path calls this module; no auto-flip exists.
|
|
12
12
|
*/
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
|
-
import { getLogicalMaxThreads, hasAgentsMaxThreads, isMultiAgentV2Enabled, transitionMultiAgentV2 } from "../codex/features";
|
|
14
|
+
import { getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, transitionMultiAgentV2 } from "../codex/features";
|
|
15
15
|
|
|
16
16
|
import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
|
|
17
17
|
import { loadConfig, saveConfig } from "../config";
|
|
@@ -85,6 +85,15 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
85
85
|
log.log(multiAgentModeLine(cfg.multiAgentMode ?? "default"));
|
|
86
86
|
const threads = getLogicalMaxThreads();
|
|
87
87
|
log.log(`max_threads: ${threads ?? "(unset — codex default)"}`);
|
|
88
|
+
const v2Active = isEnabled();
|
|
89
|
+
const agentsEnabled = getAgentsEnabled();
|
|
90
|
+
log.log(`agents.enabled: ${agentsEnabled === null ? "(unset — upstream default true)" : agentsEnabled}`);
|
|
91
|
+
const maxDepth = getAgentsMaxDepth();
|
|
92
|
+
// max_depth is V1-only upstream; say so whenever V2 is active so the number
|
|
93
|
+
// cannot be misread as an effective V2 limit.
|
|
94
|
+
log.log(`agents.max_depth: ${maxDepth ?? "(unset — upstream default 1)"}${v2Active ? " (V1-only — ignored while multi_agent_v2 is enabled)" : ""}`);
|
|
95
|
+
const instructions = getSubagentDeveloperInstructions();
|
|
96
|
+
log.log(`subagent_developer_instructions: ${instructions === null ? "(unset — children inherit)" : instructions === "" ? '"" (clears inherited instructions)' : JSON.stringify(instructions)}`);
|
|
88
97
|
if (isEnabled() && hasMaxThreads()) {
|
|
89
98
|
log.log("WARNING: [agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove it from config.toml (concurrency lives in features.multi_agent_v2.max_concurrent_threads_per_session).");
|
|
90
99
|
}
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { closeSync, existsSync, readFileSync, mkdirSync, openSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
5
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
5
6
|
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
6
7
|
|
|
7
8
|
type LegacyCodexAccountStore = Record<string, CodexAccountCredentials>;
|
|
@@ -97,6 +98,7 @@ function loadCodexAccountRecordStore(): CodexAccountStore {
|
|
|
97
98
|
|
|
98
99
|
function persist(store: CodexAccountStore): void {
|
|
99
100
|
const dir = getConfigDir();
|
|
101
|
+
assertNotRealHomeUnderTest(dir);
|
|
100
102
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
101
103
|
atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
|
|
102
104
|
}
|
|
@@ -151,14 +151,21 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): RawCatal
|
|
|
151
151
|
let cacheKey: string | null = null;
|
|
152
152
|
const candidates = deps.commandCandidates?.() ?? (() => {
|
|
153
153
|
const resolved = resolveAndPersistCodexRuntime({
|
|
154
|
-
execFileSync
|
|
154
|
+
// Forward an INJECTED execFileSync only. Passing the real one unconditionally made
|
|
155
|
+
// resolveCacheKey() bail out (it refuses to memoize injected-dep resolves), so every
|
|
156
|
+
// catalog read re-ran the ~1s `codex --version` probe even on a warm cache hit.
|
|
157
|
+
...(deps.execFileSync ? { execFileSync: deps.execFileSync } : {}),
|
|
155
158
|
configDir: deps.configDir,
|
|
156
159
|
env: deps.env,
|
|
157
160
|
platform: deps.platform,
|
|
158
161
|
existsSync: deps.existsSync,
|
|
159
162
|
readFileSync: deps.readFileSync,
|
|
160
163
|
now: deps.now,
|
|
161
|
-
|
|
164
|
+
// Catalog loading only consumes `resolved.runtime.command`, never `newerAvailable`.
|
|
165
|
+
// Full PATH discovery probes every candidate launcher (100+ on a dev machine, ~1.2s),
|
|
166
|
+
// which alone can exceed the 3s budget `ocx claude` allows /api/claude-code. Priority
|
|
167
|
+
// selection is identical either way; callers wanting discovery diagnostics opt in.
|
|
168
|
+
discoverAlternatives: deps.discoverAlternatives ?? false,
|
|
162
169
|
});
|
|
163
170
|
if (useCache) {
|
|
164
171
|
cacheKey = [
|
|
@@ -123,6 +123,12 @@ export function visibleNativeSlugs(config: Pick<OcxConfig, "disabledModels">): s
|
|
|
123
123
|
return nativeOpenAiSlugs().filter(slug => !disabled.has(slug));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Native slugs exposed to Claude Desktop show/export/apply (opt-out via claudeCode.desktopNativeModels). */
|
|
127
|
+
export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" | "disabledModels">): string[] {
|
|
128
|
+
if (config.claudeCode?.desktopNativeModels === false) return [];
|
|
129
|
+
return visibleNativeSlugs(config);
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
|
|
127
133
|
const disabled = disabledNativeSlugs(config);
|
|
128
134
|
return NATIVE_OPENAI_MODELS.map(slug => {
|
|
@@ -110,6 +110,8 @@ export interface CatalogModel {
|
|
|
110
110
|
/** Whether Codex may send Responses text.verbosity for this routed model. */
|
|
111
111
|
supportsVerbosity?: boolean;
|
|
112
112
|
supportsReasoningSummaries?: boolean;
|
|
113
|
+
/** Normalized upstream capability names retained for management/API consumers (#485 follow-up). */
|
|
114
|
+
capabilities?: string[];
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
export type RawEntry = Record<string, unknown>;
|
|
@@ -272,6 +274,16 @@ export function ensureStrictCatalogFields(
|
|
|
272
274
|
if (!Array.isArray(entry.input_modalities) && !options.preserveExactInputModalities) {
|
|
273
275
|
entry.input_modalities = ["text"];
|
|
274
276
|
}
|
|
277
|
+
// Codex parses `input_modalities` as a closed enum. One out-of-enum value (zenmux advertises
|
|
278
|
+
// "video") makes its config loader reject the entire catalog, which takes down plugins, apps and
|
|
279
|
+
// MCP servers — not just that model. Normalize at the single point every entry passes through,
|
|
280
|
+
// because provider metadata, jawcode metadata and effort sync each write this field.
|
|
281
|
+
if (Array.isArray(entry.input_modalities)) {
|
|
282
|
+
const accepted = entry.input_modalities.filter(value =>
|
|
283
|
+
value === "text" || value === "image" || value === "audio");
|
|
284
|
+
// Never leave it empty: an entry with no modality at all is worse than a text-only one.
|
|
285
|
+
entry.input_modalities = accepted.length > 0 ? accepted : ["text"];
|
|
286
|
+
}
|
|
275
287
|
const contextWindow = typeof entry.context_window === "number" && entry.context_window > 0 ? entry.context_window : 128000;
|
|
276
288
|
entry.context_window = contextWindow;
|
|
277
289
|
if (
|
|
@@ -288,7 +300,18 @@ export function ensureStrictCatalogFields(
|
|
|
288
300
|
|
|
289
301
|
export type MultiAgentMode = "v1" | "default" | "v2";
|
|
290
302
|
|
|
291
|
-
|
|
303
|
+
/**
|
|
304
|
+
* @param v2FeatureEnabled When the native multi_agent_v2 feature is on, "default"
|
|
305
|
+
* mode stamps unpinned entries as "v2" instead of deleting the key. The native
|
|
306
|
+
* binary validates spawn_agent models against THIS catalog with its own
|
|
307
|
+
* `multi_agent_version == Some(V2)` test (codex-rs multi_agents_common.rs), so an
|
|
308
|
+
* absent pin means a clean refusal at spawn time — exactly the cross-provider
|
|
309
|
+
* spawns opencodex exists to enable (option B, devlog
|
|
310
|
+
* 260730_codex_rs_upstream_v2_live_handoff/060). Upstream pins are always
|
|
311
|
+
* preserved: a genuine "v1" pin is a real capability statement and stays excluded.
|
|
312
|
+
* With the feature off the output is byte-identical to the historical behavior.
|
|
313
|
+
*/
|
|
314
|
+
export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v2FeatureEnabled = false): RawEntry[] {
|
|
292
315
|
if (mode === "default") {
|
|
293
316
|
// Restore upstream defaults: clear any stale forced multi_agent_version and
|
|
294
317
|
// re-apply upstream pins from the snapshot for native entries that have one.
|
|
@@ -298,6 +321,8 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode):
|
|
|
298
321
|
const upstreamPin = upstream?.multi_agent_version;
|
|
299
322
|
if (typeof upstreamPin === "string") {
|
|
300
323
|
entry.multi_agent_version = upstreamPin;
|
|
324
|
+
} else if (v2FeatureEnabled) {
|
|
325
|
+
entry.multi_agent_version = "v2";
|
|
301
326
|
} else {
|
|
302
327
|
delete entry.multi_agent_version;
|
|
303
328
|
}
|