@bitkyc08/opencodex 2.64.0 → 2.65.0-preview.20260925
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 +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-CsYvvpr3.js +50 -0
- package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
- package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-CpuDF3ci.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
|
@@ -214,6 +214,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
214
214
|
const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
215
215
|
const admitted = loadConfig();
|
|
216
216
|
if (!claudeDesktopIntegrationEnabled(admitted)) return;
|
|
217
|
+
// A first-party Desktop must never get a gateway profile written and selected behind it.
|
|
218
|
+
const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
219
|
+
if (resolveClaudeDesktopMode(admitted, observeClaudeDesktopMode(admitted)) === "first-party") return;
|
|
217
220
|
if (admitted.claudeCode?.desktopAutoApply === false) return;
|
|
218
221
|
if (!admitted.claudeCode?.desktopProfile) return;
|
|
219
222
|
const { inspectDesktop3pConfigLibrary, writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
@@ -223,32 +226,38 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
223
226
|
if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(beforeKind)) return;
|
|
224
227
|
const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
225
228
|
const allModels = await (deps.fetchAllModels ?? fetchAllModels)(admitted);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
229
|
+
// Serialized with Desktop mode transitions: a first-party switch cannot interleave with this write.
|
|
230
|
+
const { runPickerTransition } = await import("./claude-desktop-picker-routes");
|
|
231
|
+
await runPickerTransition(admitted, async () => {
|
|
232
|
+
const current = loadConfig();
|
|
233
|
+
// This is the real guard: the catalog await admits a concurrent explicit OFF.
|
|
234
|
+
if (!claudeDesktopIntegrationEnabled(current)) return undefined;
|
|
235
|
+
// …and a concurrent switch to first-party.
|
|
236
|
+
if (resolveClaudeDesktopMode(current, observeClaudeDesktopMode(current)) === "first-party") return undefined;
|
|
237
|
+
if (current.claudeCode?.desktopAutoApply === false || !current.claudeCode?.desktopProfile) return undefined;
|
|
238
|
+
const afterKind = inspectDesktop3pConfigLibrary({
|
|
239
|
+
appliedFingerprint: current.claudeCode.desktopProfile.appliedFingerprint ?? null,
|
|
240
|
+
}).kind;
|
|
241
|
+
if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return undefined;
|
|
242
|
+
const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
|
|
243
|
+
const writtenProfile = current.claudeCode.desktopProfile;
|
|
244
|
+
const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
|
|
245
|
+
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
246
|
+
current.port ?? 10100,
|
|
247
|
+
[...desktopVisibleNativeSlugs(current)],
|
|
248
|
+
routed,
|
|
249
|
+
current.apiKeys?.[0]?.key,
|
|
250
|
+
"static",
|
|
251
|
+
writtenProfile,
|
|
252
|
+
nativeContextLimits(current),
|
|
253
|
+
);
|
|
254
|
+
if (result.written && result.fingerprint) {
|
|
255
|
+
const marked = commitDesktopAppliedMarker(markerBaseline, result.fingerprint);
|
|
256
|
+
if (marked.status === "unavailable" || marked.value === false) {
|
|
257
|
+
console.warn("[claude-desktop] provider-change applied marker skipped");
|
|
258
|
+
}
|
|
250
259
|
}
|
|
251
|
-
}
|
|
260
|
+
});
|
|
252
261
|
} catch { /* best-effort */ }
|
|
253
262
|
}
|
|
254
263
|
|
|
@@ -1068,9 +1077,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1068
1077
|
rethrowManagementBodyTooLarge(error);
|
|
1069
1078
|
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
1070
1079
|
}
|
|
1071
|
-
const { resolveClaudeDesktopApplyMode, applyDesktopFirstParty, captureDesktopFirstPartyRollback, removeDesktopFirstParty } = await import("../../claude/desktop-first-party");
|
|
1080
|
+
const { resolveClaudeDesktopApplyMode, observeClaudeDesktopMode, applyDesktopFirstParty, captureDesktopFirstPartyRollback, removeDesktopFirstParty } = await import("../../claude/desktop-first-party");
|
|
1072
1081
|
const requested = (parsed as { mode?: unknown } | null)?.mode;
|
|
1073
|
-
let desktopMode: "first-party" | "gateway" = resolveClaudeDesktopApplyMode(config);
|
|
1082
|
+
let desktopMode: "first-party" | "gateway" = resolveClaudeDesktopApplyMode(config, observeClaudeDesktopMode(config));
|
|
1074
1083
|
if (requested !== undefined) {
|
|
1075
1084
|
if (requested === "static" || requested === "hybrid" || requested === "discovery") {
|
|
1076
1085
|
mode = requested;
|
|
@@ -1099,53 +1108,64 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1099
1108
|
const desired = setIntegrationEnabled("claude-desktop", true);
|
|
1100
1109
|
if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
|
|
1101
1110
|
mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
|
|
1102
|
-
const {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
reason: applied.reason,
|
|
1111
|
-
path: applied.path,
|
|
1112
|
-
}, applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? 409 : 500);
|
|
1113
|
-
}
|
|
1114
|
-
// Remove the gateway only after the replacement env was written.
|
|
1115
|
-
const appliedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1116
|
-
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
|
|
1117
|
-
let gatewayRemoved = false;
|
|
1118
|
-
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
|
|
1119
|
-
const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true });
|
|
1120
|
-
if (!removed.ok) {
|
|
1121
|
-
const restored = removed.changed || !applied.changed || rollback();
|
|
1122
|
-
const modeSaved = !removed.changed || (await persistDesktopModeField(config, "first-party")).ok;
|
|
1123
|
-
const warning = [restored ? "" : "first-party settings rollback did not complete",
|
|
1124
|
-
modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
|
|
1111
|
+
const { pickerPreferenceOn, pickerStatusFor, runPickerTransition } = await import("./claude-desktop-picker-routes");
|
|
1112
|
+
// The whole switch runs under the picker lock, in today's order: env first, then gateway cleanup.
|
|
1113
|
+
return await runPickerTransition(config, async ops => {
|
|
1114
|
+
const { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } = await import("../../claude/desktop-3p");
|
|
1115
|
+
const rollback = captureDesktopFirstPartyRollback(config);
|
|
1116
|
+
const applied = applyDesktopFirstParty(config);
|
|
1117
|
+
if (!applied.ok) {
|
|
1118
|
+
const { firstPartyRefusalMessage } = await import("./native-integration-routes");
|
|
1125
1119
|
return jsonResponse({
|
|
1126
|
-
error:
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
reason: removed.reason ?? removed.kind,
|
|
1132
|
-
...(removed.residualPaths ? { residualPaths: removed.residualPaths } : {}),
|
|
1133
|
-
}, removed.kind === "cleanup_incomplete" ? 500 : 409);
|
|
1120
|
+
error: firstPartyRefusalMessage(applied.reason, applied.path),
|
|
1121
|
+
code: "claude_desktop_first_party_refused",
|
|
1122
|
+
reason: applied.reason,
|
|
1123
|
+
path: applied.path,
|
|
1124
|
+
}, applied.reason === "foreign_env" || applied.reason === "intercept_disabled" ? 409 : 500);
|
|
1134
1125
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1126
|
+
// Remove the gateway only after the replacement env was written.
|
|
1127
|
+
const appliedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1128
|
+
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
|
|
1129
|
+
let gatewayRemoved = false;
|
|
1130
|
+
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
|
|
1131
|
+
const removed = (deps.removeDesktop3pStandardPivot ?? removeDesktop3pStandardPivot)({ appliedFingerprint, replaceWhileEnabled: true });
|
|
1132
|
+
if (!removed.ok) {
|
|
1133
|
+
const restored = removed.changed || !applied.changed || rollback();
|
|
1134
|
+
const modeSaved = !removed.changed || (await persistDesktopModeField(config, "first-party")).ok;
|
|
1135
|
+
const warning = [restored ? "" : "first-party settings rollback did not complete",
|
|
1136
|
+
modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
|
|
1137
|
+
return jsonResponse({
|
|
1138
|
+
error: removed.kind === "cleanup_incomplete"
|
|
1139
|
+
? "Claude Desktop now points at standard mode, but gateway credential cleanup is incomplete; the first-party connection remains active."
|
|
1140
|
+
: "The gateway profile could not be removed safely, so first-party mode was not applied.",
|
|
1141
|
+
code: "claude_desktop_gateway_removal_failed",
|
|
1142
|
+
...(warning ? { warning } : {}),
|
|
1143
|
+
reason: removed.reason ?? removed.kind,
|
|
1144
|
+
...(removed.residualPaths ? { residualPaths: removed.residualPaths } : {}),
|
|
1145
|
+
}, removed.kind === "cleanup_incomplete" ? 500 : 409);
|
|
1146
|
+
}
|
|
1147
|
+
gatewayRemoved = removed.changed;
|
|
1148
|
+
}
|
|
1149
|
+
const modeSaved = await persistDesktopModeField(config, "first-party");
|
|
1150
|
+
const { firstPartyAccountRisk } = await import("../../claude/desktop-risk");
|
|
1151
|
+
// Picker mode is on by default in first-party; only a committed mode turns it on.
|
|
1152
|
+
const picker = modeSaved.ok && pickerPreferenceOn(loadConfig())
|
|
1153
|
+
? await ops.enableLocked({ persist: false, context: "server" })
|
|
1154
|
+
: await pickerStatusFor(loadConfig());
|
|
1155
|
+
return jsonResponse({
|
|
1156
|
+
ok: true,
|
|
1157
|
+
mode: "first-party",
|
|
1158
|
+
saved: modeSaved.ok,
|
|
1159
|
+
applied: true,
|
|
1160
|
+
changed: applied.changed || gatewayRemoved,
|
|
1161
|
+
gatewayRemoved,
|
|
1162
|
+
path: applied.path,
|
|
1163
|
+
proxyPort: applied.proxyPort,
|
|
1164
|
+
caCertPath: applied.env.NODE_EXTRA_CA_CERTS,
|
|
1165
|
+
riskWarning: firstPartyAccountRisk(),
|
|
1166
|
+
picker,
|
|
1167
|
+
...(modeSaved.ok ? {} : { warning: `First-party env was applied, but the mode marker was not saved (${modeSaved.reason}).` }),
|
|
1168
|
+
});
|
|
1149
1169
|
});
|
|
1150
1170
|
}
|
|
1151
1171
|
const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
@@ -1176,47 +1196,58 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1176
1196
|
const slash = model.route.indexOf("/");
|
|
1177
1197
|
return { provider: model.route.slice(0, slash), id: model.route.slice(slash + 1), contextWindow: model.contextWindow };
|
|
1178
1198
|
});
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1199
|
+
const { runPickerTransition } = await import("./claude-desktop-picker-routes");
|
|
1200
|
+
const outcome = await runPickerTransition(config, async ops => {
|
|
1201
|
+
// State construction can await catalog work; never write from the stale
|
|
1202
|
+
// config captured before that await if another request turned Desktop off.
|
|
1203
|
+
const latest = loadConfig();
|
|
1204
|
+
if (!claudeDesktopIntegrationEnabled(latest)) {
|
|
1205
|
+
return { response: jsonResponse({
|
|
1206
|
+
error: "Claude Desktop apply was cancelled because the desired state changed to off.",
|
|
1207
|
+
code: "claude_desktop_apply_skipped",
|
|
1208
|
+
reason: "desired_state_changed",
|
|
1209
|
+
desiredEnabled: false,
|
|
1210
|
+
saved: true,
|
|
1211
|
+
applied: false,
|
|
1212
|
+
}, 409) };
|
|
1213
|
+
}
|
|
1214
|
+
// Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
|
|
1215
|
+
const pickerOff = await ops.disableLocked({ persist: false });
|
|
1216
|
+
const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
|
|
1217
|
+
Number(url.port) || latest.port,
|
|
1218
|
+
[...desktopVisibleNativeSlugs(latest)],
|
|
1219
|
+
routed,
|
|
1220
|
+
latest.apiKeys?.[0]?.key,
|
|
1221
|
+
mode,
|
|
1222
|
+
state.profile,
|
|
1223
|
+
nativeContextLimits(latest),
|
|
1224
|
+
);
|
|
1225
|
+
if (!result.written) return { response: jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500) };
|
|
1226
|
+
const committed = persistCommittedDesktopGateway(config, state.profile, result.fingerprint);
|
|
1227
|
+
const modeWarning = committed.ok ? undefined : `Gateway applied, but its mode/profile state was not saved (${committed.reason}).`;
|
|
1228
|
+
// The durable marker describes the committed gateway even if old-mode cleanup fails.
|
|
1229
|
+
const firstPartyRemoved = removeDesktopFirstParty();
|
|
1230
|
+
if (!firstPartyRemoved.ok) {
|
|
1231
|
+
return { response: jsonResponse({
|
|
1232
|
+
error: `Claude Code settings could not be parsed (${firstPartyRemoved.path}); first-party cleanup remains incomplete after gateway apply.`,
|
|
1233
|
+
code: "claude_desktop_first_party_refused", reason: firstPartyRemoved.reason,
|
|
1234
|
+
applied: true, saved: committed.ok, mode: "gateway", path: result.path,
|
|
1235
|
+
...(modeWarning ? { warning: modeWarning } : {}),
|
|
1236
|
+
}, 500) };
|
|
1237
|
+
}
|
|
1238
|
+
return { result, committed, modeWarning, pickerOff };
|
|
1239
|
+
});
|
|
1240
|
+
if (outcome.response) return outcome.response;
|
|
1241
|
+
const { result, committed, modeWarning, pickerOff } = outcome as Exclude<typeof outcome, { response: Response }>;
|
|
1214
1242
|
const { claudeDesktopPolicyWarning, getCachedClaudeDesktopPolicy } = await import("../../claude/desktop-policy");
|
|
1215
1243
|
const policyState = deps.probeClaudeDesktopPolicy
|
|
1216
1244
|
? await deps.probeClaudeDesktopPolicy({ platform: deps.platform ?? process.platform })
|
|
1217
1245
|
: await getCachedClaudeDesktopPolicy({ platform: deps.platform ?? process.platform });
|
|
1218
1246
|
const policyWarning = claudeDesktopPolicyWarning(policyState);
|
|
1219
|
-
const
|
|
1247
|
+
const pickerWarning = pickerOff.residual?.length
|
|
1248
|
+
? `Picker mode cleanup is incomplete (${pickerOff.residual.join(", ")}); run ocx claude desktop picker off.`
|
|
1249
|
+
: undefined;
|
|
1250
|
+
const warning = [modeWarning, policyWarning, pickerWarning].filter(Boolean).join(" ");
|
|
1220
1251
|
return jsonResponse({
|
|
1221
1252
|
ok: true,
|
|
1222
1253
|
mode: "gateway",
|
|
@@ -1238,15 +1269,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1238
1269
|
try {
|
|
1239
1270
|
const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
|
|
1240
1271
|
const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
|
|
1241
|
-
const { resolveClaudeDesktopApplyMode, inspectDesktopFirstParty } = await import("../../claude/desktop-first-party");
|
|
1272
|
+
const { resolveClaudeDesktopApplyMode, inspectDesktopFirstParty, observeClaudeDesktopMode } = await import("../../claude/desktop-first-party");
|
|
1242
1273
|
const { getClaudeInterceptState } = await import("../../claude/intercept/runtime");
|
|
1274
|
+
const { DESKTOP_PICKER_ID_SUGGESTIONS, readInterceptBindings } = await import("../../claude/intercept/model-bindings");
|
|
1243
1275
|
const persisted = loadConfig();
|
|
1244
1276
|
const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
|
|
1245
1277
|
const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
|
|
1246
1278
|
const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
|
|
1247
1279
|
const gatewayApplied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
|
|
1248
1280
|
// A gateway profile on disk is what Desktop actually runs, whatever the saved mode says.
|
|
1249
|
-
const mode = gatewayApplied ? "gateway" : resolveClaudeDesktopApplyMode(persisted);
|
|
1281
|
+
const mode = gatewayApplied ? "gateway" : resolveClaudeDesktopApplyMode(persisted, observeClaudeDesktopMode(persisted));
|
|
1250
1282
|
const firstPartySeen = inspectDesktopFirstParty(persisted);
|
|
1251
1283
|
const intercept = getClaudeInterceptState();
|
|
1252
1284
|
const firstParty = {
|
|
@@ -1257,6 +1289,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1257
1289
|
interceptRunning: intercept !== null,
|
|
1258
1290
|
proxyPort: intercept?.proxyPort ?? firstPartySeen.proxyPort,
|
|
1259
1291
|
caCertPath: firstPartySeen.caCertPath,
|
|
1292
|
+
// What the running proxy routes with right now (live config), not the file on disk.
|
|
1293
|
+
modelBindings: readInterceptBindings(config.claudeCode),
|
|
1294
|
+
pickerSuggestions: [...DESKTOP_PICKER_ID_SUGGESTIONS],
|
|
1295
|
+
// Picker mode: the controller's view while it runs, else the static offline status.
|
|
1296
|
+
picker: await (await import("./claude-desktop-picker-routes")).pickerStatusFor(persisted),
|
|
1260
1297
|
};
|
|
1261
1298
|
const applied = mode === "first-party" ? firstPartySeen.applied : gatewayApplied;
|
|
1262
1299
|
// "Needs update" is only meaningful while the integration is wanted. When the
|
|
@@ -1290,6 +1327,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1290
1327
|
return jsonResponse({
|
|
1291
1328
|
desiredEnabled,
|
|
1292
1329
|
mode,
|
|
1330
|
+
// Shown wherever first-party is selected or its env is still applied (account-risk notice).
|
|
1331
|
+
riskWarning: mode === "first-party" || firstPartySeen.applied || firstPartySeen.stale
|
|
1332
|
+
? (await import("../../claude/desktop-risk")).firstPartyAccountRisk()
|
|
1333
|
+
: null,
|
|
1293
1334
|
firstParty,
|
|
1294
1335
|
installed: observed.kind !== "not_installed",
|
|
1295
1336
|
observedKind: observed.kind,
|
|
@@ -1312,6 +1353,53 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1312
1353
|
}
|
|
1313
1354
|
}
|
|
1314
1355
|
|
|
1356
|
+
// First-party model bindings: Claude Desktop Code tab picker id -> opencodex route, honoured
|
|
1357
|
+
// only on the claude-intercept ingress (src/claude/intercept/model-bindings.ts).
|
|
1358
|
+
if (url.pathname === "/api/claude-desktop/first-party-bindings" && req.method === "PUT") {
|
|
1359
|
+
let body: unknown;
|
|
1360
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1361
|
+
try {
|
|
1362
|
+
const { applyInterceptBindingPatch, parseInterceptBindingPatch, readInterceptBindings } = await import("../../claude/intercept/model-bindings");
|
|
1363
|
+
const patch = parseInterceptBindingPatch(body);
|
|
1364
|
+
if ("error" in patch) return jsonResponse({ error: patch.error }, 400);
|
|
1365
|
+
// Same route vocabulary the Desktop profile and the dashboard use, native routes included.
|
|
1366
|
+
const state = await buildClaudeDesktopState(config);
|
|
1367
|
+
const availableRoutes = new Set(state.models.filter(model => model.available).map(model => model.route));
|
|
1368
|
+
let rejection: string | null = null;
|
|
1369
|
+
const outcome = mutatePersistedConfig<OcxConfig["claudeCode"]>(persisted => {
|
|
1370
|
+
const current = readInterceptBindings(persisted.claudeCode);
|
|
1371
|
+
const next = applyInterceptBindingPatch(current, patch, availableRoutes);
|
|
1372
|
+
if (!next.ok) {
|
|
1373
|
+
rejection = next.error;
|
|
1374
|
+
return { changed: false, value: persisted.claudeCode };
|
|
1375
|
+
}
|
|
1376
|
+
if (!next.changed) return { changed: false, value: structuredClone(persisted.claudeCode) };
|
|
1377
|
+
const claudeCode = { ...(persisted.claudeCode ?? {}) };
|
|
1378
|
+
const intercept = { ...(claudeCode.intercept ?? {}) };
|
|
1379
|
+
if (Object.keys(next.bindings).length > 0) intercept.modelMap = next.bindings;
|
|
1380
|
+
else delete intercept.modelMap;
|
|
1381
|
+
if (Object.keys(intercept).length > 0) claudeCode.intercept = intercept;
|
|
1382
|
+
else delete claudeCode.intercept;
|
|
1383
|
+
persisted.claudeCode = claudeCode;
|
|
1384
|
+
return { changed: true, value: structuredClone(persisted.claudeCode) };
|
|
1385
|
+
});
|
|
1386
|
+
if (rejection) return jsonResponse({ error: rejection }, 400);
|
|
1387
|
+
if (outcome.status === "unavailable") {
|
|
1388
|
+
return jsonResponse({ error: `First-party bindings could not be saved (config ${outcome.reason})` }, outcome.reason === "conflict" ? 409 : 500);
|
|
1389
|
+
}
|
|
1390
|
+
adoptPersistedClaudeCode(config, outcome.value);
|
|
1391
|
+
// Pin the committed leaf: an unarmed live snapshot may otherwise keep its old intercept block.
|
|
1392
|
+
const committedIntercept = outcome.value?.intercept;
|
|
1393
|
+
const liveClaudeCode = { ...(config.claudeCode ?? {}) };
|
|
1394
|
+
if (committedIntercept) liveClaudeCode.intercept = structuredClone(committedIntercept);
|
|
1395
|
+
else delete liveClaudeCode.intercept;
|
|
1396
|
+
config.claudeCode = liveClaudeCode;
|
|
1397
|
+
return jsonResponse({ ok: true, modelBindings: readInterceptBindings(config.claudeCode) });
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1315
1403
|
// Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
|
|
1316
1404
|
if (url.pathname === "/api/claude-code" && req.method === "GET") {
|
|
1317
1405
|
const models = await fetchAllModels(config);
|
|
@@ -1666,8 +1754,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1666
1754
|
const warnings: string[] = [];
|
|
1667
1755
|
// authMode changes must reconcile the injected system env too: switching back to
|
|
1668
1756
|
// Subscription has to remove the opencodex-owned dummy ANTHROPIC_AUTH_TOKEN
|
|
1669
|
-
// (audit R1 blocker #1/#2, devlog 260720_claude_authmode_persist).
|
|
1670
|
-
|
|
1757
|
+
// (audit R1 blocker #1/#2, devlog 260720_claude_authmode_persist). Model slots and
|
|
1758
|
+
// levers feed the same injection, so a changed or cleared slot must not linger in
|
|
1759
|
+
// launchd until the next restart.
|
|
1760
|
+
const systemEnvInputs = ["systemEnv", "authMode", "model", "smallFastModel", "tierModels",
|
|
1761
|
+
"maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow"] as const;
|
|
1762
|
+
if (systemEnvInputs.some(field => body[field] !== undefined)) {
|
|
1671
1763
|
try {
|
|
1672
1764
|
await applySystemEnvToggle(config, config.port);
|
|
1673
1765
|
} catch (err) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Desktop picker mode over the management API, and the transition helpers every Desktop
|
|
3
|
+
* mode change uses.
|
|
4
|
+
*
|
|
5
|
+
* While this server runs a picker controller (src/claude/desktop-picker.ts), every picker
|
|
6
|
+
* mutation goes through it, serialized by its lock. With no controller (intercept disabled, client
|
|
7
|
+
* role, a failed bind), `runPickerTransition` runs the same code with offline ops that only remove
|
|
8
|
+
* leftover picker artifacts, and enabling reports `proxy_unavailable`.
|
|
9
|
+
*/
|
|
10
|
+
import type { OcxConfig } from "../../types";
|
|
11
|
+
import type { DesktopPickerController, DesktopPickerOps, DesktopPickerStatus } from "../../claude/desktop-picker";
|
|
12
|
+
import { loadConfig } from "../../config";
|
|
13
|
+
import { jsonResponse } from "../auth-cors";
|
|
14
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
15
|
+
import type { ManagementContext } from "./context";
|
|
16
|
+
import { isPlainRecord } from "./shared";
|
|
17
|
+
|
|
18
|
+
export async function currentPickerController(): Promise<DesktopPickerController | null> {
|
|
19
|
+
const { getClaudePickerController } = await import("../../claude/intercept/runtime");
|
|
20
|
+
return getClaudePickerController();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Picker status for status payloads: the controller's, or the static offline status. */
|
|
24
|
+
export async function pickerStatusFor(config: OcxConfig): Promise<DesktopPickerStatus> {
|
|
25
|
+
const controller = await currentPickerController();
|
|
26
|
+
if (controller) return controller.status();
|
|
27
|
+
const { offlinePickerStatus } = await import("../../claude/desktop-picker");
|
|
28
|
+
return offlinePickerStatus(config);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Run a Desktop mode change under the picker lock (or with offline ops when no controller runs). */
|
|
32
|
+
export async function runPickerTransition<T>(config: OcxConfig, fn: (ops: DesktopPickerOps) => Promise<T>): Promise<T> {
|
|
33
|
+
const [controller, { runDesktopTransition }] = await Promise.all([
|
|
34
|
+
currentPickerController(),
|
|
35
|
+
import("../../claude/desktop-picker"),
|
|
36
|
+
]);
|
|
37
|
+
return runDesktopTransition(controller, fn, { config });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** First-party turns the picker on unless the operator said `claudeCode.intercept.picker: false`. */
|
|
41
|
+
export function pickerPreferenceOn(config: Pick<OcxConfig, "claudeCode">): boolean {
|
|
42
|
+
return config.claudeCode?.intercept?.picker !== false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PICKER_BODY_KEYS = new Set(["enabled", "persist", "trustedLocally", "callerAddedTrust"]);
|
|
46
|
+
/** Enable outcomes that are progress, not refusal: on, waiting for a Desktop restart, or for the keychain step. */
|
|
47
|
+
const ENABLE_ACCEPTED = new Set(["active", "restart_required", "trust_pending"]);
|
|
48
|
+
|
|
49
|
+
interface PickerRequestBody { enabled: boolean; persist: boolean; trustedLocally?: boolean; callerAddedTrust?: boolean }
|
|
50
|
+
|
|
51
|
+
function parsePickerBody(raw: unknown): PickerRequestBody | string {
|
|
52
|
+
if (!isPlainRecord(raw)) return "JSON body must be an object";
|
|
53
|
+
for (const key of Object.keys(raw)) if (!PICKER_BODY_KEYS.has(key)) return `unknown field: ${key}`;
|
|
54
|
+
if (typeof raw.enabled !== "boolean") return "enabled must be a boolean";
|
|
55
|
+
if (typeof raw.persist !== "boolean") return "persist must be a boolean";
|
|
56
|
+
for (const key of ["trustedLocally", "callerAddedTrust"] as const) {
|
|
57
|
+
if (raw[key] !== undefined && typeof raw[key] !== "boolean") return `${key} must be a boolean`;
|
|
58
|
+
}
|
|
59
|
+
return raw as unknown as PickerRequestBody;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function handleClaudeDesktopPickerRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
63
|
+
const { req, url } = ctx;
|
|
64
|
+
|
|
65
|
+
if (url.pathname === "/api/claude-desktop/picker" && req.method === "GET") {
|
|
66
|
+
return jsonResponse({ ok: true, picker: await pickerStatusFor(loadConfig()) });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/api/claude-desktop/picker" && req.method === "PUT") {
|
|
70
|
+
let raw: unknown;
|
|
71
|
+
try {
|
|
72
|
+
raw = await readManagementJsonBody(req);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
rethrowManagementBodyTooLarge(error);
|
|
75
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
76
|
+
}
|
|
77
|
+
const body = parsePickerBody(raw);
|
|
78
|
+
if (typeof body === "string") return jsonResponse({ error: body }, 400);
|
|
79
|
+
const controller = await currentPickerController();
|
|
80
|
+
if (body.enabled) {
|
|
81
|
+
if (!controller) {
|
|
82
|
+
const { offlinePickerStatus } = await import("../../claude/desktop-picker");
|
|
83
|
+
return jsonResponse({
|
|
84
|
+
ok: false,
|
|
85
|
+
code: "picker_proxy_unavailable",
|
|
86
|
+
error: "Picker mode needs the Claude Desktop picker proxy, which is not running in this server.",
|
|
87
|
+
picker: offlinePickerStatus(loadConfig()),
|
|
88
|
+
}, 503);
|
|
89
|
+
}
|
|
90
|
+
const picker = await controller.enable({
|
|
91
|
+
persist: body.persist,
|
|
92
|
+
context: body.trustedLocally ? "cli-trusted" : "server",
|
|
93
|
+
...(body.callerAddedTrust !== undefined ? { callerAddedTrust: body.callerAddedTrust } : {}),
|
|
94
|
+
});
|
|
95
|
+
if (ENABLE_ACCEPTED.has(picker.reason) && !picker.residual?.length) return jsonResponse({ ok: true, picker });
|
|
96
|
+
return jsonResponse({
|
|
97
|
+
ok: false,
|
|
98
|
+
code: "picker_enable_refused",
|
|
99
|
+
reason: picker.reason,
|
|
100
|
+
error: `Picker mode was not enabled (${picker.reason}).`,
|
|
101
|
+
picker,
|
|
102
|
+
}, 409);
|
|
103
|
+
}
|
|
104
|
+
if (controller) {
|
|
105
|
+
const picker = await controller.disable({ persist: body.persist });
|
|
106
|
+
if (!picker.residual?.length) return jsonResponse({ ok: true, picker });
|
|
107
|
+
return jsonResponse({
|
|
108
|
+
ok: false,
|
|
109
|
+
code: "picker_disable_incomplete",
|
|
110
|
+
error: `Picker mode cleanup is incomplete (${picker.residual.join(", ")}).`,
|
|
111
|
+
picker,
|
|
112
|
+
}, 500);
|
|
113
|
+
}
|
|
114
|
+
// No controller: nothing can terminate claude.ai here, so cleanup is local.
|
|
115
|
+
if (body.persist) {
|
|
116
|
+
const { createPickerPreferenceWriter } = await import("../../claude/intercept/runtime");
|
|
117
|
+
if (!(await createPickerPreferenceWriter(ctx.config))(false)) {
|
|
118
|
+
return jsonResponse({ ok: false, error: "The picker preference could not be saved; nothing was removed." }, 500);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const { offlinePickerStatus, removeDesktopPickerArtifacts } = await import("../../claude/desktop-picker");
|
|
122
|
+
const removed = await removeDesktopPickerArtifacts({});
|
|
123
|
+
const picker = { ...offlinePickerStatus(loadConfig()), ...(removed.residual?.length ? { residual: removed.residual } : {}) };
|
|
124
|
+
return jsonResponse({ ok: removed.ok, picker }, removed.ok ? 200 : 500);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|