@bitkyc08/opencodex 2.77.0 → 2.78.0
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 +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** A management route a capability drives. Path text only; never a handler reference. */
|
|
2
|
+
export interface CapabilityRoute {
|
|
3
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface CapabilityFlag {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly value?: "string" | "number" | "boolean";
|
|
10
|
+
readonly required?: boolean;
|
|
11
|
+
readonly summary: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How a capability emits JSON.
|
|
16
|
+
*
|
|
17
|
+
* - `payload`: the API payload, largely unwrapped.
|
|
18
|
+
* - `envelope`: a CLI-shaped object with its own schema.
|
|
19
|
+
* - `none`: no `--json` mode.
|
|
20
|
+
*/
|
|
21
|
+
export type CapabilityJsonMode = "payload" | "envelope" | "none";
|
|
22
|
+
|
|
23
|
+
export interface Capability {
|
|
24
|
+
/** Command path, e.g. `["account", "pause"]`. */
|
|
25
|
+
readonly command: readonly string[];
|
|
26
|
+
readonly summary: string;
|
|
27
|
+
readonly routes: readonly CapabilityRoute[];
|
|
28
|
+
readonly flags: readonly CapabilityFlag[];
|
|
29
|
+
readonly mutates: boolean;
|
|
30
|
+
readonly json: CapabilityJsonMode;
|
|
31
|
+
/** Complete CLI usage, including operands, when verified for this leaf. */
|
|
32
|
+
readonly usage?: string;
|
|
33
|
+
readonly details?: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Extra banner rows this capability owns, for surfaces the banner shows separately
|
|
36
|
+
* from the bare command (`ocx restore back`, `ocx doctor --reclaim-response-temps`).
|
|
37
|
+
* Without this the banner cannot equal the capability set: it legitimately carries more
|
|
38
|
+
* rows than there are commands.
|
|
39
|
+
*/
|
|
40
|
+
readonly bannerLines?: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Surfaces resolved in the CLI head, before dispatch.
|
|
45
|
+
*
|
|
46
|
+
* They belong in `ocx capabilities` output and in the banner, but not in `CLI_COMMANDS`:
|
|
47
|
+
* `--version`, `-v`, and `version` are answered at `root.ts` and exit, so none of them is
|
|
48
|
+
* a runner key to parity-check against.
|
|
49
|
+
*/
|
|
50
|
+
export interface HeadCapability {
|
|
51
|
+
readonly invocations: readonly string[];
|
|
52
|
+
readonly summary: string;
|
|
53
|
+
readonly bannerLine: string;
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
2
|
+
import { CliUsageError, RuntimeApiError, PROXY_NOT_RUNNING_MESSAGE, printData, terminalSafeText } from "./runtime-api";
|
|
3
|
+
|
|
4
|
+
function own(value: unknown, key: string): unknown {
|
|
5
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
6
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
7
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A small error/exit boundary; routes and domain DTO validation belong to callers. */
|
|
11
|
+
export async function runCatalogAction(
|
|
12
|
+
action: () => Promise<number | void>, knownErrors: Readonly<Record<string, string>> = {},
|
|
13
|
+
): Promise<number> {
|
|
14
|
+
try {
|
|
15
|
+
return (await action()) ?? 0;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error instanceof CliUsageError) {
|
|
18
|
+
// New domain parsers use static messages instead of raw JSON/validator exceptions.
|
|
19
|
+
console.error(`Error: ${terminalSafeText(error.message)}`);
|
|
20
|
+
if (error.usage) for (const line of error.usage.split("\n")) console.error(terminalSafeText(line));
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
if (error instanceof RuntimeApiError) {
|
|
24
|
+
if (error.code) {
|
|
25
|
+
console.error(`Error: ${error.code === "proxy_not_running" ? PROXY_NOT_RUNNING_MESSAGE
|
|
26
|
+
: "This listener has no management API. Run management commands on the connected Hub. No request was sent."}`);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
|
|
30
|
+
const known = typeof code === "string" && Object.hasOwn(knownErrors, code) ? knownErrors[code] : undefined;
|
|
31
|
+
const message = error.status === 404 ? "The requested record or operation was not found on the selected target."
|
|
32
|
+
: error.status === 409 ? "The selected target refused a conflicting change. Read back before trying again."
|
|
33
|
+
: error.status === 503 ? "Management API is unavailable. Use the intended running proxy; connected clients must make management changes on their Hub. A write outcome may be unknown."
|
|
34
|
+
: "The management request failed. Inspect the selected target before retrying; the write outcome may be unknown.";
|
|
35
|
+
console.error(`Error: ${terminalSafeText(known ?? message)}`);
|
|
36
|
+
return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
|
|
37
|
+
}
|
|
38
|
+
console.error("Error: Management operation did not return a usable outcome. Inspect the selected target before retrying; no rollback is implied.");
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Caller supplies a validated, rebuilt public DTO, not an arbitrary server body.
|
|
45
|
+
* This helper normalizes only the shared catalog disposition and preserves its exit.
|
|
46
|
+
*/
|
|
47
|
+
export function printCatalogResult(
|
|
48
|
+
data: Record<string, unknown>, refresh: unknown, wantsJson: boolean, lines: readonly string[],
|
|
49
|
+
): number {
|
|
50
|
+
const catalogRefresh = normalizeCatalogDisposition(refresh);
|
|
51
|
+
if (catalogRefresh === null) throw new Error("Invalid catalog outcome");
|
|
52
|
+
const converged = catalogRefresh.status === "committed" && !catalogRefresh.degraded;
|
|
53
|
+
const outcome = converged ? "Catalog committed."
|
|
54
|
+
: "Change saved, but catalog convergence is incomplete. Read back before retrying.";
|
|
55
|
+
printData({ ...data, catalogRefresh }, wantsJson, [...lines, outcome]);
|
|
56
|
+
return converged ? 0 : 1;
|
|
57
|
+
}
|
|
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { loadConfig } from "../config";
|
|
5
|
+
import { readConfigFileSnapshot } from "../config/diagnostics";
|
|
6
|
+
import { chatgptDesktopConfigIssue } from "../config/schema/chatgpt-desktop";
|
|
5
7
|
import {
|
|
6
8
|
chatgptShimLauncherPath,
|
|
7
9
|
resolveChatgptCodexBinary,
|
|
@@ -21,6 +23,20 @@ function run(command: string, args: string[]) {
|
|
|
21
23
|
return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Why the config file's `chatgptDesktop` block reads as absent, or null when it is valid, absent
|
|
28
|
+
* or the file cannot be parsed (the config loader reports that case itself).
|
|
29
|
+
*/
|
|
30
|
+
function chatgptDesktopIssueInFile(): string | null {
|
|
31
|
+
const { raw } = readConfigFileSnapshot();
|
|
32
|
+
if (raw === undefined) return null;
|
|
33
|
+
try {
|
|
34
|
+
return chatgptDesktopConfigIssue(JSON.parse(raw.replace(/^\uFEFF/, "")));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/**
|
|
25
41
|
* The installed app, found and confirmed by bundle identifier (com.openai.codex) the same way the
|
|
26
42
|
* desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
|
|
@@ -90,7 +106,10 @@ export async function handleChatgptCommand(args: string[], platform: NodeJS.Plat
|
|
|
90
106
|
const install = discoverApp();
|
|
91
107
|
if (sub === "status") {
|
|
92
108
|
const app = install ? appState(install, launcher) : { running: false, shim: false };
|
|
93
|
-
|
|
109
|
+
const configIssue = config.chatgptDesktop?.appServerShim === true ? null : chatgptDesktopIssueInFile();
|
|
110
|
+
const flag = config.chatgptDesktop?.appServerShim === true ? "on"
|
|
111
|
+
: configIssue ? `off (config.json ${configIssue}; the whole chatgptDesktop block is ignored)` : "off";
|
|
112
|
+
console.log(`app-server shim (experimental): ${flag}
|
|
94
113
|
launcher: ${existsSync(launcher) ? "present" : "absent"}
|
|
95
114
|
app: ${install ? (app.running ? "running" : "not running") : "not installed"}
|
|
96
115
|
CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
|
|
@@ -104,7 +123,11 @@ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
|
|
|
104
123
|
let binary: string | undefined;
|
|
105
124
|
if (sub === "launch") {
|
|
106
125
|
if (config.chatgptDesktop?.appServerShim !== true) {
|
|
107
|
-
|
|
126
|
+
// Read the file only to explain a refusal; restore and a valid launch never touch it.
|
|
127
|
+
const configIssue = chatgptDesktopIssueInFile();
|
|
128
|
+
console.error(configIssue
|
|
129
|
+
? `Experimental shim disabled: config.json ${configIssue}. The whole chatgptDesktop block is ignored until that is fixed.`
|
|
130
|
+
: "Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.");
|
|
108
131
|
return 1;
|
|
109
132
|
}
|
|
110
133
|
binary = resolveChatgptCodexBinary(install.root) ?? undefined;
|
|
@@ -64,6 +64,23 @@ export async function reconcileClientStartupBeforeReady<T>(
|
|
|
64
64
|
return result;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Establish catalog observation before any unrelated startup reconciliation awaits. */
|
|
68
|
+
export async function syncCodexBeforeCatalogObservation<T>(
|
|
69
|
+
gate: ReadinessGate,
|
|
70
|
+
syncCodex: (gate: ReadinessGate) => Promise<T>,
|
|
71
|
+
startObservation: () => void,
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
let ready = false;
|
|
74
|
+
const forwardingGate: ReadinessGate = {
|
|
75
|
+
getStatus: () => gate.getStatus(),
|
|
76
|
+
markReady: () => { ready = true; gate.markReady(); },
|
|
77
|
+
markFailed: () => { ready = false; gate.markFailed(); },
|
|
78
|
+
};
|
|
79
|
+
const result = await syncCodex(forwardingGate);
|
|
80
|
+
if (ready) startObservation();
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Reconcile the generated Claude Code roster after the proxy listener is live.
|
|
69
86
|
*
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** Runtime-owned Desktop profile editing. Saving never applies a native configuration. */
|
|
2
|
+
import { DESKTOP_FAMILIES, parseDesktopProfile, type DesktopProfile } from "../claude/desktop-profile";
|
|
3
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
4
|
+
import { readJsonInput, serializeManagementJson } from "./json-input";
|
|
5
|
+
import { CliUsageError, printData, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
|
|
6
|
+
|
|
7
|
+
export interface DesktopProfileCliDeps extends RuntimeApiDeps {
|
|
8
|
+
runtimeRequestImpl?: typeof runtimeRequest;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const USAGE = "Usage: ocx claude desktop profile show [--json]\n ocx claude desktop profile import FILE|- [--json]";
|
|
12
|
+
|
|
13
|
+
function record(value: unknown): Record<string, unknown> {
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Desktop state");
|
|
15
|
+
return value as Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function text(value: unknown): string {
|
|
19
|
+
if (typeof value !== "string" || !value.length) throw new Error("Invalid Desktop text");
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function boolean(value: unknown): boolean {
|
|
24
|
+
if (typeof value !== "boolean") throw new Error("Invalid Desktop flag");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function modelIdentity(row: Record<string, unknown>) {
|
|
29
|
+
const route = text(row.route);
|
|
30
|
+
if (!route.includes("/")) throw new Error("Invalid Desktop route");
|
|
31
|
+
if (row.contextWindow !== undefined && (typeof row.contextWindow !== "number"
|
|
32
|
+
|| !Number.isSafeInteger(row.contextWindow) || row.contextWindow <= 0)) throw new Error("Invalid Desktop context window");
|
|
33
|
+
return { route, label: text(row.label), ...(row.contextWindow === undefined ? {} : { contextWindow: row.contextWindow as number }) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function desktopState(raw: unknown, saved: boolean) {
|
|
37
|
+
const value = record(raw);
|
|
38
|
+
if (saved && value.ok !== true) throw new Error("Desktop save was not confirmed");
|
|
39
|
+
const profile = parseDesktopProfile(value.profile);
|
|
40
|
+
if (!Array.isArray(value.models) || !Array.isArray(value.rendered)
|
|
41
|
+
|| !Number.isSafeInteger(value.port) || (value.port as number) < 1 || (value.port as number) > 65535) {
|
|
42
|
+
throw new Error("Invalid Desktop state");
|
|
43
|
+
}
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
const models = value.models.map(rawModel => {
|
|
46
|
+
const row = record(rawModel), identity = modelIdentity(row), assignment = record(row.assignment);
|
|
47
|
+
const expected = profile.assignments[identity.route];
|
|
48
|
+
if (!expected || seen.has(identity.route) || assignment.family !== expected.family || assignment.alias !== expected.alias) {
|
|
49
|
+
throw new Error("Invalid Desktop assignment");
|
|
50
|
+
}
|
|
51
|
+
seen.add(identity.route);
|
|
52
|
+
return { ...identity, available: boolean(row.available), effortSupported: boolean(row.effortSupported),
|
|
53
|
+
supports1m: boolean(row.supports1m), assignment: { family: expected.family, alias: expected.alias } };
|
|
54
|
+
});
|
|
55
|
+
if (seen.size !== Object.keys(profile.assignments).length) throw new Error("Incomplete Desktop models");
|
|
56
|
+
const renderedSeen = new Set<string>();
|
|
57
|
+
const rendered = value.rendered.map(rawModel => {
|
|
58
|
+
const row = record(rawModel), identity = modelIdentity(row);
|
|
59
|
+
if (!DESKTOP_FAMILIES.includes(row.family as typeof DESKTOP_FAMILIES[number])
|
|
60
|
+
|| profile.assignments[identity.route]?.family !== row.family || renderedSeen.has(identity.route)
|
|
61
|
+
|| !models.some(model => model.route === identity.route && model.available)) throw new Error("Invalid rendered Desktop model");
|
|
62
|
+
renderedSeen.add(identity.route);
|
|
63
|
+
return { ...identity, name: text(row.name), family: row.family as typeof DESKTOP_FAMILIES[number],
|
|
64
|
+
isFamilyDefault: boolean(row.isFamilyDefault), supports1m: boolean(row.supports1m) };
|
|
65
|
+
});
|
|
66
|
+
if (renderedSeen.size !== models.filter(model => model.available).length) throw new Error("Incomplete rendered Desktop models");
|
|
67
|
+
return { ...(saved ? { ok: true as const } : {}), profile, models, rendered, port: value.port as number };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Arguments start immediately after `claude desktop profile`. */
|
|
71
|
+
export async function handleClaudeDesktopProfileCommand(argv: string[], deps: DesktopProfileCliDeps = {}): Promise<number> {
|
|
72
|
+
return runCatalogAction(async () => {
|
|
73
|
+
const args = [...argv], wantsJson = takeFlag(args, "--json");
|
|
74
|
+
const action = args.shift();
|
|
75
|
+
if ((action !== "show" && action !== "import") || (action === "show" && args.length !== 0)
|
|
76
|
+
|| (action === "import" && (args.length !== 1 || !args[0]?.trim() || (args[0]!.startsWith("-") && args[0] !== "-")))) {
|
|
77
|
+
throw new CliUsageError("Desktop profile accepts show or import with an explicit file or '-'. Saving and applying are separate actions.", USAGE);
|
|
78
|
+
}
|
|
79
|
+
let body: string | undefined;
|
|
80
|
+
if (action === "import") {
|
|
81
|
+
const input = await readJsonInput(args[0]!, deps);
|
|
82
|
+
let profile: DesktopProfile;
|
|
83
|
+
try { profile = parseDesktopProfile(input); }
|
|
84
|
+
catch { throw new CliUsageError("Desktop profile input is invalid. Supply a version-1 profile with valid assignments and family defaults."); }
|
|
85
|
+
body = serializeManagementJson({ profile });
|
|
86
|
+
}
|
|
87
|
+
const raw = await (deps.runtimeRequestImpl ?? runtimeRequest)("/api/claude-desktop", {
|
|
88
|
+
method: action === "show" ? "GET" : "PUT", redirect: "error",
|
|
89
|
+
...(body === undefined ? {} : { headers: { "content-type": "application/json" }, body }),
|
|
90
|
+
}, deps);
|
|
91
|
+
const state = desktopState(raw, action === "import");
|
|
92
|
+
printData(state, wantsJson, [
|
|
93
|
+
action === "import" ? "Desktop profile saved on the selected proxy. It has not been applied by this command." : "Desktop profile on the selected proxy.",
|
|
94
|
+
`Models: ${state.models.length}; available: ${state.models.filter(model => model.available).length}; port: ${state.port}`,
|
|
95
|
+
...DESKTOP_FAMILIES.map(family => `${family}: ${state.profile.defaults[family] ?? "none"}`),
|
|
96
|
+
...(action === "import" ? ["Next: review with ocx claude desktop profile show; apply separately with ocx claude desktop apply."] : []),
|
|
97
|
+
]);
|
|
98
|
+
return 0;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -321,6 +321,27 @@ export function defaultDesktopApplyMode(
|
|
|
321
321
|
return connection.kind === "connected" ? "gateway" : "first-party";
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
const DESKTOP_APPLY_FLAGS = ["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"] as const;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A rejected argument is never echoed: an option-shaped token can carry a value (`-tVALUE`,
|
|
328
|
+
* `--opt=VALUE`, a dash-prefixed operand) and any token can carry terminal control characters, so
|
|
329
|
+
* the error counts them and lists the options that do exist instead.
|
|
330
|
+
*/
|
|
331
|
+
function unknownApplyArgsError(count: number): string {
|
|
332
|
+
return `알 수 없는 인자 ${count}개 (값은 표시하지 않습니다). 사용 가능한 옵션: ${DESKTOP_APPLY_FLAGS.join(" ")}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A rejected route is not echoed: whatever sits in that position may be a misplaced credential. */
|
|
336
|
+
const UNAVAILABLE_ROUTE = "현재 사용할 수 없는 모델입니다. 사용 가능한 모델은 ocx claude desktop show로 확인하세요.";
|
|
337
|
+
|
|
338
|
+
/** Profile files are user-chosen paths; a failure reports the operation and error code only. */
|
|
339
|
+
function profileFileError(operation: "export" | "import", error: unknown): Error {
|
|
340
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
341
|
+
const reason = typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : error instanceof SyntaxError ? "invalid JSON" : "failed";
|
|
342
|
+
return new Error(`Claude Desktop 프로필 ${operation === "export" ? "내보내기" : "가져오기"} 실패: ${reason}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
export function parseDesktopApplyArgs(
|
|
325
346
|
flags: string[],
|
|
326
347
|
config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
|
|
@@ -330,8 +351,8 @@ export function parseDesktopApplyArgs(
|
|
|
330
351
|
const wantsFirstParty = flags.includes("--first-party");
|
|
331
352
|
const wantsGateway = flags.includes("--gateway") || shapeFlags.length > 0;
|
|
332
353
|
if (wantsFirstParty && wantsGateway) return { error: "--first-party cannot be combined with --gateway or gateway shape flags." };
|
|
333
|
-
const unknown = flags.filter(arg => !
|
|
334
|
-
if (unknown.length > 0) return { error:
|
|
354
|
+
const unknown = flags.filter(arg => !(DESKTOP_APPLY_FLAGS as readonly string[]).includes(arg));
|
|
355
|
+
if (unknown.length > 0) return { error: unknownApplyArgsError(unknown.length) };
|
|
335
356
|
const kind: ClaudeDesktopMode = wantsFirstParty
|
|
336
357
|
? "first-party"
|
|
337
358
|
: wantsGateway ? "gateway" : defaultDesktopApplyMode(config, readClientConnectionState(), observed ?? observeClaudeDesktopMode(config));
|
|
@@ -679,6 +700,12 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
679
700
|
printDesktopHelp();
|
|
680
701
|
return 0;
|
|
681
702
|
}
|
|
703
|
+
if (command === "profile") {
|
|
704
|
+
const { handleClaudeDesktopProfileCommand } = await import("./claude-desktop-profile");
|
|
705
|
+
return handleClaudeDesktopProfileCommand(argv.slice(1), {
|
|
706
|
+
runtimeRequestImpl: deps.runtimeRequestImpl, findLiveProxy: deps.findLiveProxyImpl,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
682
709
|
|
|
683
710
|
// Mode flags remain apply aliases and are parsed before subcommands.
|
|
684
711
|
const applyFlags = argv.filter(arg => APPLY_FLAGS.includes(arg));
|
|
@@ -806,7 +833,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
806
833
|
if (command === "move") {
|
|
807
834
|
const [, route, familyRaw, ...flags] = argv;
|
|
808
835
|
if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new CliUsageError("Usage: ocx claude desktop move <route> <family> [--default]");
|
|
809
|
-
if (!state.models.some(model => model.route === route && model.available)) throw new Error(
|
|
836
|
+
if (!state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
|
|
810
837
|
const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
|
|
811
838
|
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
812
839
|
console.log(`${route} 모델을 ${familyRaw} 그룹으로 옮겼습니다.`);
|
|
@@ -816,7 +843,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
816
843
|
const [, familyRaw, routeRaw] = argv;
|
|
817
844
|
if (!isFamily(familyRaw) || !routeRaw || argv.length !== 3) throw new CliUsageError("Usage: ocx claude desktop default <family> <route|none>");
|
|
818
845
|
const route = routeRaw === "none" ? null : routeRaw;
|
|
819
|
-
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(
|
|
846
|
+
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
|
|
820
847
|
const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
|
|
821
848
|
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
822
849
|
console.log(`${familyRaw} 기본 모델을 ${route ?? "없음"}으로 지정했습니다.`);
|
|
@@ -827,14 +854,20 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
827
854
|
if (!target || argv.length !== 2) throw new CliUsageError("Usage: ocx claude desktop export <path|->");
|
|
828
855
|
const json = JSON.stringify(state.profile, null, 2) + "\n";
|
|
829
856
|
if (target === "-") process.stdout.write(json);
|
|
830
|
-
else
|
|
857
|
+
else {
|
|
858
|
+
try { writeFileSync(resolve(target), json, { encoding: "utf8", mode: 0o600 }); }
|
|
859
|
+
catch (error) { throw profileFileError("export", error); }
|
|
860
|
+
}
|
|
831
861
|
return 0;
|
|
832
862
|
}
|
|
833
863
|
if (command === "import") {
|
|
834
864
|
const source = argv[1];
|
|
835
865
|
const flags = argv.slice(2);
|
|
836
866
|
if (!source || flags.some(flag => flag !== "--apply")) throw new CliUsageError("Usage: ocx claude desktop import <path> [--apply]");
|
|
837
|
-
|
|
867
|
+
let raw: unknown;
|
|
868
|
+
try { raw = JSON.parse(readFileSync(resolve(source), "utf8")); }
|
|
869
|
+
catch (error) { throw profileFileError("import", error); }
|
|
870
|
+
const profile = parseDesktopProfile(raw);
|
|
838
871
|
const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
|
|
839
872
|
if (flags.includes("--apply")) assertNoClientDisconnectPending();
|
|
840
873
|
if (flags.includes("--apply") && readClientConnectionState().kind !== "disconnected") {
|
|
@@ -17,6 +17,7 @@ const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
|
|
|
17
17
|
|
|
18
18
|
export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
|
|
19
19
|
if (command === "uninstall" || command === "remove") return true;
|
|
20
|
+
if (command === "status" || command === "doctor") return true;
|
|
20
21
|
// `lab` is read-only inspection; it must not trigger shim side effects.
|
|
21
22
|
if (command === "lab") return true;
|
|
22
23
|
// `resolve` is read-only inspection for embedding shells: a lookup made to populate
|
|
@@ -29,7 +30,7 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
|
|
|
29
30
|
// or future actions. A later `apply` implementation must own its preflight.
|
|
30
31
|
if (command === "system" && args[1] === "codex-cli-update") return true;
|
|
31
32
|
if (command === "__update-badge") return true;
|
|
32
|
-
return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
|
|
33
|
+
return command === "codex-shim" && ["install", "status", "uninstall", "remove"].includes(args[1] ?? "");
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function maybeAutoRestoreCodexShim(
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type CodexRoutingKind,
|
|
5
5
|
} from "../codex/inject";
|
|
6
6
|
import { diagnoseCodexShim, type CodexShimDiagnostic } from "../codex/shim";
|
|
7
|
+
import { overlayActivationHint } from "../codex/shim-overlay";
|
|
7
8
|
import { findFirstCodexOnPath, type CodexPathCandidate } from "../codex/shim-path-resolution";
|
|
8
9
|
import { loadConfig, resolveEnvValue } from "../config";
|
|
9
10
|
|
|
@@ -32,7 +33,7 @@ export interface CodexConnectShimReadiness {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface CodexConnectShimInspectionDeps {
|
|
35
|
-
diagnose?: () => Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
|
|
36
|
+
diagnose?: () => Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
36
37
|
findOnPath?: () => CodexPathCandidate | null;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -42,10 +43,29 @@ const CODEX_TOKEN_NOTE = "The connected Codex config uses OPENCODEX_API_AUTH_TOK
|
|
|
42
43
|
const CODEX_TOKEN_ACTION = `${CODEX_TOKEN_NOTE} Run 'ocx codex-shim install' to repair it.`;
|
|
43
44
|
|
|
44
45
|
export function codexConnectShimReadiness(inputs: {
|
|
45
|
-
diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
|
|
46
|
+
diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
46
47
|
commandPath: string | null;
|
|
47
48
|
commandIsShim?: boolean;
|
|
48
49
|
}): CodexConnectShimReadiness {
|
|
50
|
+
if (inputs.diagnosis.runnable && inputs.diagnosis.active !== undefined) {
|
|
51
|
+
if (inputs.diagnosis.active === null) {
|
|
52
|
+
return {
|
|
53
|
+
status: "unverified",
|
|
54
|
+
message: `PATH activation could not be verified. Shim state: ${inputs.diagnosis.summary}. `
|
|
55
|
+
+ `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!inputs.diagnosis.active) {
|
|
59
|
+
return {
|
|
60
|
+
status: "missing",
|
|
61
|
+
message: `installed but not active; ${inputs.commandPath
|
|
62
|
+
? `PATH resolves 'codex' to ${inputs.commandPath}`
|
|
63
|
+
: "no 'codex' executable was found on PATH"}. `
|
|
64
|
+
+ `Shim state: ${inputs.diagnosis.summary}. ${overlayActivationHint()} ${CODEX_TOKEN_NOTE}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { status: "ready", message: "installed and healthy" };
|
|
68
|
+
}
|
|
49
69
|
if (inputs.diagnosis.installed && !inputs.diagnosis.healthy) {
|
|
50
70
|
return {
|
|
51
71
|
status: "unhealthy",
|
|
@@ -87,7 +107,7 @@ export function codexConnectShimReadiness(inputs: {
|
|
|
87
107
|
export function inspectCodexShimForConnect(
|
|
88
108
|
deps: CodexConnectShimInspectionDeps = {},
|
|
89
109
|
): CodexConnectShimReadiness {
|
|
90
|
-
let diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary">;
|
|
110
|
+
let diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
|
|
91
111
|
try {
|
|
92
112
|
diagnosis = (deps.diagnose ?? diagnoseCodexShim)();
|
|
93
113
|
} catch {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { JEV_MAX_CANDIDATE_FIELD_CHARS } from "../combos/types";
|
|
2
|
+
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
3
|
+
import { readJsonInput } from "./json-input";
|
|
4
|
+
import { CliUsageError, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
|
|
5
|
+
|
|
6
|
+
type ComboInput = {
|
|
7
|
+
targets?: Array<Record<string, unknown>>;
|
|
8
|
+
imageInput?: "auto" | "disabled";
|
|
9
|
+
reasoningEffortMode?: "strict" | "adaptive";
|
|
10
|
+
nativeAlias?: boolean;
|
|
11
|
+
};
|
|
12
|
+
const TARGET_KEYS = new Set(["provider", "model", "weight", "reasoningEfforts", "modelProfile", "lastResort"]);
|
|
13
|
+
|
|
14
|
+
export function parseComboTargets(value: unknown): Array<Record<string, unknown>> {
|
|
15
|
+
if (!Array.isArray(value) || value.length === 0) throw new CliUsageError("Targets input must be a nonempty array");
|
|
16
|
+
return value.map(target => {
|
|
17
|
+
if (target === null || typeof target !== "object" || Array.isArray(target)
|
|
18
|
+
|| Object.keys(target).some(key => !TARGET_KEYS.has(key))) {
|
|
19
|
+
throw new CliUsageError("Targets input contains an invalid object or unsupported field");
|
|
20
|
+
}
|
|
21
|
+
for (const key of ["provider", "model"]) {
|
|
22
|
+
if (typeof target[key] !== "string" || !target[key].trim() || /[\u0000-\u001f\u007f]/u.test(target[key])) {
|
|
23
|
+
throw new CliUsageError("Each target requires a nonblank provider and raw model ID without control characters");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (Object.hasOwn(target, "weight") && (typeof target.weight !== "number"
|
|
27
|
+
|| !Number.isInteger(target.weight) || target.weight < 1 || target.weight > 10_000)) {
|
|
28
|
+
throw new CliUsageError("Target weight must be an integer from 1 to 10000");
|
|
29
|
+
}
|
|
30
|
+
if (Object.hasOwn(target, "reasoningEfforts") && (!Array.isArray(target.reasoningEfforts)
|
|
31
|
+
|| target.reasoningEfforts.length === 0
|
|
32
|
+
|| target.reasoningEfforts.some((effort: unknown) => typeof effort !== "string" || !isCodexReasoningEffort(effort))
|
|
33
|
+
|| new Set(target.reasoningEfforts).size !== target.reasoningEfforts.length)) {
|
|
34
|
+
throw new CliUsageError("Target reasoningEfforts must contain unique low, medium, high, xhigh, max or ultra values");
|
|
35
|
+
}
|
|
36
|
+
if (Object.hasOwn(target, "modelProfile") && (typeof target.modelProfile !== "string"
|
|
37
|
+
|| !target.modelProfile.trim() || target.modelProfile.length > JEV_MAX_CANDIDATE_FIELD_CHARS
|
|
38
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(target.modelProfile))) {
|
|
39
|
+
throw new CliUsageError("Target modelProfile must be nonblank, at most 512 characters, with no controls except tab or newline");
|
|
40
|
+
}
|
|
41
|
+
if (Object.hasOwn(target, "lastResort") && typeof target.lastResort !== "boolean") {
|
|
42
|
+
throw new CliUsageError("Target lastResort must be a boolean");
|
|
43
|
+
}
|
|
44
|
+
// Preserve raw slashes, target order, explicit false and omitted metadata.
|
|
45
|
+
return { ...target };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function takeNativeAlias(args: string[]): boolean | undefined {
|
|
50
|
+
const flag = "--native-alias";
|
|
51
|
+
const positions = args.flatMap((value, index) => value === flag || value.startsWith(`${flag}=`) ? [index] : []);
|
|
52
|
+
if (positions.length === 0) return undefined;
|
|
53
|
+
if (positions.length !== 1) throw new CliUsageError("--native-alias was given more than once");
|
|
54
|
+
const index = positions[0]!;
|
|
55
|
+
const token = args[index]!;
|
|
56
|
+
const inline = token !== flag;
|
|
57
|
+
const next = inline ? token.slice(flag.length + 1) : args[index + 1];
|
|
58
|
+
if (!inline && (next === undefined || next.startsWith("--"))) {
|
|
59
|
+
args.splice(index, 1);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (next !== "on" && next !== "off") throw new CliUsageError("--native-alias accepts on, off, or the legacy bare flag");
|
|
63
|
+
args.splice(index, inline ? 1 : 2);
|
|
64
|
+
return next === "on";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Consume only extension options, before the parent set flow resolves a target or reads HTTP. */
|
|
68
|
+
export async function prepareComboInput(args: string[], deps: RuntimeApiDeps = {}): Promise<ComboInput> {
|
|
69
|
+
const file = takeOptionWithSyntax(args, "--targets-file")?.value;
|
|
70
|
+
const image = takeOptionWithSyntax(args, "--image-input")?.value;
|
|
71
|
+
const reasoning = takeOptionWithSyntax(args, "--reasoning-effort-mode")?.value;
|
|
72
|
+
const nativeAlias = takeNativeAlias(args);
|
|
73
|
+
if (file !== undefined && args.some(arg => arg === "--targets" || arg.startsWith("--targets="))) {
|
|
74
|
+
throw new CliUsageError("--targets-file cannot be combined with --targets");
|
|
75
|
+
}
|
|
76
|
+
if (image !== undefined && image !== "auto" && image !== "disabled") {
|
|
77
|
+
throw new CliUsageError("--image-input must be auto or disabled");
|
|
78
|
+
}
|
|
79
|
+
if (reasoning !== undefined && reasoning !== "strict" && reasoning !== "adaptive") {
|
|
80
|
+
throw new CliUsageError("--reasoning-effort-mode must be strict or adaptive");
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
...(file === undefined ? {} : { targets: parseComboTargets(await readJsonInput(file, deps)) }),
|
|
84
|
+
...(image === undefined ? {} : { imageInput: image }),
|
|
85
|
+
...(reasoning === undefined ? {} : { reasoningEffortMode: reasoning }),
|
|
86
|
+
...(nativeAlias === undefined ? {} : { nativeAlias }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { parseComboTargets } from "./combo-input";
|
|
2
|
+
import { printCatalogResult } from "./catalog-command-result";
|
|
3
|
+
|
|
4
|
+
const ENUMS: Readonly<Record<string, readonly (string | null)[]>> = {
|
|
5
|
+
strategy: ["failover", "round-robin", "random", "least-used", "reset-window", "jev"],
|
|
6
|
+
defaultEffort: [null, "low", "medium", "high", "xhigh", "max", "ultra"],
|
|
7
|
+
defaultEffortMode: ["fallback", "force"], reasoningEffortMode: ["strict", "adaptive"],
|
|
8
|
+
imageInput: ["auto", "disabled"], cooldownWaitPolicy: ["before-last-resort"],
|
|
9
|
+
};
|
|
10
|
+
const STRINGS = new Set(["alias", "displayName", "decisionProvider", "decisionModel"]);
|
|
11
|
+
const RANGES: Readonly<Record<string, readonly [number, number]>> = {
|
|
12
|
+
stickyLimit: [1, 100], cooldownMs: [1, 600_000], waitForCooldownMs: [0, 600_000],
|
|
13
|
+
decisionTimeoutMs: [1_000, 120_000],
|
|
14
|
+
};
|
|
15
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
17
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
18
|
+
}
|
|
19
|
+
function invalid(): never { throw new Error("Invalid combo receipt"); }
|
|
20
|
+
|
|
21
|
+
/** Shape/field ownership only; target graph/alias semantics remain on the server. */
|
|
22
|
+
function publicCombo(value: unknown): Record<string, unknown> {
|
|
23
|
+
if (!record(value)) invalid();
|
|
24
|
+
let targets: Array<Record<string, unknown>>;
|
|
25
|
+
try { targets = parseComboTargets(value.targets); } catch { return invalid(); }
|
|
26
|
+
const result: Record<string, unknown> = { targets };
|
|
27
|
+
for (const [key, child] of Object.entries(value)) {
|
|
28
|
+
if (key === "targets") continue;
|
|
29
|
+
if (Object.hasOwn(ENUMS, key)) {
|
|
30
|
+
if (!ENUMS[key]!.includes(child as string | null)) invalid();
|
|
31
|
+
} else if (STRINGS.has(key)) {
|
|
32
|
+
if (typeof child !== "string" || child.length > 4096) invalid();
|
|
33
|
+
} else if (Object.hasOwn(RANGES, key)) {
|
|
34
|
+
const [min, max] = RANGES[key]!;
|
|
35
|
+
if (typeof child !== "number" || !Number.isSafeInteger(child) || child < min || child > max) invalid();
|
|
36
|
+
} else if (key === "nativeAlias") {
|
|
37
|
+
if (typeof child !== "boolean") invalid();
|
|
38
|
+
} else invalid();
|
|
39
|
+
result[key] = child;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function printComboWriteResult(value: unknown, id: string, wantsJson: boolean): number {
|
|
45
|
+
if (!record(value) || value.success !== true || value.id !== id || typeof value.model !== "string"
|
|
46
|
+
|| Object.keys(value).some(key => !["success", "id", "model", "combo", "catalogRefresh"].includes(key))) invalid();
|
|
47
|
+
const combo = publicCombo(value.combo);
|
|
48
|
+
const model = typeof combo.alias === "string" && combo.alias.trim() ? combo.alias.trim() : `combo/${id}`;
|
|
49
|
+
if (value.model !== model) invalid();
|
|
50
|
+
return printCatalogResult({ success: true, id, model: value.model, combo }, value.catalogRefresh,
|
|
51
|
+
wantsJson, [`Saved combo ${id}.`]);
|
|
52
|
+
}
|