@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
package/src/cli/capabilities.ts
CHANGED
|
@@ -526,6 +526,45 @@ export const CAPABILITIES: readonly Capability[] = [
|
|
|
526
526
|
"Each client has its own route because a toggle rewrites that client's own config file.",
|
|
527
527
|
],
|
|
528
528
|
},
|
|
529
|
+
{
|
|
530
|
+
command: ["integration", "client"],
|
|
531
|
+
summary: "Inspect and toggle Aside profile catalogs, read their history, and restore a selected profile operation.",
|
|
532
|
+
routes: [
|
|
533
|
+
{ method: "GET", path: "/api/client-integrations/aside/profiles" },
|
|
534
|
+
{ method: "PUT", path: "/api/client-integrations/aside/profiles" },
|
|
535
|
+
{ method: "GET", path: "/api/client-integrations/aside/profiles/{profileId}" },
|
|
536
|
+
{ method: "PUT", path: "/api/client-integrations/aside/profiles/{profileId}" },
|
|
537
|
+
{ method: "GET", path: "/api/client-integrations/aside/profiles/journal" },
|
|
538
|
+
{ method: "GET", path: "/api/client-integrations/aside/profiles/{profileId}/journal" },
|
|
539
|
+
{ method: "POST", path: "/api/client-integrations/aside/profiles/{profileId}/restore" },
|
|
540
|
+
],
|
|
541
|
+
flags: [
|
|
542
|
+
{ name: "--client", value: "string", summary: "Select the file integration; use aside for profile controls." },
|
|
543
|
+
{ name: "--profile", value: "number", summary: "Select one registered Aside account; omitted toggles affect all profiles." },
|
|
544
|
+
{ name: "--op", value: "string", summary: "Operation ID for restore." },
|
|
545
|
+
{ name: "--confirm-drift", value: "boolean", summary: "Explicitly allow restore to replace subsequent edits." },
|
|
546
|
+
{ name: "--overwrite-conflict", value: "boolean", summary: "Explicitly allow enable to replace a conflicting provider block." },
|
|
547
|
+
{ name: "--json", value: "boolean", summary: "Emit the profile state, history, or mutation result as JSON." },
|
|
548
|
+
],
|
|
549
|
+
mutates: true,
|
|
550
|
+
json: "payload",
|
|
551
|
+
details: [
|
|
552
|
+
"Use status/show/list, history/journal, enable/disable, or restore after integration client.",
|
|
553
|
+
"These declarations cover the dedicated Aside profile paths; existing generic client routes retain their separate parity inventory.",
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
command: ["sync"],
|
|
558
|
+
summary: "Synchronize client catalogs, including Aside profiles through the running server's mutation owner.",
|
|
559
|
+
routes: [{ method: "POST", path: "/api/client-integrations/aside/sync" }],
|
|
560
|
+
flags: [
|
|
561
|
+
{ name: "--restart-codex", value: "boolean", summary: "Restart Codex app-servers after a catalog or cache write." },
|
|
562
|
+
{ name: "--restart-desktop-app", value: "boolean", summary: "Restart the Codex desktop app after a catalog or cache write." },
|
|
563
|
+
],
|
|
564
|
+
mutates: true,
|
|
565
|
+
json: "none",
|
|
566
|
+
details: ["The Aside refresh uses the live server; other catalog synchronization also performs local work."],
|
|
567
|
+
},
|
|
529
568
|
{
|
|
530
569
|
command: ["agent", "request-user-input"],
|
|
531
570
|
summary: "Show or set whether default mode may ask the operator a question mid-task.",
|
|
@@ -10,7 +10,7 @@ export interface ClaudeAgentStartupSyncDeps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Keep
|
|
13
|
+
* Keep readiness pending until the roster and optional Desktop registry have settled.
|
|
14
14
|
*
|
|
15
15
|
* The Codex sync remains the authority for ready versus failed. Claude roster repair is
|
|
16
16
|
* deliberately best-effort (#2200), but readiness must not become observable between the
|
|
@@ -22,6 +22,7 @@ export async function reconcileClientStartupBeforeReady<T>(
|
|
|
22
22
|
readinessGate: ReadinessGate,
|
|
23
23
|
syncCodex: (deferredGate: ReadinessGate) => Promise<T>,
|
|
24
24
|
syncClaudeRoster: () => Promise<unknown>,
|
|
25
|
+
syncDesktopRegistry?: () => Promise<unknown>,
|
|
25
26
|
): Promise<T> {
|
|
26
27
|
let codexReady = false;
|
|
27
28
|
const deferredGate: ReadinessGate = {
|
|
@@ -32,6 +33,7 @@ export async function reconcileClientStartupBeforeReady<T>(
|
|
|
32
33
|
|
|
33
34
|
const result = await syncCodex(deferredGate);
|
|
34
35
|
await syncClaudeRoster();
|
|
36
|
+
await syncDesktopRegistry?.();
|
|
35
37
|
if (codexReady) readinessGate.markReady();
|
|
36
38
|
return result;
|
|
37
39
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { loadConfig,
|
|
4
|
-
import { setIntegrationEnabled } from "../codex/desired-state";
|
|
3
|
+
import { loadConfig, mutatePersistedConfig, withConfigMutationLockSync } from "../config";
|
|
4
|
+
import { claudeDesktopIntegrationEnabledNow, setIntegrationEnabled } from "../codex/desired-state";
|
|
5
|
+
import { readClientConnectionState, assertClientConnectionUnchanged, assertNoClientDisconnectPending, type ClientConnectionState } from "../client/state";
|
|
6
|
+
import { downloadDesktop3pModels, HubClientError, normalizeHubOrigin } from "../client/hub-client";
|
|
7
|
+
import { readServiceApiTokenState } from "../lib/service-secrets";
|
|
8
|
+
import { applyRemoteDesktopStore, type DesktopStoreResult } from "../claude/desktop-remote-store";
|
|
9
|
+
import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
|
|
5
10
|
import {
|
|
6
11
|
DESKTOP_FAMILIES,
|
|
7
12
|
moveDesktopRoute,
|
|
@@ -11,6 +16,7 @@ import {
|
|
|
11
16
|
type DesktopProfile,
|
|
12
17
|
} from "../claude/desktop-profile";
|
|
13
18
|
import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p";
|
|
19
|
+
import { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } from "../claude/desktop-policy";
|
|
14
20
|
import { filterCatalogVisibleModels, desktopVisibleNativeSlugs, nativeContextLimits } from "../codex/catalog";
|
|
15
21
|
import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
|
|
16
22
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
@@ -33,6 +39,9 @@ function printDesktopHelp(): void {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export interface ApplyProfileDeps {
|
|
42
|
+
downloadDesktop3pModelsImpl?: typeof downloadDesktop3pModels;
|
|
43
|
+
applyRemoteDesktopStoreImpl?: typeof applyRemoteDesktopStore;
|
|
44
|
+
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
36
45
|
findLiveProxyImpl?: typeof findLiveProxy;
|
|
37
46
|
postApplyImpl?: (
|
|
38
47
|
mode: Desktop3pConfigMode,
|
|
@@ -41,20 +50,110 @@ export interface ApplyProfileDeps {
|
|
|
41
50
|
probeClaudeDesktopPolicy?: typeof import("../claude/desktop-policy").probeClaudeDesktopPolicy;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
/** Persist only the requested local profile, never an await-old whole configuration. */
|
|
54
|
+
function saveLocalDesktopProfile(
|
|
45
55
|
profile: DesktopProfile,
|
|
56
|
+
expectedProfile: DesktopProfile | undefined,
|
|
57
|
+
expectedConnection: ClientConnectionState,
|
|
58
|
+
deps: ApplyProfileDeps,
|
|
59
|
+
): void {
|
|
60
|
+
withClientLifecycleSync(() => {
|
|
61
|
+
const outcome = mutatePersistedConfig(current => {
|
|
62
|
+
assertNoClientDisconnectPending();
|
|
63
|
+
if (expectedConnection.kind === "connected") {
|
|
64
|
+
assertClientConnectionUnchanged(expectedConnection.value);
|
|
65
|
+
if (expectedConnection.value.pendingOperation) throw new Error("client_rotation_pending");
|
|
66
|
+
const token = readServiceApiTokenState();
|
|
67
|
+
if (token.kind !== "present" || token.fingerprint !== expectedConnection.value.tokenFingerprint) {
|
|
68
|
+
throw new Error("client_token_changed");
|
|
69
|
+
}
|
|
70
|
+
} else if (expectedConnection.kind !== "disconnected" || readClientConnectionState().kind !== "disconnected") {
|
|
71
|
+
throw new Error("client_connection_changed");
|
|
72
|
+
}
|
|
73
|
+
if (JSON.stringify(current.claudeCode?.desktopProfile) !== JSON.stringify(expectedProfile)) {
|
|
74
|
+
throw new Error("desktop_profile_changed");
|
|
75
|
+
}
|
|
76
|
+
const changed = JSON.stringify(current.claudeCode?.desktopProfile) !== JSON.stringify(profile);
|
|
77
|
+
if (changed) current.claudeCode = { ...(current.claudeCode ?? {}), desktopProfile: structuredClone(profile) };
|
|
78
|
+
return { changed, value: undefined };
|
|
79
|
+
});
|
|
80
|
+
if (outcome.status === "unavailable") throw new Error("desktop_profile_save_unavailable");
|
|
81
|
+
}, deps.lifecycleLockDeps);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function applyConnectedDesktopProfile(
|
|
85
|
+
mode: Desktop3pConfigMode,
|
|
86
|
+
connection: Extract<ClientConnectionState, { kind: "connected" }>,
|
|
87
|
+
deps: ApplyProfileDeps,
|
|
88
|
+
): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
|
|
89
|
+
try {
|
|
90
|
+
const token = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
|
|
91
|
+
assertClientConnectionUnchanged(connection.value);
|
|
92
|
+
if (connection.value.pendingOperation) throw new Error("client_rotation_pending");
|
|
93
|
+
const current = readServiceApiTokenState();
|
|
94
|
+
if (current.kind === "absent") throw new Error("client_token_absent");
|
|
95
|
+
if (current.kind === "unsafe") throw new Error("client_token_unsafe");
|
|
96
|
+
if (current.fingerprint !== connection.value.tokenFingerprint) {
|
|
97
|
+
throw new Error("client_token_mismatch");
|
|
98
|
+
}
|
|
99
|
+
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
100
|
+
if (!desired.ok) throw new Error("desktop_desired_state_write_failed");
|
|
101
|
+
return current;
|
|
102
|
+
}), deps.lifecycleLockDeps);
|
|
103
|
+
const baseUrl = normalizeHubOrigin(connection.value.serverUrl);
|
|
104
|
+
let snapshot: Awaited<ReturnType<typeof downloadDesktop3pModels>>;
|
|
105
|
+
try {
|
|
106
|
+
snapshot = await (deps.downloadDesktop3pModelsImpl ?? downloadDesktop3pModels)(baseUrl, token.token);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return { ok: false, path: "", reason: error instanceof HubClientError ? error.code : "desktop_download_failed" };
|
|
109
|
+
}
|
|
110
|
+
const result: DesktopStoreResult = withClientLifecycleSync(held => withConfigMutationLockSync(() => {
|
|
111
|
+
assertClientConnectionUnchanged(connection.value);
|
|
112
|
+
const currentToken = readServiceApiTokenState();
|
|
113
|
+
if (currentToken.kind !== "present" || currentToken.fingerprint !== connection.value.tokenFingerprint) {
|
|
114
|
+
throw new Error("client_connection_changed");
|
|
115
|
+
}
|
|
116
|
+
if (!claudeDesktopIntegrationEnabledNow()) throw new Error("desired_state_changed");
|
|
117
|
+
if (snapshot.models.length === 0) throw new Error("desktop_unavailable");
|
|
118
|
+
return (deps.applyRemoteDesktopStoreImpl ?? applyRemoteDesktopStore)(held, {
|
|
119
|
+
owner: { serverUrl: connection.value.serverUrl, apiKeyId: connection.value.apiKeyId, connectedAt: connection.value.connectedAt },
|
|
120
|
+
expectedTokenFingerprint: currentToken.fingerprint,
|
|
121
|
+
baseUrl, apiKey: currentToken.token, mode, models: snapshot.models,
|
|
122
|
+
});
|
|
123
|
+
}), deps.lifecycleLockDeps);
|
|
124
|
+
if (!result.ok) return { ok: false, path: "", reason: `desktop_lifecycle_${result.reason}` };
|
|
125
|
+
// Policy probes may spawn a process; perform them only after L/C have been released.
|
|
126
|
+
const policyWarning = claudeDesktopPolicyWarning((deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)());
|
|
127
|
+
const fallbackWarning = result.baselineKind === "standard_fallback"
|
|
128
|
+
? "Previous Desktop settings were not recorded. Disconnect will switch this managed profile to standard mode."
|
|
129
|
+
: undefined;
|
|
130
|
+
const warning = [fallbackWarning, policyWarning].filter(Boolean).join(" ");
|
|
131
|
+
return { ok: true, path: result.path ?? "", ...(warning ? { warning } : {}) };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : "";
|
|
134
|
+
return { ok: false, path: "", reason: /^[a-z][a-z0-9_]{1,100}$/.test(message) ? message : "desktop_apply_failed" };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function applyProfile(
|
|
139
|
+
profile: DesktopProfile | undefined,
|
|
46
140
|
mode: Desktop3pConfigMode,
|
|
47
141
|
deps: ApplyProfileDeps = {},
|
|
48
142
|
): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
|
|
143
|
+
try { assertNoClientDisconnectPending(); } catch { return { ok: false, path: "", reason: "client_disconnect_pending" }; }
|
|
144
|
+
const connection = readClientConnectionState();
|
|
145
|
+
if (connection.kind === "connected") return applyConnectedDesktopProfile(mode, connection, deps);
|
|
146
|
+
if (connection.kind !== "disconnected") return { ok: false, path: "", reason: "client_connection_invalid" };
|
|
49
147
|
// Explicit apply is an enable action. Persist intent before any Desktop write
|
|
50
148
|
// so a process crash cannot leave a gateway profile that startup immediately removes.
|
|
51
149
|
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
52
150
|
if (!desired.ok) return { ok: false, path: "", reason: desired.message };
|
|
53
151
|
const config = loadConfig();
|
|
54
152
|
const state = await buildClaudeDesktopState(config, profile);
|
|
55
|
-
|
|
56
|
-
saveConfigPreservingClaudeCode(config);
|
|
153
|
+
saveLocalDesktopProfile(state.profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
57
154
|
const live = await (deps.findLiveProxyImpl ?? findLiveProxy)();
|
|
155
|
+
assertNoClientDisconnectPending();
|
|
156
|
+
if (readClientConnectionState().kind !== "disconnected") throw new Error("client_connection_changed");
|
|
58
157
|
if (live) {
|
|
59
158
|
// #859: the Desktop alias reverse-map is process-local. Applying through the
|
|
60
159
|
// serving process installs the map there; a local-only write leaves the
|
|
@@ -86,7 +185,8 @@ export async function applyProfile(
|
|
|
86
185
|
// The toggle can persist OFF while fetchAllModels was awaiting (same race the
|
|
87
186
|
// management writers fence). Re-read persisted intent immediately before the
|
|
88
187
|
// writer; a lost race is a discriminated skip, not a write.
|
|
89
|
-
|
|
188
|
+
assertNoClientDisconnectPending();
|
|
189
|
+
if (readClientConnectionState().kind !== "disconnected") throw new Error("client_connection_changed");
|
|
90
190
|
if (!claudeDesktopIntegrationEnabledNow()) {
|
|
91
191
|
return { ok: false, path: "", reason: "desired_state_changed" };
|
|
92
192
|
}
|
|
@@ -103,8 +203,8 @@ export async function applyProfile(
|
|
|
103
203
|
mode,
|
|
104
204
|
state.profile,
|
|
105
205
|
nativeContextLimits(config),
|
|
206
|
+
deps.lifecycleLockDeps,
|
|
106
207
|
);
|
|
107
|
-
const { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } = await import("../claude/desktop-policy");
|
|
108
208
|
const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)();
|
|
109
209
|
const warning = result.written ? claudeDesktopPolicyWarning(policyState) : undefined;
|
|
110
210
|
return {
|
|
@@ -134,12 +234,9 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
134
234
|
const parsedMode = parseDesktop3pModeArgs(legacyFlags);
|
|
135
235
|
if ("error" in parsedMode) { console.error(parsedMode.error); return 2; }
|
|
136
236
|
try {
|
|
137
|
-
const
|
|
138
|
-
const state = await buildClaudeDesktopState(config);
|
|
139
|
-
const result = await applyProfile(state.profile, parsedMode.mode, deps);
|
|
237
|
+
const result = await applyProfile(undefined, parsedMode.mode, deps);
|
|
140
238
|
if (!result.ok) {
|
|
141
239
|
console.error(`설정 적용 실패: ${result.reason ?? "unknown error"}`);
|
|
142
|
-
console.error("프로필은 저장되었지만 Claude Desktop 설정 파일에는 반영되지 않았습니다. 프록시 상태를 확인한 뒤 다시 적용해 주세요.");
|
|
143
240
|
return 1;
|
|
144
241
|
}
|
|
145
242
|
console.log(`Claude Desktop 설정을 적용했습니다: ${result.path}`);
|
|
@@ -155,6 +252,17 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
try {
|
|
255
|
+
const connection = readClientConnectionState();
|
|
256
|
+
if (command === "import" && argv.includes("--apply")) assertNoClientDisconnectPending();
|
|
257
|
+
if (command === "import" && argv.includes("--apply") && connection.kind !== "disconnected") {
|
|
258
|
+
throw new CliUsageError(connection.kind === "connected"
|
|
259
|
+
? "Connected Desktop apply uses the hub profile. Import on the hub, then run ocx claude desktop apply here."
|
|
260
|
+
: "Client connection state is invalid; refusing import --apply.");
|
|
261
|
+
}
|
|
262
|
+
const localView = connection.kind === "connected";
|
|
263
|
+
if (localView && ["show", "export", "move", "default", "import"].includes(command ?? "")) {
|
|
264
|
+
console.warn("Local client profile only; connected Desktop apply uses the hub profile.");
|
|
265
|
+
}
|
|
158
266
|
const config = loadConfig();
|
|
159
267
|
// `status` is API-backed and must NOT build local state first: the whole point of the
|
|
160
268
|
// route the GUI polls (/api/claude-desktop/status) is the applied-vs-desired comparison,
|
|
@@ -178,7 +286,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
178
286
|
const rest = argv.slice(1);
|
|
179
287
|
const wantsJson = takeJsonFlag(rest);
|
|
180
288
|
if (rest.length > 0) throw new CliUsageError("Usage: ocx claude desktop show [--json]");
|
|
181
|
-
if (wantsJson) console.log(JSON.stringify(state));
|
|
289
|
+
if (wantsJson) console.log(JSON.stringify(localView ? { ...state, scope: "local" } : state));
|
|
182
290
|
else {
|
|
183
291
|
for (const family of DESKTOP_FAMILIES) {
|
|
184
292
|
console.log(`${family.toUpperCase()}${state.profile.defaults[family] ? ` (default: ${state.profile.defaults[family]})` : ""}`);
|
|
@@ -194,8 +302,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
194
302
|
if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new CliUsageError("Usage: ocx claude desktop move <route> <family> [--default]");
|
|
195
303
|
if (!state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
|
|
196
304
|
const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
|
|
197
|
-
|
|
198
|
-
saveConfigPreservingClaudeCode(config);
|
|
305
|
+
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
199
306
|
console.log(`${route} 모델을 ${familyRaw} 그룹으로 옮겼습니다.`);
|
|
200
307
|
return 0;
|
|
201
308
|
}
|
|
@@ -205,8 +312,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
205
312
|
const route = routeRaw === "none" ? null : routeRaw;
|
|
206
313
|
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
|
|
207
314
|
const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
|
|
208
|
-
|
|
209
|
-
saveConfigPreservingClaudeCode(config);
|
|
315
|
+
saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
|
|
210
316
|
console.log(`${familyRaw} 기본 모델을 ${route ?? "없음"}으로 지정했습니다.`);
|
|
211
317
|
return 0;
|
|
212
318
|
}
|
|
@@ -224,8 +330,11 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
|
|
|
224
330
|
if (!source || flags.some(flag => flag !== "--apply")) throw new CliUsageError("Usage: ocx claude desktop import <path> [--apply]");
|
|
225
331
|
const profile = parseDesktopProfile(JSON.parse(readFileSync(resolve(source), "utf8")));
|
|
226
332
|
const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
|
|
227
|
-
|
|
228
|
-
|
|
333
|
+
if (flags.includes("--apply")) assertNoClientDisconnectPending();
|
|
334
|
+
if (flags.includes("--apply") && readClientConnectionState().kind !== "disconnected") {
|
|
335
|
+
throw new CliUsageError("Client connection changed; refusing import --apply. Connected Desktop apply uses the hub profile.");
|
|
336
|
+
}
|
|
337
|
+
saveLocalDesktopProfile(reconciled, config.claudeCode?.desktopProfile, connection, deps);
|
|
229
338
|
if (flags.includes("--apply")) {
|
|
230
339
|
const result = await applyProfile(reconciled, "static", deps);
|
|
231
340
|
if (!result.ok) { console.error(`프로필은 저장했지만 Desktop 적용에 실패했습니다: ${result.reason ?? "unknown error"}`); return 1; }
|
package/src/cli/connect.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "../client/connect";
|
|
9
9
|
import { inspectClientRotationRecoveryGate, readClientConnectionState } from "../client/state";
|
|
10
10
|
import { readServiceApiTokenState } from "../lib/service-secrets";
|
|
11
|
+
import type { ClientLifecycleLockDeps } from "../client/lifecycle-lock";
|
|
12
|
+
import { inspectRemoteDesktopStore } from "../claude/desktop-remote-store";
|
|
11
13
|
import type { OcxConnectedClientId } from "../types";
|
|
12
14
|
import {
|
|
13
15
|
CliUsageError,
|
|
@@ -22,6 +24,10 @@ import {
|
|
|
22
24
|
type RuntimeApiDeps,
|
|
23
25
|
} from "./runtime-api";
|
|
24
26
|
|
|
27
|
+
export interface ClientCommandDeps extends RuntimeApiDeps {
|
|
28
|
+
lifecycleLockDeps?: ClientLifecycleLockDeps;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
export const CONNECT_USAGE = `Usage:
|
|
26
32
|
ocx connect <url> [--management-url <url>]
|
|
27
33
|
(--pairing-code-stdin | --admin-token-stdin)
|
|
@@ -52,10 +58,10 @@ export type ClientConnectionStatus = {
|
|
|
52
58
|
rotation: "clean" | "orphan-cleaned" | "recovery-required" | "unsafe";
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
export function collectClientConnectionStatus(now = Date.now()): ClientConnectionStatus {
|
|
61
|
+
export function collectClientConnectionStatus(now = Date.now(), lifecycleLockDeps?: ClientLifecycleLockDeps): ClientConnectionStatus {
|
|
56
62
|
const state = readClientConnectionState();
|
|
57
63
|
const tokenState = readServiceApiTokenState();
|
|
58
|
-
const rotation = inspectClientRotationRecoveryGate(state).kind;
|
|
64
|
+
const rotation = inspectClientRotationRecoveryGate(state, lifecycleLockDeps).kind;
|
|
59
65
|
let catalog: ClientConnectionStatus["catalog"] = "missing";
|
|
60
66
|
if (existsSync(DEFAULT_CATALOG_PATH)) {
|
|
61
67
|
try {
|
|
@@ -124,7 +130,7 @@ function statusLines(status: ClientConnectionStatus): string[] {
|
|
|
124
130
|
];
|
|
125
131
|
}
|
|
126
132
|
|
|
127
|
-
async function runRotate(argv: string[], deps:
|
|
133
|
+
async function runRotate(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
128
134
|
const args = [...argv];
|
|
129
135
|
const wantsJson = takeFlag(args, "--json");
|
|
130
136
|
const pairing = takeFlag(args, "--pairing-code-stdin");
|
|
@@ -136,13 +142,17 @@ async function runRotate(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
136
142
|
const value = new TextEncoder().encode(await readSecretLine(deps, pairing ? "pairing code" : "admin token"));
|
|
137
143
|
const connection = await rotateConnectedClientKey({
|
|
138
144
|
credential: { kind: pairing ? "pairing-grant" : "admin", value },
|
|
139
|
-
}, { fetchImpl: deps.fetchImpl });
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
}, { fetchImpl: deps.fetchImpl, lifecycleLockDeps: deps.lifecycleLockDeps });
|
|
146
|
+
const restartRequired = inspectRemoteDesktopStore({ serverUrl: connection.serverUrl, apiKeyId: connection.apiKeyId, connectedAt: connection.connectedAt }).kind !== "absent";
|
|
147
|
+
printData({ apiKeyId: connection.apiKeyId, rotation: connection.rotationOutcome, restartRequired }, wantsJson, [
|
|
148
|
+
connection.rotationOutcome === "committed"
|
|
149
|
+
? `Rotated connected API key ${connection.apiKeyId}; the previous key is no longer admitted.`
|
|
150
|
+
: `Rotation rolled back for API key ${connection.apiKeyId}; the previous key was retained or restored.`,
|
|
151
|
+
...(restartRequired ? ["Fully quit and reopen Claude Desktop; a running app may still hold the previous credential."] : []),
|
|
142
152
|
]);
|
|
143
153
|
}
|
|
144
154
|
|
|
145
|
-
async function runConnect(argv: string[], deps:
|
|
155
|
+
async function runConnect(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
146
156
|
const args = [...argv];
|
|
147
157
|
const serverUrl = args.shift();
|
|
148
158
|
if (!serverUrl || serverUrl.startsWith("--")) throw new CliUsageError("hub URL is required", CONNECT_USAGE);
|
|
@@ -173,28 +183,28 @@ async function runConnect(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
173
183
|
managementTransport,
|
|
174
184
|
noSync,
|
|
175
185
|
...(catalogTimeoutSeconds === undefined ? {} : { catalogTimeoutMs: catalogTimeoutSeconds * 1_000 }),
|
|
176
|
-
}, { fetchImpl: deps.fetchImpl });
|
|
186
|
+
}, { fetchImpl: deps.fetchImpl, lifecycleLockDeps: deps.lifecycleLockDeps });
|
|
177
187
|
console.log(`Connected to ${connection.serverUrl} as key ${connection.apiKeyId}.`);
|
|
178
188
|
}
|
|
179
189
|
|
|
180
|
-
async function runRevoke(argv: string[], deps:
|
|
190
|
+
async function runRevoke(argv: string[], deps: ClientCommandDeps): Promise<void> {
|
|
181
191
|
const args = [...argv];
|
|
182
192
|
const wantsJson = takeFlag(args, "--json");
|
|
183
193
|
const admin = takeFlag(args, "--admin-token-stdin");
|
|
184
194
|
if (!admin) throw new CliUsageError("revoke requires --admin-token-stdin", CONNECT_USAGE);
|
|
185
195
|
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
186
196
|
const value = new TextEncoder().encode(await readSecretLine(deps, "admin token"));
|
|
187
|
-
const result = await revokeConnectedClientKey({ kind: "admin", value }, { fetchImpl: deps.fetchImpl });
|
|
197
|
+
const result = await revokeConnectedClientKey({ kind: "admin", value }, { fetchImpl: deps.fetchImpl, lifecycleLockDeps: deps.lifecycleLockDeps });
|
|
188
198
|
printData(result, wantsJson, [`Revoked connected API key ${result.apiKeyId}. Disconnect this client next.`]);
|
|
189
199
|
}
|
|
190
200
|
|
|
191
|
-
export async function handleConnectCommand(argv: string[], deps:
|
|
201
|
+
export async function handleConnectCommand(argv: string[], deps: ClientCommandDeps = {}): Promise<number> {
|
|
192
202
|
return runCliAction(async () => {
|
|
193
203
|
if (argv[0] === "status") {
|
|
194
204
|
const args = argv.slice(1);
|
|
195
205
|
const wantsJson = takeFlag(args, "--json");
|
|
196
206
|
rejectArgs(args, CONNECT_USAGE, { redactValues: true });
|
|
197
|
-
const status = collectClientConnectionStatus();
|
|
207
|
+
const status = collectClientConnectionStatus(Date.now(), deps.lifecycleLockDeps);
|
|
198
208
|
printData(status, wantsJson, statusLines(status));
|
|
199
209
|
return;
|
|
200
210
|
}
|
|
@@ -210,13 +220,13 @@ export async function handleConnectCommand(argv: string[], deps: RuntimeApiDeps
|
|
|
210
220
|
});
|
|
211
221
|
}
|
|
212
222
|
|
|
213
|
-
export async function handleDisconnectCommand(argv: string[]): Promise<number> {
|
|
223
|
+
export async function handleDisconnectCommand(argv: string[], deps: Pick<ClientCommandDeps, "lifecycleLockDeps"> = {}): Promise<number> {
|
|
214
224
|
return runCliAction(async () => {
|
|
215
225
|
const args = [...argv];
|
|
216
226
|
const keepCatalog = takeFlag(args, "--keep-catalog");
|
|
217
227
|
const wantsJson = takeFlag(args, "--json");
|
|
218
228
|
rejectArgs(args, DISCONNECT_USAGE, { redactValues: true });
|
|
219
|
-
const result = await disconnectClient({ keepCatalog });
|
|
229
|
+
const result = await disconnectClient({ keepCatalog }, deps);
|
|
220
230
|
const payload = {
|
|
221
231
|
...result,
|
|
222
232
|
revoke: {
|
|
@@ -226,6 +236,9 @@ export async function handleDisconnectCommand(argv: string[]): Promise<number> {
|
|
|
226
236
|
};
|
|
227
237
|
printData(payload, wantsJson, [
|
|
228
238
|
"Disconnected locally; native Codex state was restored.",
|
|
239
|
+
...(result.desktopRestoration === "standard_fallback"
|
|
240
|
+
? ["Previous Desktop settings were not recorded; the managed profile was switched to standard mode."] : []),
|
|
241
|
+
...(result.restartRequired ? ["Fully quit and reopen Claude Desktop; local cleanup cannot revoke an in-memory credential."] : []),
|
|
229
242
|
`The hub key ${result.apiKeyId} is still valid. Revoke it from Integrations → API Keys.`,
|
|
230
243
|
]);
|
|
231
244
|
});
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { CliHead } from "./root";
|
|
|
14
14
|
import type { ReadyArgs } from "./ready";
|
|
15
15
|
import type { LivenessIo, LiveProxy } from "../server/proxy-liveness";
|
|
16
16
|
import type { OcxConfig } from "../types";
|
|
17
|
+
import type { OwnedIntegrationRefreshOutcome } from "../integrations/owned-refresh";
|
|
17
18
|
import { hasHelpFlag, printSubcommandUsage, printUsage } from "./help";
|
|
18
19
|
import { setIntegrationEnabled, shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
19
20
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -387,25 +388,38 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
387
388
|
if (restartDesktopApp) await handleDesktopAppRestart(console);
|
|
388
389
|
}
|
|
389
390
|
// `ocx sync` is a direct CLI path; it does not call the management
|
|
390
|
-
// `/api/sync` route. Refresh
|
|
391
|
+
// `/api/sync` route. Refresh already-connected file integrations here too,
|
|
391
392
|
// after Codex has published the catalog that supplies its capabilities.
|
|
392
|
-
if (synced.status !== "refused"
|
|
393
|
+
if (synced.status !== "refused") {
|
|
394
|
+
const results: OwnedIntegrationRefreshOutcome[] = [];
|
|
395
|
+
if (live) {
|
|
396
|
+
try {
|
|
397
|
+
const config = deps.loadConfig();
|
|
398
|
+
const { refreshOwnedCatalogIntegrations } = await import("../integrations/catalog-refresh");
|
|
399
|
+
results.push(...await refreshOwnedCatalogIntegrations({
|
|
400
|
+
models: async () => {
|
|
401
|
+
const { loadExportModels } = await import("../server/management/model-rows");
|
|
402
|
+
return loadExportModels(config);
|
|
403
|
+
},
|
|
404
|
+
config,
|
|
405
|
+
port: live.port,
|
|
406
|
+
}, ["mcode", "pi"]));
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.warn(`Client integrations were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Even without a live proxy, report why Aside could not sync. Its server
|
|
412
|
+
// owner is never bypassed, and another client's failure cannot hide it.
|
|
393
413
|
try {
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
const result = await refreshOwnedIntegration({
|
|
397
|
-
clientId: "mcode",
|
|
398
|
-
models: async () => {
|
|
399
|
-
const { loadExportModels } = await import("../server/management/model-rows");
|
|
400
|
-
return loadExportModels(config);
|
|
401
|
-
},
|
|
402
|
-
config,
|
|
403
|
-
port: live.port,
|
|
404
|
-
});
|
|
405
|
-
if (result?.changed) console.log("MCode integration refreshed from the current catalog.");
|
|
406
|
-
else if (result?.reason) console.warn(`MCode integration was not refreshed: ${result.reason}`);
|
|
414
|
+
const { refreshAsideProfilesThroughServer } = await import("./aside-profiles");
|
|
415
|
+
results.push(...await refreshAsideProfilesThroughServer({ findLiveProxy: async () => live }));
|
|
407
416
|
} catch (error) {
|
|
408
|
-
console.warn(`
|
|
417
|
+
console.warn(`Aside profiles were not refreshed: ${error instanceof Error ? error.message : String(error)}`);
|
|
418
|
+
}
|
|
419
|
+
for (const result of results) {
|
|
420
|
+
const label = result.profileId === undefined ? result.client : `${result.client}:${result.profileId}`;
|
|
421
|
+
if (result.changed) console.log(`${label} integration refreshed from the current catalog.`);
|
|
422
|
+
else if (result.reason) console.warn(`${label} integration was not refreshed: ${result.reason}${result.residual ? " Recovery did not finish." : ""}${result.snapshotPath ? ` Backup: ${result.snapshotPath}` : ""}`);
|
|
409
423
|
}
|
|
410
424
|
}
|
|
411
425
|
return code;
|
|
@@ -171,12 +171,14 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
|
|
|
171
171
|
rejectArgs(args, USAGE);
|
|
172
172
|
|
|
173
173
|
const spec = EXPORT_CLIENTS[client];
|
|
174
|
-
const config = (deps.configImpl ?? loadConfig)();
|
|
175
174
|
const root = await runtimeBaseUrl(deps);
|
|
176
175
|
const rows = await runtimeRequest<ExportProxyModelRow[]>("/api/models", {}, { ...deps, baseUrl: root });
|
|
177
176
|
if (!Array.isArray(rows)) {
|
|
178
177
|
throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
|
|
179
178
|
}
|
|
179
|
+
// Discovery can persist pending -> ready selection. Read from the caller's
|
|
180
|
+
// config source after the response, rather than filtering with a stale snapshot.
|
|
181
|
+
const config = (deps.configImpl ?? loadConfig)();
|
|
180
182
|
const models = exportModelsFromProxyRows(rows, config);
|
|
181
183
|
// The text is the client's OWN format — YAML, TOML and JSON5 clients would
|
|
182
184
|
// otherwise receive a JSON rendering their parser reads differently.
|
package/src/cli/index.ts
CHANGED
|
@@ -106,7 +106,7 @@ function reportShellHookFailure(result: { state: "installed" | "absent" | "faile
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
import {
|
|
109
|
+
import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
|
|
110
110
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
111
111
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
112
112
|
import { initializeNodeLauncherContext } from "./launcher-context";
|
|
@@ -459,8 +459,8 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
459
459
|
reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
460
460
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
461
461
|
// Codex sync owns the ready/failed verdict, but its successful transition is
|
|
462
|
-
// deferred until the best-effort Claude roster
|
|
463
|
-
// keeps /readyz closed across
|
|
462
|
+
// deferred until the best-effort Claude roster and Desktop registry settle. This
|
|
463
|
+
// keeps /readyz closed across startup initialization without making an optional
|
|
464
464
|
// Claude integration failure prevent the proxy from starting.
|
|
465
465
|
const startupSync = await reconcileClientStartupBeforeReady(
|
|
466
466
|
readinessGate,
|
|
@@ -468,6 +468,29 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
468
468
|
() => systemEnv.injected
|
|
469
469
|
? Promise.resolve(null)
|
|
470
470
|
: syncClaudeAgentDefsAtProxyStartup(config, port),
|
|
471
|
+
async () => {
|
|
472
|
+
try {
|
|
473
|
+
const { fetchAllModels } = await import("../server/management-api");
|
|
474
|
+
const { desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
475
|
+
const { resolveCodexModelEntitlements } = await import("../codex/model-entitlements");
|
|
476
|
+
const { buildDesktopDiscoveryInputs } = await import("../claude/desktop-discovery-inputs");
|
|
477
|
+
const [models, modelEntitlements] = await Promise.all([
|
|
478
|
+
fetchAllModels(config),
|
|
479
|
+
resolveCodexModelEntitlements(config, { clientVersion: null }),
|
|
480
|
+
]);
|
|
481
|
+
const inputs = buildDesktopDiscoveryInputs({
|
|
482
|
+
config, models, modelEntitlements,
|
|
483
|
+
desktopNativeCandidates: desktopVisibleNativeSlugs(config),
|
|
484
|
+
});
|
|
485
|
+
buildDesktop3pRegistry(
|
|
486
|
+
inputs.nativeSlugs, inputs.routedModels,
|
|
487
|
+
config.claudeCode?.desktopProfile, inputs.nativeContextCap,
|
|
488
|
+
);
|
|
489
|
+
} catch {
|
|
490
|
+
// Best-effort; model discovery can rebuild it. Never reflect credential or provider errors.
|
|
491
|
+
console.warn("[opencodex] Claude Desktop model registry could not be initialized at startup.");
|
|
492
|
+
}
|
|
493
|
+
},
|
|
471
494
|
);
|
|
472
495
|
if (!startupSync.ran) console.log(" Codex integration OFF; startup left Codex native.");
|
|
473
496
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
@@ -482,17 +505,6 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
482
505
|
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
483
506
|
historyGuardian = startHistoryMigrationGuardian();
|
|
484
507
|
}
|
|
485
|
-
// Build Desktop 3P alias registry so inbound claude-opus-4-8-{code} aliases (and legacy claude-opus-4-{code}) decode correctly.
|
|
486
|
-
try {
|
|
487
|
-
const { fetchAllModels } = await import("../server/management-api");
|
|
488
|
-
const { visibleNativeSlugs, filterCatalogVisibleModels } = await import("../codex/catalog");
|
|
489
|
-
const models = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
490
|
-
buildDesktop3pRegistry(
|
|
491
|
-
[...visibleNativeSlugs(config)],
|
|
492
|
-
models.map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow })),
|
|
493
|
-
config.claudeCode?.desktopProfile,
|
|
494
|
-
);
|
|
495
|
-
} catch { /* best-effort — registry rebuilds on first /v1/models call */ }
|
|
496
508
|
// Grok Build auto-registration: additive fenced block in ~/.grok/config.toml so an installed
|
|
497
509
|
// grok CLI can pick opencodex-routed models without manual config. No-op when ~/.grok is
|
|
498
510
|
// absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
|
|
@@ -1268,8 +1280,8 @@ async function handleUninstall() {
|
|
|
1268
1280
|
}
|
|
1269
1281
|
|
|
1270
1282
|
if (failures.length === 0) {
|
|
1271
|
-
await runStep("opencodex config removed", () => {
|
|
1272
|
-
const result =
|
|
1283
|
+
await runStep("opencodex config removed", async () => {
|
|
1284
|
+
const result = await removeOwnedConfigAfterDesktopCleanup(observed);
|
|
1273
1285
|
if (result.status === "absent") return false;
|
|
1274
1286
|
if (result.status === "removed") return true;
|
|
1275
1287
|
const residual = result.residualPaths.length > 0
|