@bitkyc08/opencodex 2.11.0 → 2.11.1
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/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `ocx export --client <id>` — print a client config for the live proxy.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* OpenClaw JSON5, Kimi TOML.
|
|
4
|
+
* Seven clients, four formats: opencode and Pi are JSON, OMP, Hermes and Gajae
|
|
5
|
+
* are YAML, OpenClaw JSON5, Kimi TOML.
|
|
6
6
|
*
|
|
7
7
|
* Two consumers, one payload (devlog 260731_client_config_export/020):
|
|
8
8
|
*
|
|
@@ -179,13 +179,13 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
179
179
|
|
|
180
180
|
// Every serializer already ends with exactly one newline.
|
|
181
181
|
if (out !== undefined) writeExport(out, text, force);
|
|
182
|
-
// stderr, so `--json` stdout stays
|
|
182
|
+
// stderr, so `--json` stdout stays a standalone JSON document.
|
|
183
183
|
if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);
|
|
184
184
|
|
|
185
185
|
const degraded = models.filter(model => !hasContextLimit(model)).length;
|
|
186
|
-
// `--json` keeps emitting the DOCUMENT at the top level
|
|
187
|
-
//
|
|
188
|
-
//
|
|
186
|
+
// `--json` keeps emitting the DOCUMENT at the top level as JSON for scripts;
|
|
187
|
+
// `--out` is the path that writes the selected client's native format.
|
|
188
|
+
// Format metadata rides in the human lines below.
|
|
189
189
|
printData(clientConfig, wantsJson, [
|
|
190
190
|
text.trimEnd(),
|
|
191
191
|
"",
|
package/src/cli/help.ts
CHANGED
|
@@ -173,12 +173,12 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
173
173
|
},
|
|
174
174
|
"api-key": { usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
175
175
|
export: {
|
|
176
|
-
usage: "ocx export --client <opencode|pi|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
|
|
177
|
-
summary: "Print a client config (opencode, Pi, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.",
|
|
176
|
+
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
|
|
177
|
+
summary: "Print a client config (opencode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.",
|
|
178
178
|
details: [
|
|
179
|
-
"--json prints
|
|
180
|
-
"--out <path> writes the config there and refuses to replace an existing file without --force.",
|
|
181
|
-
"The config never contains a key; it
|
|
179
|
+
"--json prints the generated document as JSON on stdout; use --out for the client's native format.",
|
|
180
|
+
"--out <path> writes the native config there and refuses to replace an existing file without --force.",
|
|
181
|
+
"The config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.",
|
|
182
182
|
"The destination path is printed for merging by hand — ocx never writes your real client config.",
|
|
183
183
|
],
|
|
184
184
|
},
|
|
@@ -313,7 +313,7 @@ Usage:
|
|
|
313
313
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
314
314
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
315
315
|
ocx access <sub> External API keys and endpoint information
|
|
316
|
-
ocx export --client <id> Print a client config wired to the running proxy (
|
|
316
|
+
ocx export --client <id> Print a client config wired to the running proxy (7 clients)
|
|
317
317
|
ocx integration client <sub> Enable, disable, inspect or roll back a client integration
|
|
318
318
|
ocx grok <sub> Grok Build model selection and apply
|
|
319
319
|
ocx system <sub> Runtime settings, startup, sync, and updates
|
package/src/cli/index.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
4
4
|
import { stripGrokConfig } from "../grok/inject";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
describeHistoryJobFailure,
|
|
7
|
+
resolveCodexHistoryJobTarget,
|
|
8
|
+
runCodexHistoryJob,
|
|
9
|
+
} from "../codex/history-job";
|
|
6
10
|
import { reconcileJournal } from "../codex/journal";
|
|
7
11
|
import {
|
|
8
12
|
codexAutoStartEnabled,
|
|
@@ -36,7 +40,7 @@ import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSele
|
|
|
36
40
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
37
41
|
import { createReadinessGate } from "../server/readiness";
|
|
38
42
|
import { parseReadyArgs, runReady, type ReadyArgs } from "./ready";
|
|
39
|
-
import { stopProxy } from "../lib/process-control";
|
|
43
|
+
import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
|
|
40
44
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
41
45
|
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
42
46
|
import { startupHealthSummary } from "../codex/autostart-health";
|
|
@@ -675,6 +679,10 @@ async function handleStop() {
|
|
|
675
679
|
// exact teardown the refusal exists to prevent.
|
|
676
680
|
const detail = err instanceof Error ? err.message : String(err);
|
|
677
681
|
if (detail) console.error(` ${detail}`);
|
|
682
|
+
if (err instanceof ProxyOwnershipRefusedError) {
|
|
683
|
+
ownershipBlocked = true;
|
|
684
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
|
|
685
|
+
}
|
|
678
686
|
}
|
|
679
687
|
} else {
|
|
680
688
|
// Snapshot the stale on-disk state BEFORE the async probe: a concurrent `ocx start`
|
|
@@ -693,6 +701,10 @@ async function handleStop() {
|
|
|
693
701
|
console.error(`❌ Failed to stop proxy (PID ${live.pid}).`);
|
|
694
702
|
const detail = err instanceof Error ? err.message : String(err);
|
|
695
703
|
if (detail) console.error(` ${detail}`);
|
|
704
|
+
if (err instanceof ProxyOwnershipRefusedError) {
|
|
705
|
+
ownershipBlocked = true;
|
|
706
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config): the foreign proxy is still running.");
|
|
707
|
+
}
|
|
696
708
|
}
|
|
697
709
|
} else if (!stoppedService) {
|
|
698
710
|
console.log("No running proxy found.");
|
|
@@ -899,7 +911,7 @@ async function handleRecoverHistory() {
|
|
|
899
911
|
: { rows: 0, files: 0, failed: true as const };
|
|
900
912
|
if (r.failed) {
|
|
901
913
|
console.error(
|
|
902
|
-
|
|
914
|
+
`⚠️ Recovery SKIPPED: ${describeHistoryJobFailure(outcome, "recover-legacy")}`,
|
|
903
915
|
);
|
|
904
916
|
process.exit(1);
|
|
905
917
|
}
|
|
@@ -1148,11 +1160,16 @@ switch (command) {
|
|
|
1148
1160
|
break;
|
|
1149
1161
|
}
|
|
1150
1162
|
case "codex-shim": {
|
|
1151
|
-
const { codexShimStatus, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
1163
|
+
const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
1152
1164
|
switch (args[1]) {
|
|
1153
1165
|
case "install": {
|
|
1154
1166
|
const r = installCodexShim();
|
|
1155
|
-
|
|
1167
|
+
const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness");
|
|
1168
|
+
const warnings = diagnoseCodexShim().healthy
|
|
1169
|
+
? collectCodexShimReadinessWarnings()
|
|
1170
|
+
: [];
|
|
1171
|
+
console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
|
|
1172
|
+
for (const warning of warnings) console.warn(` ${warning}`);
|
|
1156
1173
|
break;
|
|
1157
1174
|
}
|
|
1158
1175
|
case "status":
|
|
@@ -20,7 +20,7 @@ const USAGE = `Usage:
|
|
|
20
20
|
ocx models <enable|disable> <provider/model|native-model> [--native] [--json]
|
|
21
21
|
ocx models provider <name> <on|off> [--json]
|
|
22
22
|
ocx models selected <provider> [--set <id,id...>|--clear] [--json]
|
|
23
|
-
ocx models context <status|value <tokens
|
|
23
|
+
ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]
|
|
24
24
|
ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]`;
|
|
25
25
|
|
|
26
26
|
type ModelRow = {
|
|
@@ -158,11 +158,23 @@ async function context(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
158
158
|
const value = Number(raw.replace(/[_,]/g, ""));
|
|
159
159
|
if (!Number.isInteger(value) || value <= 0) throw new CliUsageError("context value must be a positive integer", USAGE);
|
|
160
160
|
body = { value };
|
|
161
|
+
// Explicit apply-to-all switch for headless use: re-points every routed provider to
|
|
162
|
+
// the new value, mirroring the dashboard's "apply to every routed provider" toggle.
|
|
163
|
+
// Without it the value only becomes the default for future toggles.
|
|
164
|
+
if (takeFlag(args, "--set-all")) body.setAll = true;
|
|
161
165
|
} else if (action === "provider") {
|
|
162
166
|
const provider = args.shift()?.trim();
|
|
163
167
|
const state = args.shift()?.toLowerCase();
|
|
164
168
|
if (!provider || (state !== "on" && state !== "off")) throw new CliUsageError("provider and on|off are required", USAGE);
|
|
165
169
|
body = { provider, enabled: state === "on" };
|
|
170
|
+
// Optional explicit cap value for this provider only (`ocx models context provider
|
|
171
|
+
// openai on --value 128000`). Mirrors the dashboard's per-provider cap picker; the
|
|
172
|
+
// value never leaks to other providers.
|
|
173
|
+
const value = takeIntegerOption(args, "--value", { min: 1 });
|
|
174
|
+
if (value !== undefined && state !== "on") {
|
|
175
|
+
throw new CliUsageError("--value can only be used with on", USAGE);
|
|
176
|
+
}
|
|
177
|
+
if (value !== undefined) body.value = value;
|
|
166
178
|
} else if (action === "all") {
|
|
167
179
|
const state = args.shift()?.toLowerCase();
|
|
168
180
|
if (state !== "on" && state !== "off") throw new CliUsageError("all requires on|off", USAGE);
|
package/src/cli/provider.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { apiKeyTransportConfigError, hasOwnProvider, isValidProviderName, loadCo
|
|
|
12
12
|
import { hasHelpFlag } from "./help";
|
|
13
13
|
import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
|
|
14
14
|
import { providerConfigSeed } from "../providers/derive";
|
|
15
|
+
import { dropProviderCustomModels } from "../providers/provider-id-rewrite";
|
|
15
16
|
import type { OcxProviderConfig } from "../types";
|
|
16
17
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
17
18
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -302,6 +303,7 @@ function handleRemove(args: string[]): void {
|
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
delete config.providers[name];
|
|
306
|
+
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
305
307
|
validateAndSave(config);
|
|
306
308
|
|
|
307
309
|
|
|
@@ -312,11 +314,16 @@ function handleRemove(args: string[]): void {
|
|
|
312
314
|
remainingProviders: Object.keys(config.providers),
|
|
313
315
|
defaultProvider: config.defaultProvider,
|
|
314
316
|
needsSync: true,
|
|
317
|
+
...(droppedCustomModels > 0 ? { droppedCustomModels } : {}),
|
|
315
318
|
}, null, 2));
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
console.log(`✅ Provider "${name}" removed.`);
|
|
323
|
+
if (droppedCustomModels > 0) {
|
|
324
|
+
const plural = droppedCustomModels === 1 ? "model" : "models";
|
|
325
|
+
console.log(` Also removed ${droppedCustomModels} custom ${plural} that belonged to it.`);
|
|
326
|
+
}
|
|
320
327
|
}
|
|
321
328
|
|
|
322
329
|
// ---------------------------------------------------------------------------
|
|
@@ -170,6 +170,49 @@ export function opencodeGlobalConfigPath(
|
|
|
170
170
|
return join(xdg, "opencode", "opencode.json");
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
const OMP_PROFILE_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
174
|
+
const OMP_WINDOWS_RESERVED_PROFILE_RE = /^(?:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\..*)?$/i;
|
|
175
|
+
|
|
176
|
+
function ompProfileName(env: OpencodeLaunchEnv): string | undefined {
|
|
177
|
+
// OMP_PROFILE wins by presence, even when explicitly empty; PI_PROFILE is
|
|
178
|
+
// only the legacy fallback when the canonical variable is undefined.
|
|
179
|
+
const raw = env.OMP_PROFILE !== undefined ? env.OMP_PROFILE : env.PI_PROFILE;
|
|
180
|
+
const profile = raw?.trim();
|
|
181
|
+
if (!profile || profile === "default") return undefined;
|
|
182
|
+
if (
|
|
183
|
+
profile === "."
|
|
184
|
+
|| profile === ".."
|
|
185
|
+
|| profile.endsWith(".")
|
|
186
|
+
|| !OMP_PROFILE_NAME_RE.test(profile)
|
|
187
|
+
|| OMP_WINDOWS_RESERVED_PROFILE_RE.test(profile)
|
|
188
|
+
) {
|
|
189
|
+
throw new ClientPathError(`Invalid OMP profile "${raw}"`);
|
|
190
|
+
}
|
|
191
|
+
return profile;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Resolve the global Oh My Pi agent directory using OMP's own env precedence. */
|
|
195
|
+
export function ompAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
196
|
+
const profile = ompProfileName(env);
|
|
197
|
+
if (!profile) {
|
|
198
|
+
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
199
|
+
if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
|
|
200
|
+
}
|
|
201
|
+
// OMP treats PI_CONFIG_DIR as a directory name relative to the user's home,
|
|
202
|
+
// even when the value starts with `/` or `~`. Mirror that path.join contract
|
|
203
|
+
// exactly instead of assigning those values different filesystem semantics.
|
|
204
|
+
const root = join(home, env.PI_CONFIG_DIR || ".omp");
|
|
205
|
+
return profile ? join(root, "profiles", profile, "agent") : join(root, "agent");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** OMP's canonical custom-provider catalog. */
|
|
209
|
+
export function ompModelsConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
210
|
+
const agentDir = ompAgentDir(env, home);
|
|
211
|
+
const yamlFallback = join(agentDir, "models.yaml");
|
|
212
|
+
const canonical = join(agentDir, "models.yml");
|
|
213
|
+
return !existsSync(canonical) && existsSync(yamlFallback) ? yamlFallback : canonical;
|
|
214
|
+
}
|
|
215
|
+
|
|
173
216
|
/** Compose the OpenAI-compatible proxy base URL from a live probe result. */
|
|
174
217
|
export function opencodeProxyBaseUrl(port: number, hostname?: string): string {
|
|
175
218
|
return `http://${probeHostname(hostname)}:${port}/v1`;
|
|
@@ -364,6 +407,7 @@ export interface ExportContext {
|
|
|
364
407
|
export type ExportClientId =
|
|
365
408
|
| "opencode"
|
|
366
409
|
| "pi"
|
|
410
|
+
| "omp"
|
|
367
411
|
| "hermes"
|
|
368
412
|
| "openclaw"
|
|
369
413
|
| "kimi"
|
|
@@ -399,14 +443,13 @@ export interface ExportClientSpec {
|
|
|
399
443
|
*/
|
|
400
444
|
buildContribution: BuildContribution;
|
|
401
445
|
/**
|
|
402
|
-
* True when
|
|
446
|
+
* True when the generated integration deliberately supports loopback only.
|
|
403
447
|
*
|
|
404
448
|
* `/v1/chat/completions` rejects bearer credentials and requires the
|
|
405
449
|
* dedicated `x-opencodex-api-key` header (AUTH_MATRIX in
|
|
406
|
-
* src/server/auth-cors.ts).
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* Grok managed block's non-loopback refusal.
|
|
450
|
+
* src/server/auth-cors.ts). If this exporter cannot safely emit that header,
|
|
451
|
+
* it refuses a remote bind rather than generating a config that 401s. Same
|
|
452
|
+
* reasoning as the Grok managed block's non-loopback refusal.
|
|
410
453
|
*/
|
|
411
454
|
loopbackOnly: boolean;
|
|
412
455
|
}
|
|
@@ -894,6 +937,11 @@ function buildPiContribution(ctx: ExportContext): ManagedContribution {
|
|
|
894
937
|
return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
895
938
|
}
|
|
896
939
|
|
|
940
|
+
function buildOmpContribution(ctx: ExportContext): ManagedContribution {
|
|
941
|
+
const doc = buildPiClientConfig(ctx);
|
|
942
|
+
return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
943
|
+
}
|
|
944
|
+
|
|
897
945
|
function buildHermesContribution(ctx: ExportContext): ManagedContribution {
|
|
898
946
|
const doc = buildHermesClientConfig(ctx);
|
|
899
947
|
return singleFragment("hermes", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
@@ -953,6 +1001,20 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
953
1001
|
// dedicated admission header a remote bind requires.
|
|
954
1002
|
loopbackOnly: true,
|
|
955
1003
|
},
|
|
1004
|
+
omp: {
|
|
1005
|
+
id: "omp",
|
|
1006
|
+
filename: "omp-models.yaml",
|
|
1007
|
+
destination: env => ompModelsConfigPath(env),
|
|
1008
|
+
apiKeyEnv: "",
|
|
1009
|
+
exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.",
|
|
1010
|
+
build: buildPiClientConfig,
|
|
1011
|
+
format: "yaml",
|
|
1012
|
+
summarize: summarizePi,
|
|
1013
|
+
buildContribution: buildOmpContribution,
|
|
1014
|
+
// OMP supports provider-level headers, but remote credential wiring is
|
|
1015
|
+
// intentionally deferred from this initial loopback-only integration.
|
|
1016
|
+
loopbackOnly: true,
|
|
1017
|
+
},
|
|
956
1018
|
hermes: {
|
|
957
1019
|
id: "hermes",
|
|
958
1020
|
filename: "hermes-config.yaml",
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteFile,
|
|
4
|
+
getConfigPath,
|
|
5
|
+
saveConfigPreservingClaudeCode,
|
|
6
|
+
withConfigMutationLockSync,
|
|
7
|
+
} from "../config";
|
|
1
8
|
import { removeCodexAccountCredential } from "./account-store";
|
|
2
9
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
3
10
|
import { getMainChatgptAccountId } from "./auth-collision";
|
|
@@ -8,10 +15,25 @@ import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
|
8
15
|
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache } from "./main-account-cache";
|
|
9
16
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
10
17
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
18
|
+
import { codexAccountNamespaceEntries, codexAccountPickerEnabled } from "./account-namespaces";
|
|
11
19
|
import type { OcxConfig } from "../types";
|
|
12
20
|
|
|
13
21
|
let observedMainChatgptAccountId: string | undefined;
|
|
14
22
|
|
|
23
|
+
export class CodexAccountDeleteCleanupError extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("Account deletion was saved, but local credential cleanup did not complete. Retry removal.");
|
|
26
|
+
this.name = "CodexAccountDeleteCleanupError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class CodexAccountDeleteRollbackError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Account deletion failed and the previous config could not be restored. Restart before retrying.");
|
|
33
|
+
this.name = "CodexAccountDeleteRollbackError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
15
37
|
export function purgeCodexAccountRuntimeState(accountId: string): void {
|
|
16
38
|
clearAccountNeedsReauth(accountId);
|
|
17
39
|
clearAccountQuota(accountId);
|
|
@@ -70,14 +92,81 @@ export function resetMainCodexAccountIdentityTrackingForTests(): void {
|
|
|
70
92
|
clearMainAccountCredentialPresence();
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
function restoreRuntimeConfig(target: OcxConfig, snapshot: OcxConfig): void {
|
|
96
|
+
for (const key of Object.keys(target) as Array<keyof OcxConfig>) delete target[key];
|
|
97
|
+
Object.assign(target, snapshot);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function restorePersistedConfig(configPath: string, previousBytes: string): void {
|
|
101
|
+
try {
|
|
102
|
+
if (readFileSync(configPath, "utf8") === previousBytes) return;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
105
|
+
}
|
|
106
|
+
atomicWriteFile(configPath, previousBytes);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete a stored account while retaining its selector binding.
|
|
111
|
+
*
|
|
112
|
+
* When the runtime config is backed by an existing config.json, commit the config deletion before
|
|
113
|
+
* credentials or runtime state are destroyed. Transient callers intentionally skip durable config
|
|
114
|
+
* persistence because they have no durable account row to protect. The whole sequence shares the
|
|
115
|
+
* config mutation coordinator so a cooperating writer cannot re-add a persisted account between
|
|
116
|
+
* the durable config commit and credential cleanup.
|
|
117
|
+
*
|
|
118
|
+
* Returns true when a picker-visible row disappeared and the catalog must converge.
|
|
119
|
+
*/
|
|
120
|
+
export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string): boolean {
|
|
121
|
+
let cleanupFailed = false;
|
|
122
|
+
const pickerVisibilityChanged = withConfigMutationLockSync(() => {
|
|
123
|
+
const previousConfig = structuredClone(runtimeConfig);
|
|
124
|
+
const configPath = getConfigPath();
|
|
125
|
+
const hasPersistedConfig = existsSync(configPath);
|
|
126
|
+
const previousPersistedConfig = hasPersistedConfig ? readFileSync(configPath, "utf8") : undefined;
|
|
127
|
+
const hadStoredAccount = (runtimeConfig.codexAccounts ?? [])
|
|
128
|
+
.some(account => !account.isMain && account.id === accountId);
|
|
129
|
+
const hadVisiblePickerBinding = hadStoredAccount
|
|
130
|
+
&& codexAccountPickerEnabled(runtimeConfig)
|
|
131
|
+
&& codexAccountNamespaceEntries(runtimeConfig)
|
|
132
|
+
.some(([, boundAccountId]) => boundAccountId === accountId);
|
|
133
|
+
|
|
134
|
+
runtimeConfig.codexAccounts = (runtimeConfig.codexAccounts ?? [])
|
|
135
|
+
.filter(account => account.isMain || account.id !== accountId);
|
|
136
|
+
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
137
|
+
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
138
|
+
clearCodexAccountPin(runtimeConfig, accountId);
|
|
139
|
+
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
140
|
+
|
|
141
|
+
if (previousPersistedConfig !== undefined) {
|
|
142
|
+
try {
|
|
143
|
+
// Persist first for durable configs. Destructive cleanup below must never run for a
|
|
144
|
+
// deletion that failed to commit. Transient configs intentionally skip this write.
|
|
145
|
+
saveConfigPreservingClaudeCode(runtimeConfig);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
restoreRuntimeConfig(runtimeConfig, previousConfig);
|
|
148
|
+
try {
|
|
149
|
+
restorePersistedConfig(configPath, previousPersistedConfig);
|
|
150
|
+
} catch {
|
|
151
|
+
throw new CodexAccountDeleteRollbackError();
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
removeCodexAccountCredential(accountId);
|
|
159
|
+
purgeCodexAccountRuntimeState(accountId);
|
|
160
|
+
invalidateCodexWebSocketsForAccount(accountId);
|
|
161
|
+
} catch {
|
|
162
|
+
// Do not throw through the mutation coordinator after config.json committed: that would roll
|
|
163
|
+
// back only the SQLite generation transaction, not the already-atomic file replacement.
|
|
164
|
+
cleanupFailed = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return hadVisiblePickerBinding;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (cleanupFailed) throw new CodexAccountDeleteCleanupError();
|
|
171
|
+
return pickerVisibilityChanged;
|
|
83
172
|
}
|