@bitkyc08/opencodex 2.12.0 → 2.13.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 +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry-driven command dispatch (Phase 3 of the CLI deepening).
|
|
3
|
+
*
|
|
4
|
+
* The command switch moved out of src/cli/index.ts into a runner table keyed
|
|
5
|
+
* by command name. Aliases resolve through the registry's alias pairs
|
|
6
|
+
* (init/setup, restore/eject, uninstall/remove, models/model); the registry
|
|
7
|
+
* remains the single source of command metadata. index.ts passes its local
|
|
8
|
+
* helpers (start/stop/ensure/status/...) through CliDispatchDeps so dispatch
|
|
9
|
+
* never needs to import the entry module back (no cycle).
|
|
10
|
+
*/
|
|
11
|
+
import { CLI_COMMANDS } from "./registry";
|
|
12
|
+
import type { CliHead } from "./root";
|
|
13
|
+
import type { ReadyArgs } from "./ready";
|
|
14
|
+
import type { LiveProxy } from "../server/proxy-liveness";
|
|
15
|
+
import type { OcxConfig } from "../types";
|
|
16
|
+
import { hasHelpFlag, printSubcommandUsage, printUsage } from "./help";
|
|
17
|
+
import { setIntegrationEnabled, shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
18
|
+
import { syncModelsToCodex } from "../codex/sync";
|
|
19
|
+
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
20
|
+
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
21
|
+
import { stripGrokConfig } from "../grok/inject";
|
|
22
|
+
import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
|
|
23
|
+
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
24
|
+
import { serviceCommand } from "../service";
|
|
25
|
+
|
|
26
|
+
export interface CliDispatchDeps {
|
|
27
|
+
args: string[];
|
|
28
|
+
command: string | undefined;
|
|
29
|
+
head: CliHead;
|
|
30
|
+
loadConfig: () => OcxConfig;
|
|
31
|
+
findLiveProxy: () => Promise<LiveProxy | null>;
|
|
32
|
+
probeHostname: (hostname: string | undefined) => string;
|
|
33
|
+
waitForProxy: (timeoutMs?: number) => Promise<LiveProxy | null>;
|
|
34
|
+
startArgv: (port?: number) => string[];
|
|
35
|
+
/** Spawn a detached proxy child (stdio ignore, unref'd, provenance env). */
|
|
36
|
+
spawnDetached: (argv: readonly string[]) => void;
|
|
37
|
+
handleStart: () => Promise<void>;
|
|
38
|
+
handleStop: () => Promise<boolean>;
|
|
39
|
+
handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
|
|
40
|
+
handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
|
|
41
|
+
handleTrayProxyRestart: () => Promise<void>;
|
|
42
|
+
handleRestartStartWhenStopped: () => Promise<boolean | "skipped">;
|
|
43
|
+
handleProxyRestart: (startWhenStopped: () => Promise<boolean | "skipped">) => Promise<boolean>;
|
|
44
|
+
handleUninstall: () => Promise<void>;
|
|
45
|
+
handleStatus: () => Promise<void>;
|
|
46
|
+
handleRecoverHistory: () => Promise<void>;
|
|
47
|
+
handleReady: (args: ReadyArgs) => Promise<number>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type CommandRunner = (deps: CliDispatchDeps) => Promise<number>;
|
|
51
|
+
|
|
52
|
+
const commandRunners: Record<string, CommandRunner> = {
|
|
53
|
+
init: async () => {
|
|
54
|
+
const { runInit } = await import("./init");
|
|
55
|
+
await runInit();
|
|
56
|
+
// runInit sets process.exitCode = 1 on stdin EOF/closed; preserve it.
|
|
57
|
+
return Number(process.exitCode ?? 0);
|
|
58
|
+
},
|
|
59
|
+
start: async deps => {
|
|
60
|
+
await deps.handleStart();
|
|
61
|
+
return Number(process.exitCode ?? 0);
|
|
62
|
+
},
|
|
63
|
+
stop: async deps => {
|
|
64
|
+
// Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
|
|
65
|
+
// re-start the proxy immediately, so warning there would contradict the next line.
|
|
66
|
+
if (await deps.handleStop()) {
|
|
67
|
+
console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
|
|
68
|
+
}
|
|
69
|
+
return Number(process.exitCode ?? 0);
|
|
70
|
+
},
|
|
71
|
+
restore: async deps => {
|
|
72
|
+
const restoreJson = deps.args[1] === "--json";
|
|
73
|
+
if (deps.args[1] === "back") {
|
|
74
|
+
// Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
|
|
75
|
+
// lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
|
|
76
|
+
// this only re-runs the same inject (config + catalog + history) `ocx start` does.
|
|
77
|
+
const live = await deps.findLiveProxy();
|
|
78
|
+
if (!live) {
|
|
79
|
+
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
const desired = setIntegrationEnabled("codex", true);
|
|
83
|
+
if (!desired.ok) {
|
|
84
|
+
console.error(`Codex desired state was not saved (${desired.reason}).`);
|
|
85
|
+
return desired.reason === "conflict" ? 2 : 1;
|
|
86
|
+
}
|
|
87
|
+
const synced = await syncModelsToCodex(live.port);
|
|
88
|
+
if (synced.status === "skipped") {
|
|
89
|
+
console.error("Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.");
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
if (!synced.ok) {
|
|
93
|
+
console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
const target = collectOrcaCodexHomeDiagnostic();
|
|
97
|
+
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
const desired = setIntegrationEnabled("codex", false);
|
|
101
|
+
if (!desired.ok) {
|
|
102
|
+
if (restoreJson) {
|
|
103
|
+
// Machine-readable contract: every restore --json outcome emits one
|
|
104
|
+
// schema-complete envelope on stdout, including pre-machinery failures.
|
|
105
|
+
const { skippedRestoreEnvelope } = await import("../codex/inject");
|
|
106
|
+
console.log(JSON.stringify(skippedRestoreEnvelope(false, `Codex desired state was not saved (${desired.reason}).`)));
|
|
107
|
+
} else {
|
|
108
|
+
console.error(`Codex desired state was not saved (${desired.reason}).`);
|
|
109
|
+
}
|
|
110
|
+
return desired.reason === "conflict" ? 2 : 1;
|
|
111
|
+
}
|
|
112
|
+
// A repeated OFF on an already-clean home is a policy no-op. Do not enter
|
|
113
|
+
// restore's native-profile machinery merely to prove there is nothing to
|
|
114
|
+
// restore: those locks live in CODEX_HOME and a skip must create nothing.
|
|
115
|
+
if (desired.status === "unchanged") {
|
|
116
|
+
const { classifyNativeRoutedResidue } = await import("../codex/native-residue");
|
|
117
|
+
if (classifyNativeRoutedResidue().kind === "clean") {
|
|
118
|
+
const alreadyOff = "Codex integration is already OFF and native; no Codex files changed.";
|
|
119
|
+
if (restoreJson) {
|
|
120
|
+
const { skippedRestoreEnvelope } = await import("../codex/inject");
|
|
121
|
+
console.log(JSON.stringify(skippedRestoreEnvelope(true, alreadyOff)));
|
|
122
|
+
} else {
|
|
123
|
+
console.log(alreadyOff);
|
|
124
|
+
}
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let r: { success: boolean; message: string };
|
|
129
|
+
try {
|
|
130
|
+
r = await restoreNativeCodexAsync({ revalidateDesiredState: true });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
133
|
+
}
|
|
134
|
+
if (restoreJson) {
|
|
135
|
+
// Spawned callers need the artifact-level result to distinguish a busy
|
|
136
|
+
// history worker from a successful native restore. Keep stdout machine
|
|
137
|
+
// readable; human framing remains the default command contract.
|
|
138
|
+
console.log(JSON.stringify(r));
|
|
139
|
+
return r.success ? 0 : 1;
|
|
140
|
+
}
|
|
141
|
+
if (r.success) console.log(`✅ ${r.message}`);
|
|
142
|
+
else {
|
|
143
|
+
console.error(`⚠️ ${r.message}`);
|
|
144
|
+
}
|
|
145
|
+
let code = r.success ? 0 : 1;
|
|
146
|
+
try {
|
|
147
|
+
const g = stripGrokConfig();
|
|
148
|
+
if (g.changed) console.log(`✅ ${g.message}`);
|
|
149
|
+
else if (!g.ok) {
|
|
150
|
+
console.error(`⚠️ ${g.message}`);
|
|
151
|
+
code = 1;
|
|
152
|
+
}
|
|
153
|
+
} catch { /* best-effort */ }
|
|
154
|
+
if (r.success) {
|
|
155
|
+
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
156
|
+
} else {
|
|
157
|
+
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
158
|
+
}
|
|
159
|
+
return code;
|
|
160
|
+
},
|
|
161
|
+
"recover-history": async deps => {
|
|
162
|
+
await deps.handleRecoverHistory();
|
|
163
|
+
return Number(process.exitCode ?? 0);
|
|
164
|
+
},
|
|
165
|
+
uninstall: async deps => {
|
|
166
|
+
await deps.handleUninstall();
|
|
167
|
+
return Number(process.exitCode ?? 0);
|
|
168
|
+
},
|
|
169
|
+
status: async deps => {
|
|
170
|
+
await deps.handleStatus();
|
|
171
|
+
return Number(process.exitCode ?? 0);
|
|
172
|
+
},
|
|
173
|
+
doctor: async deps => {
|
|
174
|
+
const { runDoctor } = await import("./doctor");
|
|
175
|
+
await runDoctor(deps.args.slice(1));
|
|
176
|
+
return 0;
|
|
177
|
+
},
|
|
178
|
+
debug: async deps => {
|
|
179
|
+
const { handleDebugCommand } = await import("./debug");
|
|
180
|
+
await handleDebugCommand(deps.args.slice(1));
|
|
181
|
+
return 0;
|
|
182
|
+
},
|
|
183
|
+
ensure: async deps => {
|
|
184
|
+
await deps.handleEnsure();
|
|
185
|
+
return Number(process.exitCode ?? 0);
|
|
186
|
+
},
|
|
187
|
+
login: async deps => {
|
|
188
|
+
const { handleLogin } = await import("../oauth/login-cli");
|
|
189
|
+
await handleLogin(deps.args[1]);
|
|
190
|
+
return 0;
|
|
191
|
+
},
|
|
192
|
+
logout: async deps => {
|
|
193
|
+
const { removeCredential } = await import("../oauth/store");
|
|
194
|
+
const name = (deps.args[1] ?? "").trim().toLowerCase();
|
|
195
|
+
await removeCredential(name);
|
|
196
|
+
console.log(`Logged out of ${name || "(none)"}.`);
|
|
197
|
+
return 0;
|
|
198
|
+
},
|
|
199
|
+
sync: async deps => {
|
|
200
|
+
const restartCodex = deps.args.slice(1).includes("--restart-codex");
|
|
201
|
+
const synced = await syncModelsToCodex((await deps.findLiveProxy())?.port);
|
|
202
|
+
let code = 0;
|
|
203
|
+
if (synced.status === "skipped") {
|
|
204
|
+
console.log("Codex integration is OFF; sync skipped and no Codex files changed.");
|
|
205
|
+
} else if (!synced.ok) {
|
|
206
|
+
code = 1;
|
|
207
|
+
console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
|
|
208
|
+
}
|
|
209
|
+
// Only warn/restart when a catalog or models_cache write actually happened. This is
|
|
210
|
+
// deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
|
|
211
|
+
// so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
|
|
212
|
+
// exactly when a long-lived app-server is holding the stale list.
|
|
213
|
+
if (synced.catalogWritten || synced.cacheSynced) {
|
|
214
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
215
|
+
}
|
|
216
|
+
return code;
|
|
217
|
+
},
|
|
218
|
+
v2: async deps => {
|
|
219
|
+
const { cmdV2 } = await import("./v2");
|
|
220
|
+
return await cmdV2(deps.args.slice(1), {}, async () => (await deps.findLiveProxy())?.port);
|
|
221
|
+
},
|
|
222
|
+
"sync-cache": async deps => {
|
|
223
|
+
const restartCodex = deps.args.slice(1).includes("--restart-codex");
|
|
224
|
+
if (!shouldSyncCodexOnStart(deps.loadConfig())) {
|
|
225
|
+
console.log("Codex integration is OFF; cache sync skipped and no Codex files changed.");
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
|
|
229
|
+
const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
|
|
230
|
+
const { getCodexHome } = await import("../codex/paths");
|
|
231
|
+
const owningCodexHome = getCodexHome();
|
|
232
|
+
const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
|
|
233
|
+
invalidateCodexModelsCacheWithPermit(permit, owningCodexHome));
|
|
234
|
+
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
235
|
+
if (invalidated.kind === "completed" && invalidated.value) {
|
|
236
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
237
|
+
}
|
|
238
|
+
return 0;
|
|
239
|
+
},
|
|
240
|
+
gui: async deps => {
|
|
241
|
+
const config = deps.loadConfig();
|
|
242
|
+
// Identity-checked liveness (not the pid file + a fixed sleep): finds a fallback-port
|
|
243
|
+
// proxy and waits until the spawned one actually answers before opening the browser.
|
|
244
|
+
let live = await deps.findLiveProxy();
|
|
245
|
+
if (!live) {
|
|
246
|
+
console.log("Proxy not running. Starting...");
|
|
247
|
+
deps.spawnDetached(deps.startArgv((config.port ?? 10100) > 0 ? (config.port ?? 10100) : undefined));
|
|
248
|
+
live = await deps.waitForProxy();
|
|
249
|
+
if (!live) {
|
|
250
|
+
console.error("❌ Proxy did not become healthy after starting. Not opening the GUI.");
|
|
251
|
+
return 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Open the host the proxy actually binds — `localhost` only answers for
|
|
255
|
+
// loopback/wildcard binds, not a concrete LAN/IPv6 hostname.
|
|
256
|
+
const guiHost = deps.probeHostname(live?.hostname ?? config.hostname);
|
|
257
|
+
const guiUrl = `http://${guiHost === "127.0.0.1" ? "localhost" : guiHost}:${live?.port ?? config.port}`;
|
|
258
|
+
console.log(`Opening ${guiUrl}`);
|
|
259
|
+
const { openUrl } = await import("../lib/open-url");
|
|
260
|
+
openUrl(guiUrl);
|
|
261
|
+
return 0;
|
|
262
|
+
},
|
|
263
|
+
service: async deps => {
|
|
264
|
+
await serviceCommand(...deps.args.slice(1));
|
|
265
|
+
return 0;
|
|
266
|
+
},
|
|
267
|
+
tray: async deps => {
|
|
268
|
+
const { windowsTrayCommand } = await import("../tray/windows");
|
|
269
|
+
await windowsTrayCommand(deps.args.slice(1));
|
|
270
|
+
return 0;
|
|
271
|
+
},
|
|
272
|
+
"codex-shim": async deps => {
|
|
273
|
+
const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
|
|
274
|
+
switch (deps.args[1]) {
|
|
275
|
+
case "install": {
|
|
276
|
+
const r = installCodexShim();
|
|
277
|
+
const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness");
|
|
278
|
+
const warnings = diagnoseCodexShim().healthy
|
|
279
|
+
? collectCodexShimReadinessWarnings()
|
|
280
|
+
: [];
|
|
281
|
+
console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
|
|
282
|
+
for (const warning of warnings) console.warn(` ${warning}`);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "status":
|
|
286
|
+
console.log(codexShimStatus());
|
|
287
|
+
break;
|
|
288
|
+
case "uninstall":
|
|
289
|
+
case "remove": {
|
|
290
|
+
const r = uninstallCodexShim();
|
|
291
|
+
console.log(r.removed ? `✅ ${r.message}` : `⚠️ ${r.message}`);
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
default:
|
|
295
|
+
console.error("Usage: ocx codex-shim <install|status|uninstall|remove>");
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
return 0;
|
|
299
|
+
},
|
|
300
|
+
update: async deps => {
|
|
301
|
+
// `ocx update --help` must print usage and exit WITHOUT side effects — running the
|
|
302
|
+
// real self-update stops the proxy and drops in-flight routed streams (issue #168).
|
|
303
|
+
if (hasHelpFlag(deps.args.slice(1))) {
|
|
304
|
+
printSubcommandUsage("update");
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
const { runUpdate } = await import("../update");
|
|
308
|
+
await runUpdate();
|
|
309
|
+
return 0;
|
|
310
|
+
},
|
|
311
|
+
"__refresh-version": async deps => {
|
|
312
|
+
// Hidden, detached helper spawned by the update prompt to refresh the
|
|
313
|
+
// cached latest version without blocking the foreground start. Not in help.
|
|
314
|
+
const { refreshVersionCache } = await import("../update/notify");
|
|
315
|
+
const channel = deps.args[1] === "preview" ? "preview" : "latest";
|
|
316
|
+
await refreshVersionCache(channel);
|
|
317
|
+
return 0;
|
|
318
|
+
},
|
|
319
|
+
"__tray-start": async deps => {
|
|
320
|
+
return (await deps.handleTrayProxyStart()) ? 0 : 1;
|
|
321
|
+
},
|
|
322
|
+
"__tray-restart": async deps => {
|
|
323
|
+
await deps.handleTrayProxyRestart();
|
|
324
|
+
return Number(process.exitCode ?? 0);
|
|
325
|
+
},
|
|
326
|
+
"__startup-health": async deps => {
|
|
327
|
+
const { collectStartupHealth } = await import("../codex/autostart-health");
|
|
328
|
+
console.log(JSON.stringify(collectStartupHealth(deps.loadConfig())));
|
|
329
|
+
return 0;
|
|
330
|
+
},
|
|
331
|
+
"__tray-host": async () => {
|
|
332
|
+
const { runWindowsTrayHost } = await import("../tray/windows");
|
|
333
|
+
await runWindowsTrayHost();
|
|
334
|
+
return 0;
|
|
335
|
+
},
|
|
336
|
+
"__gui-update-worker": async deps => {
|
|
337
|
+
const jobId = deps.args[1];
|
|
338
|
+
if (!jobId) return 1;
|
|
339
|
+
const channel = normalizeUpdateChannel(deps.args[2]);
|
|
340
|
+
await runGuiUpdateWorker(jobId, channel, deps.args[3] === "restart");
|
|
341
|
+
return 0;
|
|
342
|
+
},
|
|
343
|
+
restart: async deps => {
|
|
344
|
+
// The running proxy owns its drain and replacement through /api/system/restart.
|
|
345
|
+
// If nothing is live, restart degrades to the documented `ensure` start behavior.
|
|
346
|
+
await deps.handleProxyRestart(deps.handleRestartStartWhenStopped);
|
|
347
|
+
return Number(process.exitCode ?? 0);
|
|
348
|
+
},
|
|
349
|
+
health: async deps => {
|
|
350
|
+
const healthArgs = deps.args.slice(1);
|
|
351
|
+
const wantsHealthJson = healthArgs.includes("--json");
|
|
352
|
+
const live = await deps.findLiveProxy();
|
|
353
|
+
if (wantsHealthJson) {
|
|
354
|
+
console.log(JSON.stringify({ ok: !!live, pid: live?.pid ?? null, port: live?.port ?? null }));
|
|
355
|
+
} else {
|
|
356
|
+
console.log(live ? `Proxy healthy (PID ${live.pid}, port ${live.port})` : "Proxy not healthy");
|
|
357
|
+
}
|
|
358
|
+
return live ? 0 : 1;
|
|
359
|
+
},
|
|
360
|
+
ready: async deps => {
|
|
361
|
+
// Fail-closed impossible-state guard: readyArgs is populated by the
|
|
362
|
+
// preparse block in src/cli/root.ts before maybeAutoRestoreCodexShim, so
|
|
363
|
+
// reaching here without it means dispatch diverged. Refuse with code 64
|
|
364
|
+
// and perform NO I/O (no discovery/probe). process.exit is `never`,
|
|
365
|
+
// narrowing below.
|
|
366
|
+
const readyArgs = deps.head.readyArgs;
|
|
367
|
+
if (!readyArgs) return 64;
|
|
368
|
+
return await deps.handleReady(readyArgs);
|
|
369
|
+
},
|
|
370
|
+
provider: async deps => {
|
|
371
|
+
const { handleProviderCommand } = await import("./provider");
|
|
372
|
+
await handleProviderCommand(deps.args.slice(1));
|
|
373
|
+
return 0;
|
|
374
|
+
},
|
|
375
|
+
account: async deps => {
|
|
376
|
+
const { cmdAccount } = await import("./account");
|
|
377
|
+
return await cmdAccount(deps.args.slice(1));
|
|
378
|
+
},
|
|
379
|
+
models: async deps => {
|
|
380
|
+
const { handleModels } = await import("./models");
|
|
381
|
+
await handleModels(deps.args.slice(1));
|
|
382
|
+
return 0;
|
|
383
|
+
},
|
|
384
|
+
combo: async deps => {
|
|
385
|
+
const { handleComboCommand } = await import("./combo");
|
|
386
|
+
return await handleComboCommand(deps.args.slice(1));
|
|
387
|
+
},
|
|
388
|
+
route: async deps => {
|
|
389
|
+
if (deps.args[1] !== "combo" && deps.args[1] !== "policy") {
|
|
390
|
+
console.error("Usage: ocx route <combo|policy> <subcommand>");
|
|
391
|
+
return 2;
|
|
392
|
+
}
|
|
393
|
+
if (deps.args[1] === "combo") {
|
|
394
|
+
const { handleComboCommand } = await import("./combo");
|
|
395
|
+
return await handleComboCommand(deps.args.slice(2));
|
|
396
|
+
} else {
|
|
397
|
+
const { handleRoutePolicyCommand } = await import("./route-policy");
|
|
398
|
+
return await handleRoutePolicyCommand(deps.args.slice(2));
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
agent: async deps => {
|
|
402
|
+
const { handleAgentCommand } = await import("./agent");
|
|
403
|
+
return await handleAgentCommand(deps.args.slice(1));
|
|
404
|
+
},
|
|
405
|
+
observe: async deps => {
|
|
406
|
+
const { handleObserveCommand } = await import("./observe");
|
|
407
|
+
return await handleObserveCommand(deps.args.slice(1));
|
|
408
|
+
},
|
|
409
|
+
logs: async deps => {
|
|
410
|
+
const { handleObserveCommand } = await import("./observe");
|
|
411
|
+
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
412
|
+
},
|
|
413
|
+
usage: async deps => {
|
|
414
|
+
const { handleObserveCommand } = await import("./observe");
|
|
415
|
+
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
416
|
+
},
|
|
417
|
+
storage: async deps => {
|
|
418
|
+
const { handleObserveCommand } = await import("./observe");
|
|
419
|
+
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
420
|
+
},
|
|
421
|
+
memory: async deps => {
|
|
422
|
+
const { handleObserveCommand } = await import("./observe");
|
|
423
|
+
return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
|
|
424
|
+
},
|
|
425
|
+
access: async deps => {
|
|
426
|
+
const { handleAccessCommand } = await import("./access");
|
|
427
|
+
return await handleAccessCommand(deps.args.slice(1));
|
|
428
|
+
},
|
|
429
|
+
"api-key": async deps => {
|
|
430
|
+
const { handleAccessCommand } = await import("./access");
|
|
431
|
+
return await handleAccessCommand(["key", ...deps.args.slice(1)]);
|
|
432
|
+
},
|
|
433
|
+
export: async deps => {
|
|
434
|
+
const { handleExportCommand } = await import("./export-command");
|
|
435
|
+
return await handleExportCommand(deps.args.slice(1));
|
|
436
|
+
},
|
|
437
|
+
grok: async deps => {
|
|
438
|
+
const { handleGrokCommand } = await import("./integrations");
|
|
439
|
+
return await handleGrokCommand(deps.args.slice(1));
|
|
440
|
+
},
|
|
441
|
+
integration: async deps => {
|
|
442
|
+
const integration = deps.args[1];
|
|
443
|
+
if (integration === "grok") {
|
|
444
|
+
const { handleGrokCommand } = await import("./integrations");
|
|
445
|
+
return await handleGrokCommand(deps.args.slice(2));
|
|
446
|
+
} else if (integration === "claude") {
|
|
447
|
+
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
448
|
+
return await handleClaudeConfigCommand(deps.args.slice(2));
|
|
449
|
+
} else if (integration === "client") {
|
|
450
|
+
const { handleClientIntegrationCommand } = await import("./integrations");
|
|
451
|
+
return await handleClientIntegrationCommand(deps.args.slice(2));
|
|
452
|
+
} else {
|
|
453
|
+
console.error("Usage: ocx integration <claude|grok|client> <subcommand>");
|
|
454
|
+
return 2;
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
system: async deps => {
|
|
458
|
+
const { handleSystemCommand } = await import("./system-command");
|
|
459
|
+
return await handleSystemCommand(deps.args.slice(1));
|
|
460
|
+
},
|
|
461
|
+
config: async deps => {
|
|
462
|
+
const { handleConfigCommand } = await import("./config-command");
|
|
463
|
+
return await handleConfigCommand(deps.args.slice(1));
|
|
464
|
+
},
|
|
465
|
+
lab: async deps => {
|
|
466
|
+
const { handleLabCommand } = await import("./lab");
|
|
467
|
+
return await handleLabCommand(deps.args.slice(1));
|
|
468
|
+
},
|
|
469
|
+
claude: async deps => {
|
|
470
|
+
const { cmdClaude } = await import("./claude");
|
|
471
|
+
// "ocx claude desktop" → write Desktop 3P config
|
|
472
|
+
if (deps.args[1] === "desktop") {
|
|
473
|
+
const { handleClaudeDesktopCommand } = await import("./claude-desktop");
|
|
474
|
+
const exitCode = await handleClaudeDesktopCommand(deps.args.slice(2));
|
|
475
|
+
if (exitCode !== 0) return exitCode;
|
|
476
|
+
return 0;
|
|
477
|
+
}
|
|
478
|
+
if (deps.args[1] === "config") {
|
|
479
|
+
const { handleClaudeConfigCommand } = await import("./integrations");
|
|
480
|
+
return await handleClaudeConfigCommand(deps.args.slice(2));
|
|
481
|
+
}
|
|
482
|
+
return await cmdClaude(deps.args.slice(1));
|
|
483
|
+
},
|
|
484
|
+
opencode: async deps => {
|
|
485
|
+
const { cmdOpencode } = await import("./opencode");
|
|
486
|
+
return await cmdOpencode(deps.args.slice(1));
|
|
487
|
+
},
|
|
488
|
+
help: async () => {
|
|
489
|
+
printUsage();
|
|
490
|
+
return 0;
|
|
491
|
+
},
|
|
492
|
+
"--help": async () => {
|
|
493
|
+
printUsage();
|
|
494
|
+
return 0;
|
|
495
|
+
},
|
|
496
|
+
"-h": async () => {
|
|
497
|
+
printUsage();
|
|
498
|
+
return 0;
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/** Registry alias pairs → canonical dispatch name (init/setup, restore/eject, …). */
|
|
503
|
+
const aliasTargets = new Map<string, string>();
|
|
504
|
+
for (const entry of CLI_COMMANDS) {
|
|
505
|
+
for (const alias of entry.aliases ?? []) aliasTargets.set(alias, entry.name);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export const DISPATCH_COMMANDS: ReadonlySet<string> = new Set(Object.keys(commandRunners));
|
|
509
|
+
export const DISPATCH_ALIASES: ReadonlyMap<string, string> = aliasTargets;
|
|
510
|
+
|
|
511
|
+
/** Resolve the runner key for a command, following registry aliases to the
|
|
512
|
+
* canonical runner. Returns undefined when the command is unknown. */
|
|
513
|
+
export function resolveDispatchCommand(command: string | undefined): string | undefined {
|
|
514
|
+
if (command === undefined) return undefined;
|
|
515
|
+
if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command;
|
|
516
|
+
return aliasTargets.get(command);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function dispatchCommand(head: CliHead, deps: CliDispatchDeps): Promise<number> {
|
|
520
|
+
const command = head.command;
|
|
521
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
522
|
+
printUsage();
|
|
523
|
+
return 0;
|
|
524
|
+
}
|
|
525
|
+
const runner = commandRunners[resolveDispatchCommand(command) ?? ""];
|
|
526
|
+
if (!runner) {
|
|
527
|
+
console.error(`Unknown command: ${command}`);
|
|
528
|
+
printUsage();
|
|
529
|
+
return 1;
|
|
530
|
+
}
|
|
531
|
+
return await runner(deps);
|
|
532
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -10,14 +10,13 @@
|
|
|
10
10
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid,
|
|
14
|
-
import { findLiveProxy } from "../server/proxy-liveness";
|
|
15
|
-
import { gracefulStopHost } from "../lib/process-control";
|
|
13
|
+
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
|
|
14
|
+
import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
16
15
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
17
16
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
18
17
|
import { maskAccountId } from "../lib/privacy";
|
|
19
18
|
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
|
|
20
|
-
import {
|
|
19
|
+
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
21
20
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
22
21
|
import { withNativeMainSharedClaim } from "../codex/native-main-claim";
|
|
23
22
|
import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "../codex/native-profile-store";
|
|
@@ -42,6 +41,10 @@ import {
|
|
|
42
41
|
} from "../codex/runtime";
|
|
43
42
|
import { CODEX_REAUTH_ACTION, collectOAuthHealthEntriesForCli, MASKED_ACCOUNT_FALLBACK, type OAuthHealthEntry } from "../oauth/health";
|
|
44
43
|
import { getAuthRefreshIntentLockPath, getAuthStorePath } from "../oauth/store";
|
|
44
|
+
import {
|
|
45
|
+
fetchBoundLocalManagementRead,
|
|
46
|
+
type LocalManagementReadDeps,
|
|
47
|
+
} from "../server/local-management-read-client";
|
|
45
48
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
46
49
|
|
|
47
50
|
export type OAuthDoctorCheck = { level: "OK" | "WARN"; message: string };
|
|
@@ -610,20 +613,28 @@ function observedMemory(data: { rss: number; external?: number; arrayBuffers?: n
|
|
|
610
613
|
}
|
|
611
614
|
|
|
612
615
|
export async function fetchServiceMemory(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
token: string | null,
|
|
616
|
-
fetchImpl: typeof fetch = fetch,
|
|
616
|
+
target: LiveProxy,
|
|
617
|
+
deps: LocalManagementReadDeps = {},
|
|
617
618
|
): Promise<ServiceMemoryReport> {
|
|
618
619
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
const read = await fetchBoundLocalManagementRead(target, LOCAL_MANAGEMENT_READ_PATHS.systemMemory, {
|
|
621
|
+
...deps,
|
|
622
|
+
timeoutMs: SERVICE_MEMORY_TIMEOUT_MS,
|
|
622
623
|
});
|
|
624
|
+
if (read.kind === "unavailable") {
|
|
625
|
+
return read.reason === "transport"
|
|
626
|
+
? { status: "unreachable", error: "fetch failed" }
|
|
627
|
+
: { status: "unauthorized" };
|
|
628
|
+
}
|
|
629
|
+
const { response: res, targetPid } = read;
|
|
623
630
|
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
624
631
|
if (!res.ok) return { status: "unreachable", error: `http ${res.status}` };
|
|
625
632
|
const body = await res.json() as Partial<ServiceMemoryData>;
|
|
626
|
-
if (
|
|
633
|
+
if (
|
|
634
|
+
body.pid !== targetPid
|
|
635
|
+
|| typeof body.bunVersion !== "string"
|
|
636
|
+
|| typeof body.rss !== "number"
|
|
637
|
+
) {
|
|
627
638
|
return { status: "unreachable", error: "malformed response" };
|
|
628
639
|
}
|
|
629
640
|
return {
|
|
@@ -672,7 +683,7 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
|
|
|
672
683
|
const lines: string[] = [];
|
|
673
684
|
lines.push(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
674
685
|
if (report.status === "unauthorized") {
|
|
675
|
-
lines.push(" --
|
|
686
|
+
lines.push(" -- local diagnostic capability unavailable — restart the running proxy with this OpenCodex version");
|
|
676
687
|
return lines;
|
|
677
688
|
}
|
|
678
689
|
if (report.status === "unreachable") {
|
|
@@ -844,10 +855,6 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
844
855
|
const live = await findLiveProxy({
|
|
845
856
|
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
846
857
|
});
|
|
847
|
-
const livePid = live ? live.pid : readPid();
|
|
848
|
-
const liveRuntime = live
|
|
849
|
-
? { pid: live.pid ?? 0, port: live.port, hostname: live.hostname }
|
|
850
|
-
: (livePid ? readRuntimePort(livePid) : null);
|
|
851
858
|
|
|
852
859
|
const currentProxyEnv = collectProxyEnv();
|
|
853
860
|
const configuredProxy = collectConfiguredProxy();
|
|
@@ -887,13 +894,11 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
887
894
|
|
|
888
895
|
console.log("\nMemory / runtime");
|
|
889
896
|
{
|
|
890
|
-
|
|
891
|
-
if (!runtime || !live) {
|
|
897
|
+
if (!live) {
|
|
892
898
|
console.log(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
893
899
|
console.log(" -- no running ocx proxy found (no live pid/runtime record)");
|
|
894
900
|
} else {
|
|
895
|
-
const
|
|
896
|
-
const report = await fetchServiceMemory(gracefulStopHost(runtime.hostname), runtime.port, token);
|
|
901
|
+
const report = await fetchServiceMemory(live);
|
|
897
902
|
for (const line of formatServiceMemoryLines(report)) console.log(line);
|
|
898
903
|
}
|
|
899
904
|
}
|