@bitkyc08/opencodex 2.75.0 → 2.76.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/README.md +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { loadConfig } from "../config";
|
|
5
|
+
import {
|
|
6
|
+
chatgptShimLauncherPath,
|
|
7
|
+
resolveChatgptCodexBinary,
|
|
8
|
+
writeChatgptShimLauncher,
|
|
9
|
+
} from "../chatgpt/app-server-shim/launcher";
|
|
10
|
+
import { untrustedChatgptBundleReason } from "../chatgpt/app-server-shim/bundle-trust";
|
|
11
|
+
import { darwinDefaultExec, darwinDesktopAppAdapter } from "../codex/desktop-app/darwin";
|
|
12
|
+
import type { DesktopAppInstall } from "../codex/desktop-app/types";
|
|
13
|
+
|
|
14
|
+
const USAGE = `Usage (experimental, macOS only):
|
|
15
|
+
ocx chatgpt launch Relaunch ChatGPT with the experimental app-server shim (requires appServerShim: true)
|
|
16
|
+
ocx chatgpt restore Relaunch ChatGPT without the experimental shim and remove its launcher
|
|
17
|
+
ocx chatgpt status Inspect experimental flag, launcher and running app environment`;
|
|
18
|
+
|
|
19
|
+
function run(command: string, args: string[]) {
|
|
20
|
+
const result = spawnSync(command, args, { encoding: "utf8", timeout: 5000 });
|
|
21
|
+
return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The installed app, found and confirmed by bundle identifier (com.openai.codex) the same way the
|
|
26
|
+
* desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
|
|
27
|
+
* relaunching and the binary path all key on this verified bundle rather than on the name.
|
|
28
|
+
*/
|
|
29
|
+
function discoverApp(): DesktopAppInstall | null {
|
|
30
|
+
try {
|
|
31
|
+
return darwinDesktopAppAdapter.discover(darwinDefaultExec);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Only inspect the verified bundle's own process; never print the environment being inspected.
|
|
39
|
+
* `-a` keeps ancestors in the match: when ocx runs inside a ChatGPT/Codex session the app is
|
|
40
|
+
* one of this process's ancestors, and plain `pgrep -x` would report it as not running.
|
|
41
|
+
*/
|
|
42
|
+
function appState(install: DesktopAppInstall, launcher: string): { running: boolean; shim: boolean } {
|
|
43
|
+
const shell = join(install.root, "Contents", "MacOS", "ChatGPT");
|
|
44
|
+
// Only this user's processes: another account's ChatGPT can neither be quit nor relaunched here.
|
|
45
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
46
|
+
const pids = run("pgrep", [...(uid === undefined ? [] : ["-U", String(uid)]), "-a", "-x", "ChatGPT"]);
|
|
47
|
+
if (!pids.ok) return { running: false, shim: false };
|
|
48
|
+
for (const pid of pids.output.split(/\s+/).filter(value => /^\d+$/.test(value))) {
|
|
49
|
+
const command = run("ps", ["eww", "-o", "command=", "-p", pid]);
|
|
50
|
+
if (command.ok && (command.output === shell || command.output.startsWith(`${shell} `))) {
|
|
51
|
+
const marker = `CODEX_CLI_PATH=${launcher}`;
|
|
52
|
+
const start = command.output.indexOf(marker);
|
|
53
|
+
return { running: true, shim: start >= 0 && (start === 0 || command.output[start - 1] === " ")
|
|
54
|
+
&& (start + marker.length === command.output.length || command.output[start + marker.length] === " ") };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { running: false, shim: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Adapted from #5947: open ignores new launch settings until the previous app exits. */
|
|
61
|
+
async function quitApp(install: DesktopAppInstall, launcher: string): Promise<boolean> {
|
|
62
|
+
if (!appState(install, launcher).running) return true;
|
|
63
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
64
|
+
run("/usr/bin/osascript", ["-e", `quit app id "${install.id}"`]);
|
|
65
|
+
for (let poll = 0; poll < 20; poll++) {
|
|
66
|
+
if (!appState(install, launcher).running) return true;
|
|
67
|
+
await Bun.sleep(250);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return !appState(install, launcher).running;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleChatgptCommand(args: string[], platform: NodeJS.Platform = process.platform): Promise<number> {
|
|
74
|
+
const sub = args[0];
|
|
75
|
+
if (!sub || ["help", "--help", "-h"].includes(sub)) {
|
|
76
|
+
console.log(USAGE);
|
|
77
|
+
return sub ? 0 : 64;
|
|
78
|
+
}
|
|
79
|
+
if (!["launch", "restore", "status"].includes(sub) || args.length !== 1) {
|
|
80
|
+
console.error(USAGE);
|
|
81
|
+
return 64;
|
|
82
|
+
}
|
|
83
|
+
if (platform !== "darwin") {
|
|
84
|
+
console.error("ChatGPT app-server shim (experimental): macOS only.");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const config = loadConfig();
|
|
89
|
+
const launcher = chatgptShimLauncherPath();
|
|
90
|
+
const install = discoverApp();
|
|
91
|
+
if (sub === "status") {
|
|
92
|
+
const app = install ? appState(install, launcher) : { running: false, shim: false };
|
|
93
|
+
console.log(`app-server shim (experimental): ${config.chatgptDesktop?.appServerShim === true ? "on" : "off"}
|
|
94
|
+
launcher: ${existsSync(launcher) ? "present" : "absent"}
|
|
95
|
+
app: ${install ? (app.running ? "running" : "not running") : "not installed"}
|
|
96
|
+
CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (!install) {
|
|
100
|
+
if (sub === "restore") rmSync(launcher, { force: true });
|
|
101
|
+
console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
if (sub === "launch") {
|
|
105
|
+
if (config.chatgptDesktop?.appServerShim !== true) {
|
|
106
|
+
console.error('Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.');
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const binary = resolveChatgptCodexBinary(install.root);
|
|
110
|
+
if (!binary) {
|
|
111
|
+
console.error(`No bundled app-server binary was found in ${install.root}; the shim cannot launch this build.`);
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const untrusted = untrustedChatgptBundleReason(install.root, binary);
|
|
115
|
+
if (untrusted) {
|
|
116
|
+
console.error(`Refusing to launch the shim: ${untrusted}.`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
writeChatgptShimLauncher(undefined, binary);
|
|
120
|
+
}
|
|
121
|
+
if (!(await quitApp(install, launcher))) {
|
|
122
|
+
console.error("ChatGPT did not quit; quit it manually and retry.");
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
// Remove an inherited override too: restore must launch without CODEX_CLI_PATH.
|
|
126
|
+
const env = { ...process.env };
|
|
127
|
+
delete env.CODEX_CLI_PATH;
|
|
128
|
+
// Open the verified bundle by path, so the relaunch is the same app that was quit.
|
|
129
|
+
const result = spawnSync("/usr/bin/open", ["-a", install.root, ...(sub === "launch" ? ["--env", `CODEX_CLI_PATH=${launcher}`] : [])], {
|
|
130
|
+
encoding: "utf8", env, timeout: 10000,
|
|
131
|
+
});
|
|
132
|
+
if (result.status !== 0) throw new Error(result.error?.message ?? (result.stderr?.trim() || "open failed"));
|
|
133
|
+
if (sub === "restore") rmSync(launcher, { force: true });
|
|
134
|
+
console.log(`ChatGPT relaunched ${sub === "launch" ? "with" : "without"} the experimental app-server shim.`);
|
|
135
|
+
return 0;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`ChatGPT shim (experimental): ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/cli/combo.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JEV_DECISION_TIMEOUT_MAX_MS, JEV_DECISION_TIMEOUT_MIN_MS } from "../combos/types";
|
|
1
2
|
import {
|
|
2
3
|
CliUsageError,
|
|
3
4
|
printData,
|
|
@@ -5,7 +6,6 @@ import {
|
|
|
5
6
|
runCliAction,
|
|
6
7
|
runtimeRequest,
|
|
7
8
|
takeFlag,
|
|
8
|
-
takeIntegerOption,
|
|
9
9
|
takeOption,
|
|
10
10
|
type RuntimeApiDeps,
|
|
11
11
|
} from "./runtime-api";
|
|
@@ -13,15 +13,21 @@ import {
|
|
|
13
13
|
const USAGE = `Usage:
|
|
14
14
|
ocx combo [list] [--json]
|
|
15
15
|
ocx combo show <id> [--json]
|
|
16
|
-
ocx combo set <id> --targets <provider/model[:weight],...>
|
|
17
|
-
[--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100
|
|
18
|
-
[--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force
|
|
16
|
+
ocx combo set <id> [--targets <provider/model[:weight],...>]
|
|
17
|
+
[--strategy <failover|round-robin|random|least-used|reset-window|jev>] [--sticky <1-100|->]
|
|
18
|
+
[--effort <low|medium|high|xhigh|max|ultra|->] [--effort-mode <fallback|force|->]
|
|
19
19
|
(force overrides valid client effort and can increase cost/latency) [--alias <name|->]
|
|
20
20
|
[--native-alias] [--display-name <label|->]
|
|
21
|
+
[--decision-provider <provider|-> | --decision-model <route|->] [--decision-timeout <ms|->]
|
|
22
|
+
(jev only; the provider must be a configured jev-decision row)
|
|
21
23
|
[--rename-from <id>] [--json]
|
|
22
|
-
ocx combo remove <id> --yes [--json]
|
|
24
|
+
ocx combo remove <id> --yes [--json]
|
|
25
|
+
ocx combo test [--combo <id>] [--decision-provider <provider|jev> | --decision-model <route>]
|
|
26
|
+
[--decision-timeout <ms>] [--json]
|
|
27
|
+
(one synthetic decision probe through the decision method; may spend a decision call)
|
|
28
|
+
ocx combo discover [--query <text>] [--json]`;
|
|
23
29
|
|
|
24
|
-
type ComboRow = Record<string, unknown> & { id?: string; model?: string };
|
|
30
|
+
type ComboRow = Record<string, unknown> & { id?: string; model?: string; strategy?: string };
|
|
25
31
|
|
|
26
32
|
function parseTargets(value: string): Array<{ provider: string; model: string; weight?: number }> {
|
|
27
33
|
const targets = value.split(",").map(part => part.trim()).filter(Boolean).map(part => {
|
|
@@ -72,35 +78,87 @@ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
72
78
|
const wantsJson = takeFlag(args, "--json");
|
|
73
79
|
if (!id) throw new CliUsageError("combo id is required", USAGE);
|
|
74
80
|
const targetsRaw = takeOption(args, "--targets");
|
|
75
|
-
|
|
76
|
-
const
|
|
81
|
+
const renameFrom = takeOption(args, "--rename-from");
|
|
82
|
+
const partialCurrent = targetsRaw === undefined
|
|
83
|
+
? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps)
|
|
84
|
+
: undefined;
|
|
85
|
+
const partialExisting = partialCurrent?.combos?.find(row => row.id === (renameFrom ?? id));
|
|
86
|
+
if (targetsRaw === undefined && !partialExisting) {
|
|
87
|
+
throw new CliUsageError("--targets is required when creating a combo", USAGE);
|
|
88
|
+
}
|
|
89
|
+
const strategyRaw = takeOption(args, "--strategy");
|
|
90
|
+
const strategy = strategyRaw ?? partialExisting?.strategy ?? "failover";
|
|
77
91
|
if (strategy !== "failover" && strategy !== "round-robin" && strategy !== "random" && strategy !== "least-used" && strategy !== "reset-window" && strategy !== "jev") throw new CliUsageError("--strategy must be failover, round-robin, random, least-used, reset-window, or jev", USAGE);
|
|
78
|
-
const
|
|
92
|
+
const stickyRaw = takeOption(args, "--sticky");
|
|
93
|
+
const stickyLimit = stickyRaw === undefined ? undefined : stickyRaw === "-" ? 1 : Number(stickyRaw);
|
|
94
|
+
if (stickyLimit !== undefined && (!Number.isInteger(stickyLimit) || stickyLimit < 1)) {
|
|
95
|
+
throw new CliUsageError("--sticky must be an integer from 1 to 100, or -", USAGE);
|
|
96
|
+
}
|
|
79
97
|
if (stickyLimit !== undefined) {
|
|
80
98
|
if (stickyLimit > 100) throw new CliUsageError("--sticky must be <= 100", USAGE);
|
|
81
99
|
if (strategy !== "round-robin") throw new CliUsageError("--sticky applies only to round-robin", USAGE);
|
|
82
100
|
}
|
|
83
101
|
const effort = takeOption(args, "--effort");
|
|
84
102
|
const effortMode = takeOption(args, "--effort-mode");
|
|
85
|
-
if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force") {
|
|
86
|
-
throw new CliUsageError("--effort-mode must be fallback or
|
|
103
|
+
if (effortMode !== undefined && effortMode !== "fallback" && effortMode !== "force" && effortMode !== "-") {
|
|
104
|
+
throw new CliUsageError("--effort-mode must be fallback, force, or -", USAGE);
|
|
87
105
|
}
|
|
88
106
|
const alias = takeOption(args, "--alias");
|
|
89
107
|
const nativeAlias = takeFlag(args, "--native-alias");
|
|
90
108
|
const displayName = takeOption(args, "--display-name");
|
|
91
|
-
const
|
|
109
|
+
const decisionProvider = takeOption(args, "--decision-provider");
|
|
110
|
+
const decisionModel = takeOption(args, "--decision-model");
|
|
111
|
+
if (decisionModel !== undefined && decisionModel !== "-" && decisionProvider !== undefined && decisionProvider !== "-") {
|
|
112
|
+
throw new CliUsageError("--decision-model cannot be combined with --decision-provider", USAGE);
|
|
113
|
+
}
|
|
114
|
+
if (decisionModel !== undefined && decisionModel !== "-" && strategy !== "jev") {
|
|
115
|
+
throw new CliUsageError("--decision-model applies only to the jev strategy", USAGE);
|
|
116
|
+
}
|
|
117
|
+
if (decisionProvider !== undefined && decisionProvider !== "-" && strategy !== "jev") {
|
|
118
|
+
throw new CliUsageError("--decision-provider applies only to the jev strategy", USAGE);
|
|
119
|
+
}
|
|
120
|
+
const decisionTimeout = takeOption(args, "--decision-timeout");
|
|
121
|
+
let decisionTimeoutMs: number | null | undefined;
|
|
122
|
+
if (decisionTimeout !== undefined) {
|
|
123
|
+
decisionTimeoutMs = decisionTimeout === "-" ? null : Number(decisionTimeout);
|
|
124
|
+
if (decisionTimeoutMs !== null
|
|
125
|
+
&& (!Number.isInteger(decisionTimeoutMs)
|
|
126
|
+
|| decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS
|
|
127
|
+
|| decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS)) {
|
|
128
|
+
throw new CliUsageError(
|
|
129
|
+
`--decision-timeout must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}, or -`,
|
|
130
|
+
USAGE,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (decisionTimeoutMs !== null && strategy !== "jev") {
|
|
134
|
+
throw new CliUsageError("--decision-timeout applies only to the jev strategy", USAGE);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
92
137
|
rejectArgs(args, USAGE);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
targets: parseTargets(targetsRaw)
|
|
97
|
-
|
|
138
|
+
// Listing-only nulls must not be resent as explicit clears (alias rejects null).
|
|
139
|
+
const combo: Record<string, unknown> = partialExisting
|
|
140
|
+
? Object.fromEntries(Object.entries(partialExisting).filter(([, value]) => value !== null))
|
|
141
|
+
: { strategy, stickyLimit: stickyLimit ?? 1, targets: parseTargets(targetsRaw!) };
|
|
142
|
+
delete combo.id;
|
|
143
|
+
delete combo.model;
|
|
144
|
+
if (strategyRaw !== undefined) combo.strategy = strategy;
|
|
145
|
+
if (stickyLimit !== undefined) combo.stickyLimit = stickyLimit;
|
|
98
146
|
if (effort !== undefined) combo.defaultEffort = effort === "-" ? null : effort;
|
|
99
|
-
if (effortMode !== undefined) combo.defaultEffortMode = effortMode;
|
|
147
|
+
if (effortMode !== undefined) combo.defaultEffortMode = effortMode === "-" ? "fallback" : effortMode;
|
|
100
148
|
if (alias !== undefined) combo.alias = alias === "-" ? "" : alias;
|
|
101
149
|
if (nativeAlias) combo.nativeAlias = true;
|
|
102
150
|
if (displayName !== undefined) combo.displayName = displayName === "-" ? "" : displayName;
|
|
103
|
-
|
|
151
|
+
if (decisionProvider !== undefined) combo.decisionProvider = decisionProvider === "-" ? null : decisionProvider;
|
|
152
|
+
if (decisionModel !== undefined) combo.decisionModel = decisionModel === "-" ? null : decisionModel;
|
|
153
|
+
if (decisionProvider !== undefined && decisionProvider !== "-") combo.decisionModel = null;
|
|
154
|
+
if (decisionModel !== undefined && decisionModel !== "-") combo.decisionProvider = null;
|
|
155
|
+
if (decisionTimeoutMs !== undefined) combo.decisionTimeoutMs = decisionTimeoutMs;
|
|
156
|
+
if (strategy !== "jev") {
|
|
157
|
+
delete combo.decisionProvider;
|
|
158
|
+
delete combo.decisionModel;
|
|
159
|
+
delete combo.decisionTimeoutMs;
|
|
160
|
+
}
|
|
161
|
+
const current = partialCurrent ?? await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
104
162
|
const existing = (current.combos ?? []).find(row => row.id === (renameFrom ?? id));
|
|
105
163
|
if (existing?.imageInput === "disabled") combo.imageInput = "disabled";
|
|
106
164
|
if (effortMode === undefined && existing?.defaultEffortMode === "force") {
|
|
@@ -125,6 +183,71 @@ async function remove(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
125
183
|
printData(result, wantsJson, [`Removed combo ${id}.`]);
|
|
126
184
|
}
|
|
127
185
|
|
|
186
|
+
async function testDecision(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
187
|
+
const args = [...argv];
|
|
188
|
+
const wantsJson = takeFlag(args, "--json");
|
|
189
|
+
const comboId = takeOption(args, "--combo")?.trim();
|
|
190
|
+
let decisionProvider = takeOption(args, "--decision-provider")?.trim();
|
|
191
|
+
let decisionModel = takeOption(args, "--decision-model")?.trim();
|
|
192
|
+
const timeoutRaw = takeOption(args, "--decision-timeout");
|
|
193
|
+
rejectArgs(args, USAGE);
|
|
194
|
+
if (decisionProvider && decisionModel) {
|
|
195
|
+
throw new CliUsageError("use either --decision-provider or --decision-model", USAGE);
|
|
196
|
+
}
|
|
197
|
+
let decisionTimeoutMs: number | undefined = timeoutRaw === undefined ? undefined : Number(timeoutRaw);
|
|
198
|
+
if (decisionTimeoutMs !== undefined && (!Number.isInteger(decisionTimeoutMs)
|
|
199
|
+
|| decisionTimeoutMs < JEV_DECISION_TIMEOUT_MIN_MS || decisionTimeoutMs > JEV_DECISION_TIMEOUT_MAX_MS)) {
|
|
200
|
+
throw new CliUsageError(
|
|
201
|
+
`--decision-timeout must be an integer from ${JEV_DECISION_TIMEOUT_MIN_MS} to ${JEV_DECISION_TIMEOUT_MAX_MS}`,
|
|
202
|
+
USAGE,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (comboId && !decisionProvider && !decisionModel) {
|
|
206
|
+
// Without an override, probe the decision method the combo has saved.
|
|
207
|
+
const current = await runtimeRequest<{ combos?: ComboRow[] }>("/api/combos", {}, deps);
|
|
208
|
+
const row = (current.combos ?? []).find(item => item.id === comboId);
|
|
209
|
+
if (!row) throw new CliUsageError(`unknown combo ${comboId}`, USAGE);
|
|
210
|
+
if (row.strategy !== "jev") throw new CliUsageError(`combo ${comboId} does not use the jev strategy`, USAGE);
|
|
211
|
+
if (typeof row.decisionModel === "string") decisionModel = row.decisionModel;
|
|
212
|
+
else if (typeof row.decisionProvider === "string") decisionProvider = row.decisionProvider;
|
|
213
|
+
if (decisionTimeoutMs === undefined && typeof row.decisionTimeoutMs === "number") decisionTimeoutMs = row.decisionTimeoutMs;
|
|
214
|
+
}
|
|
215
|
+
const result = await runtimeRequest<Record<string, unknown>>("/api/combos/decision-test", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
...(comboId ? { comboId } : {}),
|
|
219
|
+
...(decisionProvider ? { decisionProvider } : {}),
|
|
220
|
+
...(decisionModel ? { decisionModel } : {}),
|
|
221
|
+
...(decisionTimeoutMs !== undefined ? { decisionTimeoutMs } : {}),
|
|
222
|
+
}),
|
|
223
|
+
}, deps);
|
|
224
|
+
const outcome = result.ok === true ? "Decision probe applied" : "Decision probe failed open";
|
|
225
|
+
printData(result, wantsJson, [
|
|
226
|
+
`${outcome} (${String(result.backend)}, gate ${String(result.gate)}, ${String(result.latencyMs)} ms).`,
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function discover(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
231
|
+
const args = [...argv];
|
|
232
|
+
const wantsJson = takeFlag(args, "--json");
|
|
233
|
+
const query = takeOption(args, "--query")?.trim();
|
|
234
|
+
rejectArgs(args, USAGE);
|
|
235
|
+
const path = query ? `/api/combos/decision-discovery?q=${encodeURIComponent(query)}` : "/api/combos/decision-discovery";
|
|
236
|
+
const result = await runtimeRequest<{
|
|
237
|
+
configured?: Array<{ id: string; url: string; model?: string; usable: boolean; issue?: string }>;
|
|
238
|
+
discovered?: Array<{ provider: string; model: string; endpoint: string }>;
|
|
239
|
+
}>(path, {}, deps);
|
|
240
|
+
const lines = [
|
|
241
|
+
...(result.configured ?? []).map(row => {
|
|
242
|
+
const model = row.model ? ` (${row.model})` : "";
|
|
243
|
+
const issue = row.usable ? "" : ` [${row.issue ?? "unusable"}]`;
|
|
244
|
+
return `row ${row.id}: ${row.url}${model}${issue}`;
|
|
245
|
+
}),
|
|
246
|
+
...(result.discovered ?? []).map(row => `catalog ${row.provider}/${row.model}: ${row.endpoint}`),
|
|
247
|
+
];
|
|
248
|
+
printData(result, wantsJson, lines.length ? lines : ["No decision services found."]);
|
|
249
|
+
}
|
|
250
|
+
|
|
128
251
|
export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
129
252
|
return runCliAction(async () => {
|
|
130
253
|
const [sub = "list", ...rest] = argv;
|
|
@@ -132,6 +255,8 @@ export async function handleComboCommand(argv: string[], deps: RuntimeApiDeps =
|
|
|
132
255
|
else if (sub === "show") await show(rest, deps);
|
|
133
256
|
else if (sub === "set" || sub === "create" || sub === "update") await set(rest, deps);
|
|
134
257
|
else if (sub === "remove" || sub === "delete") await remove(rest, deps);
|
|
258
|
+
else if (sub === "test") await testDecision(rest, deps);
|
|
259
|
+
else if (sub === "discover") await discover(rest, deps);
|
|
135
260
|
else throw new CliUsageError(`unknown combo command ${sub}`, USAGE);
|
|
136
261
|
});
|
|
137
262
|
}
|
|
@@ -159,14 +159,16 @@ function setPath(root: Record<string, unknown>, path: string, value: unknown, re
|
|
|
159
159
|
else current[leaf] = value;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** Interpret config-set input as JSON when valid, otherwise preserve it as a string. */
|
|
162
163
|
function parseValue(raw: string): unknown {
|
|
163
164
|
try { return JSON.parse(raw); }
|
|
164
165
|
catch { return raw; }
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/** Accept one UTF-8 BOM from Windows JSON files or piped input. */
|
|
167
169
|
function loadInput(path: string): unknown {
|
|
168
170
|
const raw = path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
|
|
169
|
-
try { return JSON.parse(raw); }
|
|
171
|
+
try { return JSON.parse(raw.replace(/^\uFEFF/, "")); }
|
|
170
172
|
catch { throw new CliUsageError(`invalid JSON in ${path}`); }
|
|
171
173
|
}
|
|
172
174
|
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -351,7 +351,8 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
351
351
|
console.error(clientState.kind === "connected"
|
|
352
352
|
? "Client mode does not start a local provider proxy; use 'ocx sync'."
|
|
353
353
|
: `Client state is ${clientState.kind}: ${clientState.reason}`);
|
|
354
|
-
|
|
354
|
+
// A validated client delegates inference to its hub; no local startup is needed.
|
|
355
|
+
return clientState.kind === "connected" ? 0 : 1;
|
|
355
356
|
}
|
|
356
357
|
await deps.handleEnsure();
|
|
357
358
|
return Number(process.exitCode ?? 0);
|
|
@@ -947,6 +948,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
947
948
|
const { handleLabCommand } = await import("./lab");
|
|
948
949
|
return await handleLabCommand(deps.args.slice(1));
|
|
949
950
|
},
|
|
951
|
+
chatgpt: async deps => {
|
|
952
|
+
const { handleChatgptCommand } = await import("./chatgpt-command");
|
|
953
|
+
return await handleChatgptCommand(deps.args.slice(1));
|
|
954
|
+
},
|
|
950
955
|
claude: async deps => {
|
|
951
956
|
const { cmdClaude } = await import("./claude");
|
|
952
957
|
// "ocx claude desktop" → write Desktop 3P config
|
|
@@ -956,6 +961,10 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
956
961
|
if (exitCode !== 0) return exitCode;
|
|
957
962
|
return 0;
|
|
958
963
|
}
|
|
964
|
+
if (deps.args[1] === "intercept") {
|
|
965
|
+
const { handleClaudeInterceptCommand } = await import("./integrations");
|
|
966
|
+
return await handleClaudeInterceptCommand(deps.args.slice(2));
|
|
967
|
+
}
|
|
959
968
|
if (deps.args[1] === "config") {
|
|
960
969
|
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
961
970
|
return await handleClaudeConfigCommand(deps.args.slice(2));
|
package/src/cli/help.ts
CHANGED
|
@@ -90,6 +90,7 @@ Usage:
|
|
|
90
90
|
ocx config <sub> Validated configuration show/get/set/import/export
|
|
91
91
|
ocx companion <show|set|reset> Menu-bar and widget companion usage settings
|
|
92
92
|
ocx lab <sub> Read-only Compatibility Lab projection inspection
|
|
93
|
+
ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
|
|
93
94
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
94
95
|
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
95
96
|
ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
|
package/src/cli/index.ts
CHANGED
|
@@ -127,7 +127,6 @@ import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxySti
|
|
|
127
127
|
import { acquireOwnershipMutationLease } from "../service/ownership-mutation-lease.mjs";
|
|
128
128
|
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
129
129
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
130
|
-
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
131
130
|
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
132
131
|
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
133
132
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
@@ -711,27 +710,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
711
710
|
? Promise.resolve(null)
|
|
712
711
|
: syncClaudeAgentDefsAtProxyStartup(config, port),
|
|
713
712
|
async () => {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
const { resolveAdmittedCodexModelEntitlements } = await import("../codex/model-entitlement-admission");
|
|
718
|
-
const { buildDesktopDiscoveryInputs } = await import("../claude/desktop-discovery-inputs");
|
|
719
|
-
const [models, modelEntitlements] = await Promise.all([
|
|
720
|
-
fetchAllModels(config),
|
|
721
|
-
resolveAdmittedCodexModelEntitlements(config, { clientVersion: null }),
|
|
722
|
-
]);
|
|
723
|
-
const inputs = buildDesktopDiscoveryInputs({
|
|
724
|
-
config, models, modelEntitlements,
|
|
725
|
-
desktopNativeCandidates: desktopVisibleNativeSlugs(config),
|
|
726
|
-
});
|
|
727
|
-
buildDesktop3pRegistry(
|
|
728
|
-
inputs.nativeSlugs, inputs.routedModels,
|
|
729
|
-
config.claudeCode?.desktopProfile, inputs.nativeContextCap,
|
|
730
|
-
);
|
|
731
|
-
} catch {
|
|
732
|
-
// Best-effort; model discovery can rebuild it. Never reflect credential or provider errors.
|
|
733
|
-
console.warn("[opencodex] Claude Desktop model registry could not be initialized at startup.");
|
|
734
|
-
}
|
|
713
|
+
// Shared with the Claude Code CLI picker, which awaits this same build (desktop-3p-startup.ts).
|
|
714
|
+
const { initDesktop3pRegistry } = await import("../claude/desktop-3p-startup");
|
|
715
|
+
await initDesktop3pRegistry(config);
|
|
735
716
|
},
|
|
736
717
|
);
|
|
737
718
|
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config), server.port ?? port));
|
|
@@ -1212,7 +1193,7 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1212
1193
|
|
|
1213
1194
|
if (snapshot) {
|
|
1214
1195
|
if (guardedStep?.effect === "approval-changed") {
|
|
1215
|
-
return approvalChanged();
|
|
1196
|
+
return approvalChanged(guardedStep.detail);
|
|
1216
1197
|
}
|
|
1217
1198
|
if (guardedStep?.effect === "manager-still-active") {
|
|
1218
1199
|
return managerStillActive(record.service, record);
|
|
@@ -1705,7 +1686,7 @@ async function handleStatus() {
|
|
|
1705
1686
|
process.exit(1);
|
|
1706
1687
|
}
|
|
1707
1688
|
|
|
1708
|
-
const status = await collectStatus();
|
|
1689
|
+
const status = await collectStatus({ mainAccountPolicy: wantsJson });
|
|
1709
1690
|
if (wantsJson) {
|
|
1710
1691
|
console.log(JSON.stringify(status.json, null, 2));
|
|
1711
1692
|
return;
|
package/src/cli/init.ts
CHANGED
|
@@ -11,6 +11,14 @@ import { deriveInitProviders } from "../providers/derive";
|
|
|
11
11
|
import { pinSponsorsWithinKind } from "../providers/sponsor-order";
|
|
12
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
13
|
|
|
14
|
+
/** Parse a setup TCP port; only an empty answer selects the default. */
|
|
15
|
+
export function parseInitPort(raw: string): number | null {
|
|
16
|
+
const text = raw.trim();
|
|
17
|
+
if (!text) return 10100;
|
|
18
|
+
const value = /^\d+$/.test(text) ? Number(text) : NaN;
|
|
19
|
+
return Number.isInteger(value) && value >= 1 && value <= 65535 ? value : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
class InitCancelledError extends Error {
|
|
15
23
|
constructor(readonly exitCode: 1 | 130) {
|
|
16
24
|
super(exitCode === 130 ? "Setup cancelled." : "stdin reached EOF while waiting for input. Re-run `ocx init` in an interactive terminal.");
|
|
@@ -111,6 +119,7 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
|
|
|
111
119
|
} catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
|
|
112
120
|
}
|
|
113
121
|
|
|
122
|
+
/** Create a first configuration interactively, preserving existing files and refusing invalid input. */
|
|
114
123
|
export async function runInit(): Promise<void> {
|
|
115
124
|
const initial = observeInitialConfigState();
|
|
116
125
|
if (initial === "exists") {
|
|
@@ -194,8 +203,13 @@ export async function runInit(): Promise<void> {
|
|
|
194
203
|
};
|
|
195
204
|
}
|
|
196
205
|
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
// A mistyped port re-asks instead of discarding every earlier answer; EOF and SIGINT
|
|
207
|
+
// still reject the pending question with InitCancelledError.
|
|
208
|
+
let port: number | null;
|
|
209
|
+
do {
|
|
210
|
+
port = parseInitPort(await prompt.ask("\nProxy port [10100]: "));
|
|
211
|
+
if (port === null) console.error("Proxy port must be a whole decimal number from 1 to 65535. Please try again.");
|
|
212
|
+
} while (port === null);
|
|
199
213
|
|
|
200
214
|
initializeProviderModelSelection(providerName, providerConfig);
|
|
201
215
|
const config: OcxConfig = {
|
package/src/cli/integrations.ts
CHANGED
|
@@ -368,3 +368,16 @@ export async function handleZcodeCommand(argv: string[], deps: RuntimeApiDeps =
|
|
|
368
368
|
}
|
|
369
369
|
return code;
|
|
370
370
|
}
|
|
371
|
+
|
|
372
|
+
export async function handleClaudeInterceptCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
|
|
373
|
+
return runCliAction(async () => {
|
|
374
|
+
const args = [...argv];
|
|
375
|
+
const action = args.shift();
|
|
376
|
+
const wantsJson = takeFlag(args, "--json");
|
|
377
|
+
const usage = "Usage: ocx claude intercept start [--json]";
|
|
378
|
+
rejectArgs(args, usage);
|
|
379
|
+
if (action !== "start") throw new CliUsageError("Expected start", usage);
|
|
380
|
+
const result = await runtimeRequest("/api/claude-intercept/start", { method: "POST" }, deps);
|
|
381
|
+
printData(result, wantsJson, summaryLines(result));
|
|
382
|
+
});
|
|
383
|
+
}
|
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Deliberately NOT in `src/cli/registry.ts` and NOT in `src/cli/capabilities.ts`, so it
|
|
5
5
|
* is absent from help, from the generated skill surface, and from the registry-parity
|
|
6
|
-
* gate.
|
|
7
|
-
*
|
|
8
|
-
* rather than a second implementation in a shell script.
|
|
6
|
+
* gate. These are implementation entrypoints for the detached restart helper and
|
|
7
|
+
* experimental ChatGPT stdout filter; compiled builds re-enter the same CLI binary.
|
|
9
8
|
*
|
|
10
9
|
* It is routed before the dispatch table for the same reason `help` is - adding it as a
|
|
11
10
|
* runner key would make it a command the registry-parity test expects to find
|
|
12
11
|
* documented.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
13
|
+
* The restart handoff is intentionally unauthenticated, and that is not an oversight. Any process running
|
|
15
14
|
* as this user can invoke it with a hand-written plan file, and it gains nothing by
|
|
16
15
|
* doing so: the helper only does what the public `--restart-codex` flag already does for
|
|
17
16
|
* that same user, and a same-uid process could call `kill` directly. A token here would
|
|
18
17
|
* protect nothing and would imply a boundary that does not exist.
|
|
19
18
|
*/
|
|
20
|
-
const USAGE = "Usage: ocx internal desktop-restart-handoff --plan <path>";
|
|
19
|
+
const USAGE = "Usage: ocx internal desktop-restart-handoff --plan <path> | chatgpt-app-server-filter [--self-test]";
|
|
21
20
|
|
|
22
21
|
function optionValue(args: readonly string[], name: string): string | undefined {
|
|
23
22
|
const index = args.indexOf(name);
|
|
@@ -26,6 +25,14 @@ function optionValue(args: readonly string[], name: string): string | undefined
|
|
|
26
25
|
|
|
27
26
|
export async function handleInternalCommand(args: readonly string[]): Promise<number> {
|
|
28
27
|
const sub = args[0];
|
|
28
|
+
if (sub === "chatgpt-app-server-filter") {
|
|
29
|
+
if (args.length > 2 || (args[1] !== undefined && args[1] !== "--self-test")) {
|
|
30
|
+
console.error(USAGE);
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
const { runChatgptAppServerFilter } = await import("../chatgpt/app-server-shim/filter");
|
|
34
|
+
return await runChatgptAppServerFilter({ selfTest: args[1] === "--self-test" });
|
|
35
|
+
}
|
|
29
36
|
if (sub !== "desktop-restart-handoff") {
|
|
30
37
|
console.error(`Unknown internal command: ${sub ?? "(none)"}. ${USAGE}`);
|
|
31
38
|
return 2;
|
package/src/cli/observe.ts
CHANGED
|
@@ -52,6 +52,7 @@ function logRows(data: unknown): LogEntry[] {
|
|
|
52
52
|
return [];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** Render one human log row with an exact, control-free request ID suitable for history lookup. */
|
|
55
56
|
function formatLog(row: LogEntry): string {
|
|
56
57
|
const time = String(row.timestamp ?? row.createdAt ?? "");
|
|
57
58
|
const route = [row.provider, row.model].filter(Boolean).join("/");
|
|
@@ -69,9 +70,13 @@ function formatLog(row: LogEntry): string {
|
|
|
69
70
|
const account = typeof row.accountLogLabel === "string" && row.accountLogLabel.length > 0
|
|
70
71
|
? `acct=${row.accountLogLabel}`
|
|
71
72
|
: "";
|
|
72
|
-
|
|
73
|
+
const requestId = typeof row.requestId === "string" && row.requestId.length > 0
|
|
74
|
+
&& !/[\x00-\x1f\x7f-\x9f\u2028\u2029]/.test(row.requestId)
|
|
75
|
+
? `id=${row.requestId}` : "";
|
|
76
|
+
return [time, String(status), route, duration, account, conversation, requestId].filter(Boolean).join(" ");
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
/** Read or follow request logs while preserving raw JSON and JSONL output. */
|
|
75
80
|
async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
76
81
|
const args = [...argv];
|
|
77
82
|
const wantsJson = takeFlag(args, "--json");
|
package/src/cli/registry.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface CliCommandEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "chatgpt",
|
|
13
|
+
usage: "ocx chatgpt <launch|restore|status>",
|
|
14
|
+
summary: "Experimental ChatGPT app-server shim (macOS only, default off).",
|
|
15
|
+
details: ["Experimental launch requires chatgptDesktop.appServerShim: true; restore removes its launcher."],
|
|
16
|
+
},
|
|
11
17
|
{
|
|
12
18
|
name: "init",
|
|
13
19
|
aliases: ["setup"],
|
package/src/cli/route-policy.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CliUsageError,
|
|
3
|
+
RuntimeApiError,
|
|
3
4
|
printData,
|
|
4
5
|
rejectArgs,
|
|
5
6
|
runCliAction,
|
|
@@ -38,6 +39,7 @@ async function list(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
38
39
|
);
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/** Show an existing routing profile, distinguishing malformed usage from a missing record. */
|
|
41
43
|
async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
42
44
|
const args = [...argv];
|
|
43
45
|
const id = args.shift();
|
|
@@ -46,7 +48,7 @@ async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
46
48
|
rejectArgs(args, USAGE);
|
|
47
49
|
const result = await runtimeRequest<{ profiles?: ProfileRow[] }>("/api/routing-profiles", {}, deps);
|
|
48
50
|
const profile = (result.profiles ?? []).find(candidate => candidate.id === id);
|
|
49
|
-
if (!profile) throw new
|
|
51
|
+
if (!profile) throw new RuntimeApiError(`unknown routing profile: ${id}`, 404, null);
|
|
50
52
|
printData(profile, wantsJson);
|
|
51
53
|
}
|
|
52
54
|
|