@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
package/src/cli/help.ts
CHANGED
|
@@ -38,6 +38,8 @@ Usage:
|
|
|
38
38
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
|
39
39
|
ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
|
|
40
40
|
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
41
|
+
ocx connect <url> Connect this machine to a remote OpenCodex hub (credential via stdin)
|
|
42
|
+
ocx disconnect Restore local state and clear the hub connection
|
|
41
43
|
ocx sync [--restart-codex] Fetch models from providers and inject into Codex config
|
|
42
44
|
ocx sync-cache [--restart-codex]
|
|
43
45
|
Refresh Codex's model cache from the active catalog
|
|
@@ -50,7 +52,8 @@ Usage:
|
|
|
50
52
|
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
|
|
51
53
|
ocx login <provider> OAuth or API-key provider login
|
|
52
54
|
ocx logout <provider> Remove a stored OAuth login
|
|
53
|
-
ocx gui
|
|
55
|
+
ocx gui [pair --origin <browser-origin> [--json]]
|
|
56
|
+
Open the dashboard or create a single-use remote pairing grant
|
|
54
57
|
ocx update [--tag <tag>] Update opencodex (keeps preview installs on @preview)
|
|
55
58
|
ocx restart Stop and restart the proxy
|
|
56
59
|
ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
runCodexHistoryJob,
|
|
10
10
|
} from "../codex/history-job";
|
|
11
11
|
import { reconcileJournal } from "../codex/journal";
|
|
12
|
+
import { inspectClientRotationRecoveryGate, readClientConnectionState } from "../client/state";
|
|
12
13
|
import {
|
|
13
14
|
codexAutoStartEnabled,
|
|
14
15
|
getConfigDir,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
} from "./tray-proxy";
|
|
49
50
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
50
51
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
51
|
-
import { dispatchCommand } from "./dispatch";
|
|
52
|
+
import { dispatchCommand , decideStartWithLiveOwner } from "./dispatch";
|
|
52
53
|
import { findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
53
54
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
54
55
|
import { createReadinessGate } from "../server/readiness";
|
|
@@ -58,7 +59,6 @@ import { isProcessAlive, ProxyOwnershipRefusedError, stopProxy } from "../lib/pr
|
|
|
58
59
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
59
60
|
import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, proxyStillLiveAfterStop, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalledDetailed, uninstallServiceIfInstalled, uninstallServiceDetailed } from "../service";
|
|
60
61
|
import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
|
|
61
|
-
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
62
62
|
import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
|
|
63
63
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
64
64
|
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
@@ -149,7 +149,10 @@ function startArgv(port?: number): string[] {
|
|
|
149
149
|
return selfLaunchArgv(args);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
async function chooseListenPort(
|
|
152
|
+
async function chooseListenPort(
|
|
153
|
+
requestedPort?: number,
|
|
154
|
+
options: { sibling?: boolean } = {},
|
|
155
|
+
): Promise<number> {
|
|
153
156
|
const config = loadConfig();
|
|
154
157
|
const preferred = requestedPort ?? config.port ?? 10100;
|
|
155
158
|
const hardPin = requestedPort !== undefined && requestedPort > 0;
|
|
@@ -197,7 +200,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
|
|
|
197
200
|
if (preferred > 0 && selected !== preferred) {
|
|
198
201
|
console.log(`⚠️ Port ${preferred} is busy; starting opencodex on ${selected}.`);
|
|
199
202
|
}
|
|
200
|
-
if (shouldPersistSelectedPort(config.port, selected, preferred)) {
|
|
203
|
+
if (shouldPersistSelectedPort(config.port, selected, preferred, options)) {
|
|
201
204
|
config.port = selected;
|
|
202
205
|
saveConfig(config);
|
|
203
206
|
}
|
|
@@ -224,7 +227,12 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
224
227
|
// The probe established that the snapshotted owner is stale. Compare before
|
|
225
228
|
// deleting so a concurrent start that rewrote the PID file keeps its state.
|
|
226
229
|
removePidIfValueIs(pidSnapshot);
|
|
227
|
-
if (!currentExternalCodexModelProvider())
|
|
230
|
+
if (!currentExternalCodexModelProvider()) {
|
|
231
|
+
const clientState = readClientConnectionState();
|
|
232
|
+
reconcileJournal(clientState.kind === "connected"
|
|
233
|
+
? { activeClientApiKeyId: clientState.value.apiKeyId }
|
|
234
|
+
: undefined);
|
|
235
|
+
}
|
|
228
236
|
return { live: null, pidSnapshot };
|
|
229
237
|
}
|
|
230
238
|
|
|
@@ -248,20 +256,52 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
248
256
|
// shutdown then left no runtime record for discovery at all. `handleEnsure`
|
|
249
257
|
// already passes this; `handleStart` is the path that did not.
|
|
250
258
|
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
|
|
259
|
+
let siblingStart = false;
|
|
251
260
|
if (owner.live) {
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
// Rationale and the full decision table live on `decideStartWithLiveOwner`.
|
|
262
|
+
const decision = decideStartWithLiveOwner({
|
|
263
|
+
livePort: owner.live.port,
|
|
264
|
+
requestedPort,
|
|
265
|
+
ocxService: process.env.OCX_SERVICE,
|
|
266
|
+
});
|
|
267
|
+
if (decision === "service-stay-out") {
|
|
268
|
+
// Service-wrapper context (opencodex-service.cmd `:loop`): a healthy proxy from
|
|
269
|
+
// ANY source means the requested port is already served. Exit 0 so the wrapper's
|
|
270
|
+
// `if %ERRORLEVEL% NEQ 0` retry loop terminates instead of respawning every 5s
|
|
271
|
+
// against a listener it can never claim (observed as an endless
|
|
272
|
+
// "Proxy already running" service.log loop).
|
|
260
273
|
console.log(`Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}); service wrapper staying out of the way.`);
|
|
261
274
|
process.exit(0);
|
|
262
275
|
}
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
if (decision === "refuse") {
|
|
277
|
+
console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
// Sibling path. Honest about the side effects it shares with any start in this home:
|
|
281
|
+
// the new instance takes over this home's ocx.pid / runtime-port.json while it runs,
|
|
282
|
+
// and re-points this home's Codex config at the new port when injection applies.
|
|
283
|
+
// What it must NOT do is persist its port into config.port: the configured-port
|
|
284
|
+
// proxy is still the owner of this home, and a later `ocx service` reads config.port
|
|
285
|
+
// to bake the service (observed: a probe on 10198 left the service pinned there).
|
|
286
|
+
siblingStart = true;
|
|
287
|
+
console.warn(
|
|
288
|
+
`Proxy already running on port ${owner.live.port}; starting a second instance on requested port ${requestedPort}. `
|
|
289
|
+
+ `The new instance takes over this home's pid/runtime records and Codex config while it runs.`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const clientState = readClientConnectionState();
|
|
294
|
+
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
295
|
+
throw new Error(`client startup refused: ${clientState.reason}`);
|
|
296
|
+
}
|
|
297
|
+
const rotationGate = inspectClientRotationRecoveryGate(clientState);
|
|
298
|
+
if (clientState.kind === "connected") {
|
|
299
|
+
if (rotationGate.kind === "recovery-required" || rotationGate.kind === "unsafe") {
|
|
300
|
+
throw new Error(`client startup refused: ${rotationGate.reason}`);
|
|
301
|
+
}
|
|
302
|
+
const { startClientRuntime } = await import("../client/runtime");
|
|
303
|
+
await startClientRuntime({ port: requestedPort, block: options.block });
|
|
304
|
+
return;
|
|
265
305
|
}
|
|
266
306
|
|
|
267
307
|
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
@@ -272,7 +312,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
272
312
|
// Port selection is check-then-bind: a concurrent `ocx start`/`ensure` can win the port
|
|
273
313
|
// between the probe and Bun.serve. Soft starts may re-pick; hard-pinned `--port` retries
|
|
274
314
|
// the same port only (never hop — that was the remaining PR #152 gap).
|
|
275
|
-
let port = await chooseListenPort(requestedPort);
|
|
315
|
+
let port = await chooseListenPort(requestedPort, { sibling: siblingStart });
|
|
316
|
+
const { drainAndShutdown, isRecyclingForExit, startServer } = await import("../server");
|
|
276
317
|
// One private readiness gate for this startServer invocation, captured by the
|
|
277
318
|
// listener's closure. handleStart owns it and transitions it after the
|
|
278
319
|
// post-startup sync settles. A second startServer in the same process would
|
|
@@ -301,7 +342,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
301
342
|
continue;
|
|
302
343
|
}
|
|
303
344
|
console.log(`⚠️ Port ${port} was taken while starting; picking another...`);
|
|
304
|
-
port = await chooseListenPort(requestedPort);
|
|
345
|
+
port = await chooseListenPort(requestedPort, { sibling: siblingStart });
|
|
305
346
|
}
|
|
306
347
|
}
|
|
307
348
|
// A single request's streaming error must never crash the daemon serving every
|
|
@@ -737,6 +778,7 @@ async function handleStop() {
|
|
|
737
778
|
// restart-window wait; launchd, systemd and WinSW are down when they say so.
|
|
738
779
|
let schedulerCanRespawn = false;
|
|
739
780
|
let stoppedService = false;
|
|
781
|
+
let nativeRestoreHandledByProxy = false;
|
|
740
782
|
// An ownership mismatch means the service manager was never even contacted: the installed
|
|
741
783
|
// service is still live and will respawn the proxy. Tearing down SHARED state in that
|
|
742
784
|
// situation (native Codex config, the Grok fence) removes config out from under a running
|
|
@@ -785,17 +827,22 @@ async function handleStop() {
|
|
|
785
827
|
* guessed one fails closed into manual recovery rather than letting a later probe read
|
|
786
828
|
* "the configured port refuses" as proof that the right proxy is down.
|
|
787
829
|
*/
|
|
788
|
-
const stopWithDeferral = async (pid: number, discovered?: { hostname: string; port: number } | null): Promise<
|
|
830
|
+
const stopWithDeferral = async (pid: number, discovered?: { hostname: string; port: number } | null): Promise<boolean> => {
|
|
789
831
|
// Resolve ONCE. Reading the runtime record twice let the receipt name the configured
|
|
790
832
|
// guess while the request went to a runtime endpoint that appeared in between.
|
|
791
833
|
const exact = discovered ?? endpointOf(readRuntimePort(pid));
|
|
792
834
|
claimTeardown(exact ?? configuredEndpoint(), exact ? "exact" : "guessed");
|
|
793
|
-
await stopProxy(pid, {
|
|
835
|
+
const graceful = await stopProxy(pid, {
|
|
794
836
|
deferSharedTeardownNonce: teardownNonce,
|
|
795
837
|
// Only an exact endpoint may direct the request; the configured fallback is a guess
|
|
796
838
|
// good enough to record an obligation against, not to POST a stop to.
|
|
797
839
|
runtimeEndpoint: exact ?? undefined,
|
|
798
840
|
});
|
|
841
|
+
// A valid receipt means the proxy deferred shared teardown to this process. If the
|
|
842
|
+
// receipt could not be written, the proxy restores it itself and the caller must not
|
|
843
|
+
// attempt a second restore after a graceful stop. A hard-kill always leaves restore
|
|
844
|
+
// to this process.
|
|
845
|
+
return graceful && !teardownNonce;
|
|
799
846
|
};
|
|
800
847
|
try {
|
|
801
848
|
const serviceStop = stopServiceIfInstalledDetailed();
|
|
@@ -838,7 +885,7 @@ async function handleStop() {
|
|
|
838
885
|
// verification below, so a survivor does not get its client config pulled first.
|
|
839
886
|
// The receipt goes down first — the proxy honours the deferral only when it can
|
|
840
887
|
// see one, so an unrecordable claim degrades to the child doing its own teardown.
|
|
841
|
-
await stopWithDeferral(pid);
|
|
888
|
+
nativeRestoreHandledByProxy = await stopWithDeferral(pid);
|
|
842
889
|
console.log(`✅ Proxy (PID ${pid}) stopped.`);
|
|
843
890
|
removePid(pid);
|
|
844
891
|
removeRuntimePort(pid);
|
|
@@ -868,7 +915,10 @@ async function handleStop() {
|
|
|
868
915
|
try {
|
|
869
916
|
// The probe already found where it answers, and on this path the runtime record is
|
|
870
917
|
// typically what went missing in the first place.
|
|
871
|
-
await stopWithDeferral(
|
|
918
|
+
nativeRestoreHandledByProxy = await stopWithDeferral(
|
|
919
|
+
live.pid,
|
|
920
|
+
{ hostname: live.hostname ?? "127.0.0.1", port: live.port },
|
|
921
|
+
);
|
|
872
922
|
console.log(`✅ Proxy (PID ${live.pid}) stopped.`);
|
|
873
923
|
} catch (err) {
|
|
874
924
|
stopFailed = true;
|
|
@@ -986,7 +1036,7 @@ async function handleStop() {
|
|
|
986
1036
|
console.error(" The obligation is preserved; retry once the proxy is confirmed stopped.");
|
|
987
1037
|
}
|
|
988
1038
|
}
|
|
989
|
-
const restoreBlocked = ownershipBlocked || inheritedBlocks;
|
|
1039
|
+
const restoreBlocked = ownershipBlocked || inheritedBlocks || nativeRestoreHandledByProxy;
|
|
990
1040
|
if (!restoreBlocked) {
|
|
991
1041
|
if (recoveredNonces.length > 0) {
|
|
992
1042
|
// A previous deferred stop died before restoring, and the probe says its endpoint is
|
|
@@ -1294,6 +1344,10 @@ async function handleStatus() {
|
|
|
1294
1344
|
console.log(` Runtime: ${status.json.paths.runtime}`);
|
|
1295
1345
|
console.log(` Runtime source: ${status.json.runtime.source}${status.json.runtime.overrideEnv ? ` (${status.json.runtime.overrideEnv})` : ""}`);
|
|
1296
1346
|
console.log(` Default provider: ${status.json.defaultProvider}`);
|
|
1347
|
+
console.log(` Remote hub: ${status.json.connection.state}${status.json.connection.serverUrl ? ` (${status.json.connection.serverUrl})` : ""}`);
|
|
1348
|
+
if (status.json.connection.state === "invalid" || status.json.connection.state === "mismatched") {
|
|
1349
|
+
console.log(` ⚠️ ${status.json.connection.reason}`);
|
|
1350
|
+
}
|
|
1297
1351
|
console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
|
|
1298
1352
|
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
1299
1353
|
console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ocx models` subcommands that live in `models-runtime` and talk to the
|
|
3
|
+
* management API, rather than editing `config.json` directly.
|
|
4
|
+
*
|
|
5
|
+
* This list is shared rather than duplicated on purpose. `handleModels` in
|
|
6
|
+
* `models.ts` decides which names to hand to the runtime module, and
|
|
7
|
+
* `handleModelsRuntimeCommand` decides which names it answers. When those two
|
|
8
|
+
* lists were written out separately, `new-policy` and `new-arrivals` were
|
|
9
|
+
* implemented and documented but never routed, so both failed with
|
|
10
|
+
* "Unexpected argument(s)" (#3094).
|
|
11
|
+
*
|
|
12
|
+
* It lives in its own leaf module so `models.ts` can read the set without
|
|
13
|
+
* statically importing `models-runtime` — that import is deliberately dynamic
|
|
14
|
+
* to keep the management-API client off the `ocx models add` path.
|
|
15
|
+
*/
|
|
16
|
+
export const MODELS_RUNTIME_SUBCOMMANDS = [
|
|
17
|
+
"live",
|
|
18
|
+
"edit",
|
|
19
|
+
"enable",
|
|
20
|
+
"disable",
|
|
21
|
+
"provider",
|
|
22
|
+
"selected",
|
|
23
|
+
"preset",
|
|
24
|
+
"new-policy",
|
|
25
|
+
"new-arrivals",
|
|
26
|
+
"context",
|
|
27
|
+
"shadow",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export type ModelsRuntimeSubcommand = (typeof MODELS_RUNTIME_SUBCOMMANDS)[number];
|
|
31
|
+
|
|
32
|
+
export function isModelsRuntimeSubcommand(value: string | undefined): value is ModelsRuntimeSubcommand {
|
|
33
|
+
return MODELS_RUNTIME_SUBCOMMANDS.includes(value as ModelsRuntimeSubcommand);
|
|
34
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
takeOption,
|
|
13
13
|
type RuntimeApiDeps,
|
|
14
14
|
} from "./runtime-api";
|
|
15
|
+
import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
|
|
15
16
|
|
|
16
17
|
const USAGE = `Usage:
|
|
17
18
|
ocx models live [--provider <name>] [--json]
|
|
@@ -321,6 +322,9 @@ async function shadow(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
321
322
|
}
|
|
322
323
|
|
|
323
324
|
export async function handleModelsRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
|
|
325
|
+
// The dispatch below and MODELS_RUNTIME_SUBCOMMANDS must name the same set;
|
|
326
|
+
// tests/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
|
|
327
|
+
if (!isModelsRuntimeSubcommand(sub)) return null;
|
|
324
328
|
let action: (() => Promise<void>) | undefined;
|
|
325
329
|
if (sub === "live") action = () => live(argv, deps);
|
|
326
330
|
else if (sub === "edit") action = () => edit(argv, deps);
|
package/src/cli/models.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
modelRecordValue,
|
|
13
13
|
} from "../reasoning-effort";
|
|
14
14
|
import { encodedModelIdCollides, resolveSlugSelection, routedSlug } from "../providers/slug-codec";
|
|
15
|
+
import { isModelsRuntimeSubcommand } from "./models-runtime-subcommands";
|
|
15
16
|
import { knownModelIdsForProvider } from "../router";
|
|
16
17
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
17
18
|
import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
|
|
@@ -445,7 +446,7 @@ export async function handleModels(args: string[]): Promise<void> {
|
|
|
445
446
|
handleCustomList(rest);
|
|
446
447
|
return;
|
|
447
448
|
}
|
|
448
|
-
if (
|
|
449
|
+
if (isModelsRuntimeSubcommand(subcommand)) {
|
|
449
450
|
const { handleModelsRuntimeCommand } = await import("./models-runtime");
|
|
450
451
|
const code = await handleModelsRuntimeCommand(subcommand!, rest);
|
|
451
452
|
if (code !== null) process.exitCode = code;
|
package/src/cli/opencode.ts
CHANGED
|
@@ -94,6 +94,7 @@ export interface OpencodeProxyModelRow {
|
|
|
94
94
|
native?: boolean;
|
|
95
95
|
disabled?: boolean;
|
|
96
96
|
displayName?: string;
|
|
97
|
+
displayNameSource?: "operator" | "provider" | "fallback";
|
|
97
98
|
contextWindow?: number;
|
|
98
99
|
/** Declared effort ladder from `/api/models`; carried into opencode model variants. */
|
|
99
100
|
reasoningEfforts?: string[];
|
|
@@ -387,7 +388,7 @@ export function opencodeCatalogFromProxyRows(
|
|
|
387
388
|
provider: row.provider,
|
|
388
389
|
id: row.id,
|
|
389
390
|
contextWindow: row.contextWindow,
|
|
390
|
-
displayName: row.displayName,
|
|
391
|
+
displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
|
|
391
392
|
...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
|
|
392
393
|
? { reasoningEfforts: [...row.reasoningEfforts] }
|
|
393
394
|
: {}),
|
|
@@ -24,12 +24,14 @@ const USAGE = `Usage:
|
|
|
24
24
|
[--auth-mode <key|forward|oauth|local|->] [--note <text|->]
|
|
25
25
|
[--api-key-transport <x-api-key|bearer|->]
|
|
26
26
|
[--headers <json>] [--enabled <on|off>] [--live-models <on|off>]
|
|
27
|
+
[--retain-models <id,id|->]
|
|
27
28
|
[--allow-private-network <on|off>] [--json]
|
|
28
29
|
ocx provider test <name> [--json]
|
|
29
30
|
ocx provider quota [--refresh] [--json]
|
|
30
31
|
ocx provider presets [--json]
|
|
31
32
|
ocx provider account-mode <pool|direct> [--json]
|
|
32
|
-
ocx provider selected <name> [--set <model,model...>] [--clear] [--json]
|
|
33
|
+
ocx provider selected <name> [--set <model,model...>] [--clear] [--json]
|
|
34
|
+
ocx provider keychain <name> [status|store|restore] [--json]`;
|
|
33
35
|
|
|
34
36
|
function cleared(value: string | undefined): string | undefined {
|
|
35
37
|
return value === "-" ? "" : value;
|
|
@@ -48,6 +50,7 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
48
50
|
const note = cleared(takeOption(args, "--note"));
|
|
49
51
|
const apiKeyTransport = cleared(takeOption(args, "--api-key-transport"));
|
|
50
52
|
const headers = takeOption(args, "--headers");
|
|
53
|
+
const retainModelsRaw = takeOption(args, "--retain-models");
|
|
51
54
|
const enabled = takeBooleanOption(args, "--enabled");
|
|
52
55
|
const liveModels = takeBooleanOption(args, "--live-models");
|
|
53
56
|
const allowPrivateNetwork = takeBooleanOption(args, "--allow-private-network");
|
|
@@ -74,6 +77,10 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
if (enabled !== undefined) patch.disabled = !enabled;
|
|
80
|
+
if (retainModelsRaw !== undefined) {
|
|
81
|
+
// `-` clears, matching the other `edit` scalars; test before csv() or it becomes ["-"].
|
|
82
|
+
patch.retainModels = retainModelsRaw.trim() === "-" ? null : csv(retainModelsRaw);
|
|
83
|
+
}
|
|
77
84
|
if (liveModels !== undefined) patch.liveModels = liveModels;
|
|
78
85
|
if (allowPrivateNetwork !== undefined) patch.allowPrivateNetwork = allowPrivateNetwork;
|
|
79
86
|
if (Object.keys(patch).length === 0) throw new CliUsageError("at least one edit option is required", USAGE);
|
|
@@ -175,6 +182,28 @@ async function selected(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
175
182
|
printData(result, wantsJson, [`${name}: ${models.length ? models.join(", ") : "all models"}`]);
|
|
176
183
|
}
|
|
177
184
|
|
|
185
|
+
async function keychain(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
186
|
+
const args = [...argv];
|
|
187
|
+
const name = args.shift()?.trim();
|
|
188
|
+
const wantsJson = takeFlag(args, "--json");
|
|
189
|
+
const action = (args.shift() ?? "status").toLowerCase();
|
|
190
|
+
if (!name) throw new CliUsageError("provider name is required", USAGE);
|
|
191
|
+
if (!["status", "store", "restore"].includes(action)) throw new CliUsageError(`unknown keychain action ${action}`, USAGE);
|
|
192
|
+
rejectArgs(args, USAGE);
|
|
193
|
+
if (action === "status") {
|
|
194
|
+
const result = await runtimeRequest<Record<string, unknown>>(`/api/providers/keychain?name=${encodeURIComponent(name)}`, {}, deps);
|
|
195
|
+
printData(result, wantsJson, summaryLines(result));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const result = await runtimeRequest<Record<string, unknown>>("/api/providers/keychain", {
|
|
199
|
+
method: "POST",
|
|
200
|
+
body: JSON.stringify({ name, action }),
|
|
201
|
+
}, deps);
|
|
202
|
+
printData(result, wantsJson, [action === "store"
|
|
203
|
+
? `${name}: API key moved to the OS keychain; config.json now holds a keychain: reference.`
|
|
204
|
+
: `${name}: API key restored to config.json; keychain entries removed.`]);
|
|
205
|
+
}
|
|
206
|
+
|
|
178
207
|
export async function handleProviderRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
|
|
179
208
|
const handlers: Record<string, (args: string[], deps: RuntimeApiDeps) => Promise<void>> = {
|
|
180
209
|
edit,
|
|
@@ -184,6 +213,7 @@ export async function handleProviderRuntimeCommand(sub: string, argv: string[],
|
|
|
184
213
|
presets,
|
|
185
214
|
"account-mode": accountMode,
|
|
186
215
|
selected,
|
|
216
|
+
keychain,
|
|
187
217
|
};
|
|
188
218
|
const handler = handlers[sub];
|
|
189
219
|
if (!handler) return null;
|
package/src/cli/registry.ts
CHANGED
|
@@ -86,6 +86,24 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
86
86
|
],
|
|
87
87
|
},
|
|
88
88
|
{ name: "ensure", usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
89
|
+
{
|
|
90
|
+
name: "connect",
|
|
91
|
+
usage: "ocx connect <url> [--management-url <url>] (--pairing-code-stdin | --admin-token-stdin) [--clients codex,claude] [--management-transport direct|relay] [--no-sync]",
|
|
92
|
+
summary: "Connect this machine to a remote OpenCodex hub without persisting the one-time authority.",
|
|
93
|
+
details: [
|
|
94
|
+
"Status: ocx connect status [--json]",
|
|
95
|
+
"Rotate or recover: ocx connect rotate (--pairing-code-stdin | --admin-token-stdin) [--json]",
|
|
96
|
+
"Revoke while connected: ocx connect revoke --admin-token-stdin [--json]",
|
|
97
|
+
"Machine resources: /api/machine/status, /api/machine/shim, /api/machine/clients, /api/machine/sync, /api/machine/disconnect, and the fixed /api/machine/hub-relay namespace.",
|
|
98
|
+
"Remote browser self-logout uses /api/session/logout from the GUI; it is distinct from client disconnect and key revocation.",
|
|
99
|
+
"Credentials are accepted only through stdin; argv and environment credential forms are not supported.",
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "disconnect",
|
|
104
|
+
usage: "ocx disconnect [--keep-catalog] [--json]",
|
|
105
|
+
summary: "Restore local client state offline and clear the remote-hub connection.",
|
|
106
|
+
},
|
|
89
107
|
{
|
|
90
108
|
name: "sync",
|
|
91
109
|
usage: "ocx sync [--restart-codex] [--restart-desktop-app]",
|
|
@@ -128,7 +146,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
128
146
|
},
|
|
129
147
|
{ name: "login", usage: "ocx login <provider>", summary: "OAuth or API-key login for a provider." },
|
|
130
148
|
{ name: "logout", usage: "ocx logout <provider>", summary: "Remove a stored provider login." },
|
|
131
|
-
{
|
|
149
|
+
{
|
|
150
|
+
name: "gui",
|
|
151
|
+
usage: "ocx gui [pair --origin <browser-origin> [--json]]",
|
|
152
|
+
summary: "Open the opencodex dashboard or create a secret single-use remote pairing grant.",
|
|
153
|
+
details: [
|
|
154
|
+
"Pairing requires an explicit allowed --origin; there is no localhost or config-derived default.",
|
|
155
|
+
"The printed grant is secret, single-use, short-lived, and must not be persisted.",
|
|
156
|
+
],
|
|
157
|
+
},
|
|
132
158
|
{
|
|
133
159
|
name: "update",
|
|
134
160
|
usage: "ocx update [--tag latest|preview]",
|
|
@@ -240,8 +266,12 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
240
266
|
name: "access",
|
|
241
267
|
usage: "ocx access <key|endpoints|models|test> ...",
|
|
242
268
|
summary: "Manage OpenCodex admission API keys and inspect external endpoints.",
|
|
269
|
+
details: [
|
|
270
|
+
"Key rotation start uses POST /api/keys/rotate and returns the replacement secret once.",
|
|
271
|
+
"Commit uses POST /api/keys/rotate/commit; abort uses DELETE /api/keys/rotate with the returned rotation id.",
|
|
272
|
+
],
|
|
243
273
|
},
|
|
244
|
-
{ name: "api-key", usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
|
|
274
|
+
{ name: "api-key", usage: "ocx api-key <list|create|rotate|remove> ...", summary: "Alias of ocx access key." },
|
|
245
275
|
{
|
|
246
276
|
name: "export",
|
|
247
277
|
usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|aside> [--json] [--out <path>] [--force]",
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -182,7 +182,16 @@ export function csv(value: string | undefined): string[] | undefined {
|
|
|
182
182
|
* `--code=https://…?code=SECRET` that writes the authorization code to stderr,
|
|
183
183
|
* which is the exact exposure the stdin path exists to avoid.
|
|
184
184
|
*/
|
|
185
|
-
const SECRET_OPTIONS = [
|
|
185
|
+
const SECRET_OPTIONS = [
|
|
186
|
+
"--code",
|
|
187
|
+
"--headers",
|
|
188
|
+
"--token",
|
|
189
|
+
"--admin-token",
|
|
190
|
+
"--pairing-code",
|
|
191
|
+
"--credential-env",
|
|
192
|
+
"--admin-token-env",
|
|
193
|
+
"--pairing-code-env",
|
|
194
|
+
];
|
|
186
195
|
|
|
187
196
|
/**
|
|
188
197
|
* Replace credential values before they are reported back.
|
package/src/cli/status.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "..
|
|
|
18
18
|
import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
|
|
19
19
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
20
20
|
import { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy, type ClaudeDesktopPolicyHealth } from "../claude/desktop-policy";
|
|
21
|
+
import { collectClientConnectionStatus } from "./connect";
|
|
21
22
|
|
|
22
23
|
type HealthCheck = {
|
|
23
24
|
ok: boolean;
|
|
@@ -63,6 +64,18 @@ export type CliStatusJson = {
|
|
|
63
64
|
source: "default" | "file" | "fallback";
|
|
64
65
|
error: string | null;
|
|
65
66
|
};
|
|
67
|
+
connection: {
|
|
68
|
+
state: "disconnected" | "connected" | "invalid" | "mismatched";
|
|
69
|
+
reason?: string;
|
|
70
|
+
serverUrl?: string;
|
|
71
|
+
managementUrl?: string;
|
|
72
|
+
protocolVersion?: number;
|
|
73
|
+
apiKeyId?: string;
|
|
74
|
+
selectedClients?: string[];
|
|
75
|
+
catalog?: "present" | "missing" | "unsafe";
|
|
76
|
+
catalogAgeSeconds?: number;
|
|
77
|
+
credentialFile: "owned" | "missing" | "changed" | "unsafe";
|
|
78
|
+
};
|
|
66
79
|
service: { summary: string };
|
|
67
80
|
codexShim: { summary: string };
|
|
68
81
|
codexPlugins: CodexPluginsDiagnostic;
|
|
@@ -309,6 +322,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
309
322
|
desiredEnabled: claudeDesktopIntegrationEnabled(config),
|
|
310
323
|
policy: claudeDesktopPolicyHealth(probeClaudeDesktopPolicy()),
|
|
311
324
|
};
|
|
325
|
+
const clientConnection = collectClientConnectionStatus();
|
|
312
326
|
// Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
|
|
313
327
|
// Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
|
|
314
328
|
// warn on malformed config.json (status --json must stay stderr-clean).
|
|
@@ -491,6 +505,18 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
491
505
|
source: configDiagnostics.source,
|
|
492
506
|
error: configDiagnostics.error,
|
|
493
507
|
},
|
|
508
|
+
connection: {
|
|
509
|
+
state: clientConnection.state,
|
|
510
|
+
...(clientConnection.reason ? { reason: clientConnection.reason } : {}),
|
|
511
|
+
...(clientConnection.serverUrl ? { serverUrl: clientConnection.serverUrl } : {}),
|
|
512
|
+
...(clientConnection.managementUrl ? { managementUrl: clientConnection.managementUrl } : {}),
|
|
513
|
+
...(clientConnection.protocolVersion ? { protocolVersion: clientConnection.protocolVersion } : {}),
|
|
514
|
+
...(clientConnection.apiKeyId ? { apiKeyId: clientConnection.apiKeyId } : {}),
|
|
515
|
+
...(clientConnection.selectedClients ? { selectedClients: [...clientConnection.selectedClients] } : {}),
|
|
516
|
+
catalog: clientConnection.catalog,
|
|
517
|
+
...(clientConnection.catalogAgeSeconds !== undefined ? { catalogAgeSeconds: clientConnection.catalogAgeSeconds } : {}),
|
|
518
|
+
credentialFile: clientConnection.token,
|
|
519
|
+
},
|
|
494
520
|
service: { summary: serviceSummary },
|
|
495
521
|
codexShim: { summary: codexShimSummary },
|
|
496
522
|
codexPlugins,
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const USAGE = `Usage:
|
|
15
15
|
ocx system [status] [--json]
|
|
16
|
-
ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
|
|
16
|
+
ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
|
|
17
|
+
[--desktop-authless <on|off>] [--json]
|
|
17
18
|
ocx system startup <health|install-service|install-shim> [--json]
|
|
18
19
|
ocx system diagnostics [--json]
|
|
19
20
|
ocx system sync [--json]
|
|
@@ -42,13 +43,18 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
42
43
|
const wantsJson = takeFlag(args, "--json");
|
|
43
44
|
const autoStart = takeBooleanOption(args, "--auto-start");
|
|
44
45
|
const streamMode = takeOption(args, "--stream-mode");
|
|
46
|
+
const desktopAuthless = takeBooleanOption(args, "--desktop-authless");
|
|
45
47
|
rejectArgs(args, USAGE);
|
|
46
|
-
if (autoStart === undefined && streamMode === undefined) {
|
|
48
|
+
if (autoStart === undefined && streamMode === undefined && desktopAuthless === undefined) {
|
|
47
49
|
const result = await runtimeRequest("/api/settings", {}, deps);
|
|
48
50
|
printData(result, wantsJson, summaryLines(result));
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
|
-
const body = {
|
|
53
|
+
const body = {
|
|
54
|
+
...(autoStart !== undefined ? { codexAutoStart: autoStart } : {}),
|
|
55
|
+
...(streamMode !== undefined ? { streamMode } : {}),
|
|
56
|
+
...(desktopAuthless !== undefined ? { codexDesktopAuthless: desktopAuthless } : {}),
|
|
57
|
+
};
|
|
52
58
|
const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
53
59
|
printData(result, wantsJson, ["System settings updated."]);
|
|
54
60
|
}
|