@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether this process was started by a coding agent rather than typed
|
|
3
|
+
* by a person.
|
|
4
|
+
*
|
|
5
|
+
* Agent harnesses run `ocx` on the user's behalf and answer prompts from their
|
|
6
|
+
* own logic, which means a consent question would be decided by the agent
|
|
7
|
+
* instead of the account owner. Prompts that act on the user's identity check
|
|
8
|
+
* this and defer instead: they stay silent so the question reaches the human on
|
|
9
|
+
* a later hand-typed run.
|
|
10
|
+
*
|
|
11
|
+
* Detection is env-var based and deliberately conservative — a false positive
|
|
12
|
+
* only postpones a prompt, while a false negative would let an agent answer for
|
|
13
|
+
* the user.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Env vars set by agent harnesses and CI runners inside the shell they spawn. */
|
|
17
|
+
const AGENT_ENV_VARS = [
|
|
18
|
+
"CLAUDECODE",
|
|
19
|
+
"CLAUDE_CODE_ENTRYPOINT",
|
|
20
|
+
"CODEX_THREAD_ID",
|
|
21
|
+
"CODEX_SHELL",
|
|
22
|
+
"CODEX_CI",
|
|
23
|
+
"CURSOR_TRACE_ID",
|
|
24
|
+
"CURSOR_SESSION_TOKEN",
|
|
25
|
+
"AIDER_CHAT",
|
|
26
|
+
"REPL_ID",
|
|
27
|
+
"CI",
|
|
28
|
+
"GITHUB_ACTIONS",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when an agent or automated runner is driving this process. Reads the
|
|
33
|
+
* environment on every call so tests and long-lived processes see current state.
|
|
34
|
+
*/
|
|
35
|
+
export function isAgentDriven(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
36
|
+
return AGENT_ENV_VARS.some(name => (env[name] ?? "").trim() !== "");
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
type GatherRoutedModels = (config: OcxConfig) => Promise<unknown>;
|
|
4
|
+
|
|
5
|
+
export type CatalogPrewarmDeps = {
|
|
6
|
+
loadConfig?: () => OcxConfig;
|
|
7
|
+
importCatalog?: () => Promise<{ gatherRoutedModels: GatherRoutedModels }>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* After the listen port is bound, kick off live provider discovery so the first
|
|
12
|
+
* GUI /v1/models and syncModelsToCodex share one gather flight instead of racing
|
|
13
|
+
* duplicate upstream /models fetches.
|
|
14
|
+
*/
|
|
15
|
+
export function scheduleCatalogPrewarm(deps: CatalogPrewarmDeps = {}): void {
|
|
16
|
+
void Promise.resolve()
|
|
17
|
+
.then(async () => {
|
|
18
|
+
const load = deps.loadConfig ?? (await import("../config")).loadConfig;
|
|
19
|
+
const { gatherRoutedModels } = await (deps.importCatalog?.() ?? import("../codex/catalog"));
|
|
20
|
+
return gatherRoutedModels(load());
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {});
|
|
23
|
+
}
|
|
24
|
+
|
package/src/cli/claude.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
|
|
|
14
14
|
import { commandInvocation } from "../lib/win-exec";
|
|
15
15
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
17
18
|
import { PROXY_MARKER, ownAdmissionTokens, defaultAuthDetectDeps, detectClaudeAuth, type AuthDetectDeps } from "../claude/auth-detect";
|
|
18
19
|
import { resolveClaudeAuthMode } from "../claude/auth-mode";
|
|
19
20
|
|
|
@@ -47,6 +48,27 @@ export function buildClaudeEnv(
|
|
|
47
48
|
// leaving the child with no token at all (audit R2-1). It is opencodex state, never
|
|
48
49
|
// user auth, so dropping it unconditionally is safe.
|
|
49
50
|
if (env.ANTHROPIC_AUTH_TOKEN === PROXY_MARKER) delete env.ANTHROPIC_AUTH_TOKEN;
|
|
51
|
+
// Step 1b — drop Anthropic credentials that the bundled Bun runtime synthesized from a
|
|
52
|
+
// project `.env`/`.env.local` (issue #701). Claude Code disables claude.ai connectors the
|
|
53
|
+
// moment either token slot is populated, so an ambient project file silently moved a
|
|
54
|
+
// subscriber onto API billing while their OAuth login stayed healthy. The npm launcher
|
|
55
|
+
// runs under Node, which does NOT auto-load dotenv, so it records the slots that existed
|
|
56
|
+
// before Bun started; anything populated now but absent then came from the working
|
|
57
|
+
// directory, not from the user. A genuine shell export is still honored, which keeps
|
|
58
|
+
// auto-mode API-key auth working. An ABSENT marker means provenance is unknowable
|
|
59
|
+
// (a direct `bun src/cli/index.ts` run, a test, or an older launcher), and then we
|
|
60
|
+
// change nothing rather than guess — an EMPTY marker is different: the launcher ran
|
|
61
|
+
// and saw no pre-existing slots.
|
|
62
|
+
const preBunSlots = base.OCX_PRE_BUN_ANTHROPIC_ENV;
|
|
63
|
+
if (preBunSlots !== undefined) {
|
|
64
|
+
const exported = new Set(preBunSlots.split(",").filter(name => name.length > 0));
|
|
65
|
+
for (const name of ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"] as const) {
|
|
66
|
+
const value = env[name];
|
|
67
|
+
if (value !== undefined && value !== "" && !exported.has(name)) delete env[name];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Never forward the seam itself to Claude Code.
|
|
71
|
+
delete env.OCX_PRE_BUN_ANTHROPIC_ENV;
|
|
50
72
|
const setDefault = (name: string, value: string | undefined) => {
|
|
51
73
|
if (value === undefined || value.length === 0) return;
|
|
52
74
|
if (env[name] !== undefined && env[name] !== "") return; // user wins
|
|
@@ -75,15 +97,19 @@ export function buildClaudeEnv(
|
|
|
75
97
|
if ((config.apiKeys?.length ?? 0) > 0) {
|
|
76
98
|
setDefault("ANTHROPIC_AUTH_TOKEN", config.apiKeys![0].key);
|
|
77
99
|
}
|
|
78
|
-
// Detection reads the
|
|
79
|
-
// spawned process cannot disagree.
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
100
|
+
// Detection reads the SANITIZED launch env — the exact object spawned below — so the
|
|
101
|
+
// resolver and the spawned process cannot disagree. It deliberately does NOT read the
|
|
102
|
+
// raw base: the provenance strip above already removed dotenv-only credentials, and
|
|
103
|
+
// letting a value the child never receives decide the marker left an auto-mode user
|
|
104
|
+
// with neither the credential NOR the proxy marker (#701 audit round 2). Injected deps
|
|
105
|
+
// are spread FIRST and `env` bound LAST, and the injection type excludes `env`, so a
|
|
106
|
+
// test fake cannot break that. `ownTokens` is bound last for the same reason: it is
|
|
107
|
+
// config-derived, and a fake that replaced it could make our own admission key look
|
|
108
|
+
// like user auth.
|
|
83
109
|
const resolved = resolveClaudeAuthMode(config, detectClaudeAuth({
|
|
84
|
-
...defaultAuthDetectDeps(
|
|
110
|
+
...defaultAuthDetectDeps(env as NodeJS.ProcessEnv),
|
|
85
111
|
...(deps.authDetect ?? {}),
|
|
86
|
-
env: () =>
|
|
112
|
+
env: () => env as NodeJS.ProcessEnv,
|
|
87
113
|
ownTokens: ownAdmissionTokens(config),
|
|
88
114
|
}));
|
|
89
115
|
if (!env.ANTHROPIC_AUTH_TOKEN && resolved.markerMode === "proxy") {
|
|
@@ -149,14 +175,13 @@ export function buildClaudeEnv(
|
|
|
149
175
|
|
|
150
176
|
/**
|
|
151
177
|
* Context-window map from the RUNNING proxy's management API (warm TTL cache; the
|
|
152
|
-
* daemon registers every selector form — audit R3#1). 3s bound + auth header
|
|
153
|
-
* (OPENCODEX_API_AUTH_TOKEN first, config key fallback — audit R4#1). Failure → {}
|
|
178
|
+
* daemon registers every selector form — audit R3#1). 3s bound + management auth header.
|
|
154
179
|
* (no [1m] marking, conservative).
|
|
155
180
|
*/
|
|
156
181
|
export async function fetchClaudeContextWindows(config: OcxConfig, port: number, timeoutMs = 3_000): Promise<Record<string, number>> {
|
|
157
182
|
try {
|
|
158
183
|
const headers = new Headers();
|
|
159
|
-
const token =
|
|
184
|
+
const token = configuredAdminToken();
|
|
160
185
|
if (token) headers.set("x-opencodex-api-key", token);
|
|
161
186
|
const res = await fetch(`http://127.0.0.1:${port}/api/claude-code`, {
|
|
162
187
|
headers,
|
package/src/cli/doctor.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
|
|
14
|
+
import { findLiveProxy } from "../server/proxy-liveness";
|
|
14
15
|
import { gracefulStopHost } from "../lib/process-control";
|
|
15
16
|
import { maskAccountId } from "../lib/privacy";
|
|
16
|
-
import {
|
|
17
|
+
import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
|
|
18
|
+
import { configuredAdminToken } from "../lib/admin-secrets";
|
|
17
19
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
18
20
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
19
21
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
@@ -285,17 +287,15 @@ export function collectWslDualInstall(deps: WslDualInstallDeps = {}): WslDualIns
|
|
|
285
287
|
};
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"] as const;
|
|
289
|
-
|
|
290
290
|
export type ProxyEnvRow = { key: string; present: boolean };
|
|
291
291
|
export type EnvMap = Record<string, string | undefined>;
|
|
292
292
|
|
|
293
293
|
/** Report only presence/absence of proxy env vars - never the value (it may
|
|
294
294
|
* embed credentials). Checks both upper- and lower-case forms. */
|
|
295
295
|
export function collectProxyEnv(env: EnvMap = process.env): ProxyEnvRow[] {
|
|
296
|
-
return
|
|
296
|
+
return PROXY_ENV_KEYS.map(key => ({
|
|
297
297
|
key,
|
|
298
|
-
present:
|
|
298
|
+
present: proxyEnvPresent(key, env),
|
|
299
299
|
}));
|
|
300
300
|
}
|
|
301
301
|
|
|
@@ -307,13 +307,47 @@ export type ConfiguredProxyDiagnostic = {
|
|
|
307
307
|
detail: string;
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
-
function envReferenceName(value: string): string | null {
|
|
310
|
+
export function envReferenceName(value: string): string | null {
|
|
311
311
|
const braced = value.match(/^\$\{(\w+)\}$/);
|
|
312
312
|
if (braced) return braced[1]!;
|
|
313
313
|
const bare = value.match(/^\$(\w+)$/);
|
|
314
314
|
return bare ? bare[1]! : null;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
export type ProviderApiKeyDiagnostic = {
|
|
318
|
+
provider: string;
|
|
319
|
+
envName: string;
|
|
320
|
+
detail: string;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/** Warn when a key-auth provider's apiKey env reference resolves empty in this process. */
|
|
324
|
+
export function collectProviderApiKeyDiagnostics(
|
|
325
|
+
providers: Record<string, { authMode?: string; apiKey?: string }> = readConfigDiagnostics().config.providers ?? {},
|
|
326
|
+
env: EnvMap = process.env,
|
|
327
|
+
): ProviderApiKeyDiagnostic[] {
|
|
328
|
+
const resolveInEnv = (value: string): string | undefined => {
|
|
329
|
+
const name = envReferenceName(value);
|
|
330
|
+
if (!name) return value;
|
|
331
|
+
return env[name];
|
|
332
|
+
};
|
|
333
|
+
const rows: ProviderApiKeyDiagnostic[] = [];
|
|
334
|
+
for (const [provider, config] of Object.entries(providers)) {
|
|
335
|
+
if (config.authMode !== "key") continue;
|
|
336
|
+
const raw = typeof config.apiKey === "string" ? config.apiKey.trim() : "";
|
|
337
|
+
if (!raw) continue;
|
|
338
|
+
const envName = envReferenceName(raw);
|
|
339
|
+
if (!envName) continue;
|
|
340
|
+
const resolved = resolveInEnv(raw);
|
|
341
|
+
if (resolved?.trim()) continue;
|
|
342
|
+
rows.push({
|
|
343
|
+
provider,
|
|
344
|
+
envName,
|
|
345
|
+
detail: `provider ${provider}: env reference ${envName} is unset or empty in this process`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return rows;
|
|
349
|
+
}
|
|
350
|
+
|
|
317
351
|
export function collectConfiguredProxy(): ConfiguredProxyDiagnostic {
|
|
318
352
|
const diagnostics = readConfigDiagnostics();
|
|
319
353
|
const rawProxy = typeof diagnostics.config.proxy === "string" ? diagnostics.config.proxy.trim() : "";
|
|
@@ -571,7 +605,7 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
|
|
|
571
605
|
const lines: string[] = [];
|
|
572
606
|
lines.push(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
573
607
|
if (report.status === "unauthorized") {
|
|
574
|
-
lines.push(" -- proxy reachable but rejected the request — set
|
|
608
|
+
lines.push(" -- proxy reachable but rejected the request — set OPENCODEX_ADMIN_AUTH_TOKEN to match the service");
|
|
575
609
|
return lines;
|
|
576
610
|
}
|
|
577
611
|
if (report.status === "unreachable") {
|
|
@@ -718,9 +752,21 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
718
752
|
}
|
|
719
753
|
}
|
|
720
754
|
|
|
755
|
+
// #618: identity-verified liveness first so pid-file absence does not hide a live service.
|
|
756
|
+
// Reuse the diagnostics config already loaded above so doctor stays read-only on malformed JSON.
|
|
757
|
+
const live = await findLiveProxy({
|
|
758
|
+
configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
|
|
759
|
+
});
|
|
760
|
+
const livePid = live ? live.pid : readPid();
|
|
761
|
+
const liveRuntime = live
|
|
762
|
+
? { pid: live.pid ?? 0, port: live.port, hostname: live.hostname }
|
|
763
|
+
: (livePid ? readRuntimePort(livePid) : null);
|
|
764
|
+
|
|
721
765
|
const currentProxyEnv = collectProxyEnv();
|
|
722
766
|
const configuredProxy = collectConfiguredProxy();
|
|
723
|
-
const runningProxyEnv = collectRunningProxyEnv(
|
|
767
|
+
const runningProxyEnv = collectRunningProxyEnv({
|
|
768
|
+
readPidFn: () => (live ? live.pid : readPid()),
|
|
769
|
+
});
|
|
724
770
|
|
|
725
771
|
console.log("\nCurrent doctor process proxy env (presence only)");
|
|
726
772
|
for (const row of currentProxyEnv) {
|
|
@@ -730,6 +776,16 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
730
776
|
console.log("\nConfigured proxy (value hidden)");
|
|
731
777
|
console.log(` ${configuredProxy.present ? "set " : "unset "} ${configuredProxy.key} (${configuredProxy.source}; ${configuredProxy.detail})`);
|
|
732
778
|
|
|
779
|
+
const providerApiKeys = collectProviderApiKeyDiagnostics(doctorConfig.providers);
|
|
780
|
+
console.log("\nProvider API keys (value hidden)");
|
|
781
|
+
if (providerApiKeys.length === 0) {
|
|
782
|
+
console.log(" ok no empty env-referenced provider keys detected in this process");
|
|
783
|
+
} else {
|
|
784
|
+
for (const row of providerApiKeys) {
|
|
785
|
+
console.log(` !! ${row.detail}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
733
789
|
console.log("\nRunning proxy process proxy env (presence only)");
|
|
734
790
|
if (runningProxyEnv.status === "not_running") {
|
|
735
791
|
console.log(" -- no running ocx proxy process found");
|
|
@@ -742,21 +798,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
742
798
|
}
|
|
743
799
|
}
|
|
744
800
|
|
|
745
|
-
// #314: service-process memory/runtime identity via the authed management
|
|
746
|
-
// endpoint. readPid() FIRST (liveness), then the pid-scoped runtime record —
|
|
747
|
-
// readRuntimePort alone can serve a stale file pointing at a foreign port.
|
|
748
|
-
// Hoisted out of the block below: the Hints section reuses the same liveness pair
|
|
749
|
-
// for the proxy-down restart hint.
|
|
750
|
-
const livePid = readPid();
|
|
751
|
-
const liveRuntime = livePid ? readRuntimePort(livePid) : null;
|
|
752
801
|
console.log("\nMemory / runtime");
|
|
753
802
|
{
|
|
754
803
|
const runtime = liveRuntime;
|
|
755
|
-
if (!runtime) {
|
|
804
|
+
if (!runtime || !live) {
|
|
756
805
|
console.log(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
757
806
|
console.log(" -- no running ocx proxy found (no live pid/runtime record)");
|
|
758
807
|
} else {
|
|
759
|
-
const token =
|
|
808
|
+
const token = configuredAdminToken();
|
|
760
809
|
const report = await fetchServiceMemory(gracefulStopHost(runtime.hostname), runtime.port, token);
|
|
761
810
|
for (const line of formatServiceMemoryLines(report)) console.log(line);
|
|
762
811
|
}
|
|
@@ -815,11 +864,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
815
864
|
// Hints, not fixes.
|
|
816
865
|
const hints: string[] = [];
|
|
817
866
|
const proxyDown = proxyDownRestartHint({
|
|
818
|
-
proxyRunning: Boolean(
|
|
819
|
-
port: doctorConfig.port ?? 10100,
|
|
867
|
+
proxyRunning: Boolean(live),
|
|
868
|
+
port: live?.port ?? doctorConfig.port ?? 10100,
|
|
820
869
|
serviceViable: startup.serviceViable,
|
|
821
870
|
});
|
|
822
871
|
if (proxyDown) hints.push(proxyDown);
|
|
872
|
+
for (const row of providerApiKeys) {
|
|
873
|
+
hints.push(`${row.detail}. Set ${row.envName} in the shell that starts the proxy, or store a literal key in config (value hidden here).`);
|
|
874
|
+
}
|
|
823
875
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
824
876
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
825
877
|
if (!startup.rebootSafe) {
|
package/src/cli/help.ts
CHANGED
|
@@ -30,12 +30,18 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
30
30
|
uninstall: {
|
|
31
31
|
usage: "ocx uninstall",
|
|
32
32
|
summary: "Remove service/shim/config and restore native Codex.",
|
|
33
|
-
details: [
|
|
33
|
+
details: [
|
|
34
|
+
"Alias: ocx remove",
|
|
35
|
+
"Config cleanup requires ownership metadata created by a fresh install; legacy or shared directories are left in place.",
|
|
36
|
+
],
|
|
34
37
|
},
|
|
35
38
|
remove: {
|
|
36
39
|
usage: "ocx remove",
|
|
37
40
|
summary: "Remove service/shim/config and restore native Codex.",
|
|
38
|
-
details: [
|
|
41
|
+
details: [
|
|
42
|
+
"Alias of: ocx uninstall",
|
|
43
|
+
"Config cleanup requires ownership metadata created by a fresh install; legacy or shared directories are left in place.",
|
|
44
|
+
],
|
|
39
45
|
},
|
|
40
46
|
service: {
|
|
41
47
|
usage: "ocx service [install|start|stop|status|uninstall|remove]",
|
|
@@ -60,8 +66,22 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
60
66
|
],
|
|
61
67
|
},
|
|
62
68
|
ensure: { usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
|
|
63
|
-
sync: {
|
|
64
|
-
|
|
69
|
+
sync: {
|
|
70
|
+
usage: "ocx sync [--restart-codex]",
|
|
71
|
+
summary: "Fetch provider models and inject them into Codex config.",
|
|
72
|
+
details: [
|
|
73
|
+
"After writing the catalog, warns if long-lived Codex app-server processes are still running.",
|
|
74
|
+
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
"sync-cache": {
|
|
78
|
+
usage: "ocx sync-cache [--restart-codex]",
|
|
79
|
+
summary: "Refresh Codex's model cache from the active catalog.",
|
|
80
|
+
details: [
|
|
81
|
+
"Warns when Codex app-server processes still hold an in-memory model list.",
|
|
82
|
+
"--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
65
85
|
status: { usage: "ocx status", summary: "Check proxy server status." },
|
|
66
86
|
doctor: { usage: "ocx doctor", summary: "Diagnose environment/network issues (paths, WSL /mnt, proxy env, ChatGPT reachability)." },
|
|
67
87
|
debug: {
|
|
@@ -184,6 +204,20 @@ const helpEntries: Record<string, HelpEntry> = {
|
|
|
184
204
|
"Claude Code settings: ocx claude config <status|set> ...",
|
|
185
205
|
],
|
|
186
206
|
},
|
|
207
|
+
opencode: {
|
|
208
|
+
usage: "ocx opencode [opencode args...]",
|
|
209
|
+
summary: "Launch opencode wired to the proxy (runtime provider config).",
|
|
210
|
+
details: [
|
|
211
|
+
"Ensures the proxy is running, then execs `opencode` with the generated `provider.opencodex`",
|
|
212
|
+
"block injected through OpenCode's inline runtime layer (`OPENCODE_CONFIG_CONTENT`). Any",
|
|
213
|
+
"existing inline config in the environment is preserved and only `provider.opencodex` is",
|
|
214
|
+
"overwritten for this launch.",
|
|
215
|
+
"Global/project opencode.json may be read to warn about an existing provider.opencodex",
|
|
216
|
+
"override; on-disk files are never modified.",
|
|
217
|
+
"Routed models appear in the model picker as opencodex/<provider>/<model>.",
|
|
218
|
+
"Stop using `ocx opencode` and plain `opencode` behaves exactly as before.",
|
|
219
|
+
],
|
|
220
|
+
},
|
|
187
221
|
restart: {
|
|
188
222
|
usage: "ocx restart",
|
|
189
223
|
summary: "Stop the proxy and restart it (background). Equivalent to stop + ensure.",
|
|
@@ -232,8 +266,9 @@ Usage:
|
|
|
232
266
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
|
233
267
|
ocx tray <sub> Windows status tray (install|start|stop|status|uninstall)
|
|
234
268
|
ocx ensure Ensure the proxy is running and Codex config/cache are current
|
|
235
|
-
ocx sync
|
|
236
|
-
ocx sync-cache
|
|
269
|
+
ocx sync [--restart-codex] Fetch models from providers and inject into Codex config
|
|
270
|
+
ocx sync-cache [--restart-codex]
|
|
271
|
+
Refresh Codex's model cache from the active catalog
|
|
237
272
|
ocx status Check proxy server status
|
|
238
273
|
ocx doctor Diagnose environment/network issues (WSL, proxy, ChatGPT reachability)
|
|
239
274
|
ocx debug <scope> provider/usage/injection/claude on|off|status|reset
|
|
@@ -256,6 +291,7 @@ Usage:
|
|
|
256
291
|
ocx config <sub> Validated configuration show/get/set/import/export
|
|
257
292
|
ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
|
|
258
293
|
ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
|
|
294
|
+
ocx opencode [args...] Launch opencode wired to the proxy (runtime provider config)
|
|
259
295
|
ocx help [command] Show help
|
|
260
296
|
ocx --version | -v Print version
|
|
261
297
|
|
package/src/cli/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { rmSync } from "node:fs";
|
|
4
3
|
import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
|
|
5
4
|
import { stripGrokConfig } from "../grok/inject";
|
|
6
5
|
import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
|
|
@@ -31,7 +30,7 @@ import { stopProxy } from "../lib/process-control";
|
|
|
31
30
|
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
32
31
|
import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
|
|
33
32
|
import { startupHealthSummary } from "../codex/autostart-health";
|
|
34
|
-
import { drainAndShutdown, startServer } from "../server";
|
|
33
|
+
import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
|
|
35
34
|
import { injectSystemEnv, revertSystemEnv } from "../server/system-env";
|
|
36
35
|
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
37
36
|
import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
@@ -39,10 +38,12 @@ import { startTokenGuardian } from "../oauth/token-guardian";
|
|
|
39
38
|
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
40
39
|
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
41
40
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
41
|
+
import { scheduleCatalogPrewarm } from "./catalog-prewarm";
|
|
42
42
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
43
43
|
import { syncModelsToCodex } from "../codex/sync";
|
|
44
44
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
45
45
|
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
|
|
46
|
+
import { removeOwnedConfigState } from "../lib/config-ownership";
|
|
46
47
|
|
|
47
48
|
const args = process.argv.slice(2);
|
|
48
49
|
const command = args[0];
|
|
@@ -190,6 +191,10 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
190
191
|
for (let attempt = 0; ; attempt++) {
|
|
191
192
|
try {
|
|
192
193
|
server = startServer(port);
|
|
194
|
+
// Prewarm the live provider model cache as soon as the port is bound so the
|
|
195
|
+
// first GUI /v1/models (and syncModelsToCodex below) share one discovery flight
|
|
196
|
+
// instead of racing duplicate upstream /models fetches.
|
|
197
|
+
scheduleCatalogPrewarm();
|
|
193
198
|
break;
|
|
194
199
|
} catch (err) {
|
|
195
200
|
if (!isAddrInUse(err) || attempt >= 2) throw err;
|
|
@@ -227,24 +232,40 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
227
232
|
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
228
233
|
|
|
229
234
|
let cleaned = false;
|
|
235
|
+
let cleanupSucceeded = true;
|
|
230
236
|
const syncCleanup = () => {
|
|
231
|
-
if (cleaned) return;
|
|
237
|
+
if (cleaned) return cleanupSucceeded;
|
|
232
238
|
cleaned = true;
|
|
233
239
|
try { guardian.stop(); } catch { /* best-effort */ }
|
|
234
240
|
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
235
|
-
|
|
241
|
+
// Dashboard drain-and-restart (#563) must not tear down injection: the replacement
|
|
242
|
+
// process expects Codex/Grok/env fences to still be in place.
|
|
243
|
+
const recycling = isRecyclingForExit();
|
|
244
|
+
if (!recycling) {
|
|
245
|
+
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
246
|
+
}
|
|
236
247
|
removePid(process.pid);
|
|
237
248
|
removeRuntimePort(process.pid);
|
|
238
|
-
if (!process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
239
|
-
try {
|
|
249
|
+
if (!recycling && !process.env.OCX_SERVICE && !currentExternalCodexModelProvider()) {
|
|
250
|
+
try {
|
|
251
|
+
const restored = restoreNativeCodex();
|
|
252
|
+
if (!restored.success) {
|
|
253
|
+
cleanupSucceeded = false;
|
|
254
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${restored.message}`);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
cleanupSucceeded = false;
|
|
258
|
+
console.error(`⚠️ Native Codex restore failed during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
|
259
|
+
}
|
|
240
260
|
}
|
|
241
261
|
// Same ownership rule as `ocx stop`: if the installed service belongs to another home, the
|
|
242
262
|
// Grok fence is shared state we must not remove — that service keeps running and would be
|
|
243
263
|
// left pointing nowhere. This guard also covers signal-driven exits, which is the path that
|
|
244
264
|
// would otherwise bypass handleStop's gate entirely.
|
|
245
|
-
if (!process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
265
|
+
if (!recycling && !process.env.OCX_SERVICE && serviceEnvironmentOwnedHere()) {
|
|
246
266
|
try { stripGrokConfig(); } catch { /* best-effort restore */ }
|
|
247
267
|
}
|
|
268
|
+
return cleanupSucceeded;
|
|
248
269
|
};
|
|
249
270
|
|
|
250
271
|
let shuttingDown = false;
|
|
@@ -269,8 +290,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
269
290
|
try {
|
|
270
291
|
await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
271
292
|
} finally {
|
|
272
|
-
syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
273
|
-
process.exit(0);
|
|
293
|
+
const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
294
|
+
process.exit(restored ? 0 : 1);
|
|
274
295
|
}
|
|
275
296
|
})();
|
|
276
297
|
};
|
|
@@ -288,7 +309,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
288
309
|
// Auto-install .zshrc hook (idempotent — skips if already present).
|
|
289
310
|
installShellHook();
|
|
290
311
|
|
|
291
|
-
await maybeShowStarPrompt(); // once-only
|
|
312
|
+
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
292
313
|
await syncModelsToCodex(port).catch(() => {});
|
|
293
314
|
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
294
315
|
historyGuardian = startHistoryMigrationGuardian();
|
|
@@ -499,7 +520,11 @@ async function handleStop() {
|
|
|
499
520
|
}
|
|
500
521
|
if (!ownershipBlocked) {
|
|
501
522
|
const r = restoreNativeCodex();
|
|
502
|
-
console.log(`↩️ ${r.message}`);
|
|
523
|
+
if (r.success) console.log(`↩️ ${r.message}`);
|
|
524
|
+
else {
|
|
525
|
+
stopFailed = true;
|
|
526
|
+
console.error(`⚠️ ${r.message}`);
|
|
527
|
+
}
|
|
503
528
|
}
|
|
504
529
|
// revertSystemEnv is NOT gated: it carries its own ownership check and concerns launchctl
|
|
505
530
|
// user env, not CODEX_HOME. Safety net for when the daemon's syncCleanup didn't run (SIGKILL).
|
|
@@ -588,7 +613,13 @@ async function handleUninstall() {
|
|
|
588
613
|
|
|
589
614
|
if (failures.length === 0) {
|
|
590
615
|
await runStep("opencodex config removed", () => {
|
|
591
|
-
|
|
616
|
+
const result = removeOwnedConfigState(getConfigDir());
|
|
617
|
+
if (result.status === "absent") return false;
|
|
618
|
+
if (result.status === "removed") return true;
|
|
619
|
+
const residual = result.residualPaths.length > 0
|
|
620
|
+
? ` Residual path(s): ${result.residualPaths.join(", ")}`
|
|
621
|
+
: "";
|
|
622
|
+
throw new Error(`${result.status} uninstall: ${result.reason ?? "config state was not removed"}.${residual}`);
|
|
592
623
|
});
|
|
593
624
|
} else {
|
|
594
625
|
console.error("Leaving opencodex config/backups in place so the failed restore step can be retried.");
|
|
@@ -716,19 +747,40 @@ switch (command) {
|
|
|
716
747
|
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
|
|
717
748
|
process.exit(1);
|
|
718
749
|
}
|
|
719
|
-
await syncModelsToCodex(live.port);
|
|
750
|
+
const synced = await syncModelsToCodex(live.port);
|
|
751
|
+
if (!synced.ok) {
|
|
752
|
+
process.exitCode = 1;
|
|
753
|
+
console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
720
756
|
const target = collectOrcaCodexHomeDiagnostic();
|
|
721
757
|
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
|
|
722
758
|
break;
|
|
723
759
|
}
|
|
724
|
-
|
|
725
|
-
|
|
760
|
+
let r: { success: boolean; message: string };
|
|
761
|
+
try {
|
|
762
|
+
r = restoreNativeCodex();
|
|
763
|
+
} catch (err) {
|
|
764
|
+
r = { success: false, message: err instanceof Error ? err.message : String(err) };
|
|
765
|
+
}
|
|
766
|
+
if (r.success) console.log(`✅ ${r.message}`);
|
|
767
|
+
else {
|
|
768
|
+
console.error(`⚠️ ${r.message}`);
|
|
769
|
+
process.exitCode = 1;
|
|
770
|
+
}
|
|
726
771
|
try {
|
|
727
772
|
const g = stripGrokConfig();
|
|
728
773
|
if (g.changed) console.log(`✅ ${g.message}`);
|
|
729
|
-
else if (!g.ok)
|
|
774
|
+
else if (!g.ok) {
|
|
775
|
+
console.error(`⚠️ ${g.message}`);
|
|
776
|
+
process.exitCode = 1;
|
|
777
|
+
}
|
|
730
778
|
} catch { /* best-effort */ }
|
|
731
|
-
|
|
779
|
+
if (r.success) {
|
|
780
|
+
console.log("Plain `codex` now runs natively (no proxy). Switch back with: ocx restore back");
|
|
781
|
+
} else {
|
|
782
|
+
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
783
|
+
}
|
|
732
784
|
break;
|
|
733
785
|
}
|
|
734
786
|
case "recover-history":
|
|
@@ -767,7 +819,20 @@ switch (command) {
|
|
|
767
819
|
break;
|
|
768
820
|
}
|
|
769
821
|
case "sync": {
|
|
770
|
-
|
|
822
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
823
|
+
const synced = await syncModelsToCodex((await findLiveProxy())?.port);
|
|
824
|
+
if (!synced.ok) {
|
|
825
|
+
process.exitCode = 1;
|
|
826
|
+
console.error("Codex sync did not complete. Fix the reported Codex config issue and retry.");
|
|
827
|
+
}
|
|
828
|
+
// Only warn/restart when a catalog or models_cache write actually happened. This is
|
|
829
|
+
// deliberately not an `else`: refreshCodexModelCatalog runs before injectCodexConfig,
|
|
830
|
+
// so a sync can fail (`ok: false`) after the catalog was already rewritten — which is
|
|
831
|
+
// exactly when a long-lived app-server is holding the stale list.
|
|
832
|
+
if (synced.catalogWritten || synced.cacheSynced) {
|
|
833
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
834
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
835
|
+
}
|
|
771
836
|
break;
|
|
772
837
|
}
|
|
773
838
|
case "v2": {
|
|
@@ -776,8 +841,13 @@ switch (command) {
|
|
|
776
841
|
break;
|
|
777
842
|
}
|
|
778
843
|
case "sync-cache": {
|
|
844
|
+
const restartCodex = args.slice(1).includes("--restart-codex");
|
|
779
845
|
const { invalidateCodexModelsCache } = await import("../codex/catalog");
|
|
780
|
-
|
|
846
|
+
// Only warn/restart when models_cache was actually rewritten from a readable catalog.
|
|
847
|
+
if (invalidateCodexModelsCache()) {
|
|
848
|
+
const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
|
|
849
|
+
afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
|
|
850
|
+
}
|
|
781
851
|
break;
|
|
782
852
|
}
|
|
783
853
|
case "gui": {
|
|
@@ -1005,6 +1075,10 @@ switch (command) {
|
|
|
1005
1075
|
break;
|
|
1006
1076
|
}
|
|
1007
1077
|
process.exit(await cmdClaude(args.slice(1)));
|
|
1078
|
+
}
|
|
1079
|
+
case "opencode": {
|
|
1080
|
+
const { cmdOpencode } = await import("./opencode");
|
|
1081
|
+
process.exit(await cmdOpencode(args.slice(1)));
|
|
1008
1082
|
}
|
|
1009
1083
|
case "help":
|
|
1010
1084
|
case "--help":
|