@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.
Files changed (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-CpuDF3ci.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { recordCommittedDesktopGateway } from "../claude/desktop-gateway-state";
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
+ import { getConfigDir } from "../config/paths";
4
5
  import { loadConfig, mutatePersistedConfig, withConfigMutationLockSync } from "../config";
5
6
  import { claudeDesktopIntegrationEnabledNow, setIntegrationEnabled } from "../codex/desired-state";
6
7
  import { readClientConnectionState, assertClientConnectionUnchanged, assertNoClientDisconnectPending, type ClientConnectionState } from "../client/state";
@@ -21,16 +22,24 @@ import {
21
22
  applyDesktopFirstParty,
22
23
  captureDesktopFirstPartyRollback,
23
24
  isClaudeDesktopMode,
25
+ observeClaudeDesktopMode,
24
26
  recordClaudeDesktopMode,
25
27
  removeDesktopFirstParty,
28
+ resolveClaudeDesktopMode,
26
29
  resolveClaudeDesktopApplyMode,
30
+ type ClaudeDesktopModeObservation,
27
31
  type ClaudeDesktopMode,
28
32
  } from "../claude/desktop-first-party";
33
+ import { FIRST_PARTY_ACCOUNT_RISK } from "../claude/desktop-risk";
34
+ import { claudeInterceptEnabled } from "../claude/intercept/runtime";
35
+ import { ensurePickerCa, pickerCaCertPath, pickerCaFingerprints, pickerLeafCertPath } from "../claude/intercept/picker-ca";
36
+ import { inspectPickerTrust, trustPickerCa, untrustPickerCa, type SecurityRunner } from "../claude/intercept/picker-trust";
37
+ import { offlinePickerStatus, removeDesktopPickerArtifacts, type DesktopPickerStatus } from "../claude/desktop-picker";
29
38
  import { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } from "../claude/desktop-policy";
30
39
  import { filterCatalogVisibleModels, desktopVisibleNativeSlugs, nativeContextLimits } from "../codex/catalog";
31
40
  import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
32
41
  import { findLiveProxy } from "../server/proxy-liveness";
33
- import { CliUsageError, runtimeRequest, takeJsonFlag } from "./runtime-api";
42
+ import { CliUsageError, RuntimeApiError, runtimeRequest, takeJsonFlag, type RuntimeApiDeps } from "./runtime-api";
34
43
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
35
44
  import type { OcxConfig } from "../types";
36
45
 
@@ -43,11 +52,18 @@ function isFamily(value: string | undefined): value is DesktopFamily {
43
52
  function printDesktopHelp(): void {
44
53
  console.log(`Usage:
45
54
  ocx claude desktop [apply] [--first-party | --gateway [--static|--hybrid|--discovery-only]]
46
- --first-party (default) keep Desktop on claude.ai; route only the Code tab's Claude Code
47
- through the local intercept proxy via ~/.claude/settings.json env
48
- --gateway install the third-party gateway profile for the whole app
55
+ --gateway (default) install the third-party gateway profile for the whole app
56
+ --first-party keep Desktop on claude.ai; route only the Code tab's Claude Code through the
57
+ local intercept proxy via ~/.claude/settings.json env. Account risk: this sends
58
+ Claude subscription traffic through a local interception proxy, and Anthropic
59
+ may suspend the account.
49
60
  ocx claude desktop show [--json]
50
61
  ocx claude desktop status [--json]
62
+ ocx claude desktop picker on|off|status|trust
63
+ ocx claude desktop bind <picker-model-id> <provider/model|native/slug>
64
+ first-party: serve a Code tab picker model (e.g. claude-sonnet-4-6) with an opencodex model;
65
+ the picker keeps Anthropic's label, and only Claude Code traffic through the local proxy uses it
66
+ ocx claude desktop unbind <picker-model-id>
51
67
  ocx claude desktop move <provider/model> <opus|fable|sonnet|haiku> [--default]
52
68
  ocx claude desktop default <family> <provider/model|none>
53
69
  ocx claude desktop export <path|->
@@ -62,10 +78,111 @@ export interface ApplyProfileDeps {
62
78
  postApplyImpl?: (
63
79
  mode: Desktop3pConfigMode,
64
80
  profile: DesktopProfile,
65
- ) => Promise<{ ok?: boolean; path?: string; error?: string; warning?: string }>;
81
+ ) => Promise<{ ok?: boolean; path?: string; error?: string; warning?: string; picker?: DesktopPickerStatus }>;
82
+ runtimeRequestImpl?: typeof runtimeRequest;
83
+ ensurePickerCaImpl?: typeof ensurePickerCa;
84
+ inspectPickerTrustImpl?: typeof inspectPickerTrust;
85
+ trustPickerCaImpl?: typeof trustPickerCa;
86
+ untrustPickerCaImpl?: typeof untrustPickerCa;
87
+ removeDesktopPickerArtifacts?: typeof removeDesktopPickerArtifacts;
88
+ security?: SecurityRunner;
89
+ platform?: NodeJS.Platform;
66
90
  probeClaudeDesktopPolicy?: typeof import("../claude/desktop-policy").probeClaudeDesktopPolicy;
67
91
  }
68
92
 
93
+ type DesktopApplyResult = {
94
+ ok: boolean;
95
+ path: string;
96
+ reason?: string;
97
+ warning?: string;
98
+ picker?: DesktopPickerStatus;
99
+ delegated?: boolean;
100
+ };
101
+
102
+ type PickerRouteResponse = {
103
+ ok?: boolean;
104
+ code?: string;
105
+ reason?: string;
106
+ hint?: string;
107
+ picker?: DesktopPickerStatus;
108
+ };
109
+
110
+ function pickerRuntimeRequest<T>(
111
+ path: string,
112
+ init: RequestInit,
113
+ deps: ApplyProfileDeps,
114
+ ): Promise<T> {
115
+ const request = deps.runtimeRequestImpl ?? runtimeRequest;
116
+ const requestDeps: RuntimeApiDeps = deps.findLiveProxyImpl
117
+ ? { findLiveProxy: deps.findLiveProxyImpl }
118
+ : {};
119
+ return request<T>(path, init, requestDeps);
120
+ }
121
+
122
+ async function liveDesktopProxy(deps: ApplyProfileDeps): Promise<boolean> {
123
+ return !!await (deps.findLiveProxyImpl ?? findLiveProxy)();
124
+ }
125
+
126
+ function persistPickerPreference(value: boolean): boolean {
127
+ const outcome = mutatePersistedConfig(current => {
128
+ const claudeCode = current.claudeCode ?? {};
129
+ const intercept = claudeCode.intercept ?? {};
130
+ if (intercept.picker === value) return { changed: false, value: structuredClone(current.claudeCode) };
131
+ current.claudeCode = { ...claudeCode, intercept: { ...intercept, picker: value } };
132
+ return { changed: true, value: structuredClone(current.claudeCode) };
133
+ });
134
+ return outcome.status !== "unavailable";
135
+ }
136
+
137
+ function pickerTrustPaths(deps: ApplyProfileDeps, configDir = getConfigDir()): { caPath: string; leafPath: string; sha1: string } {
138
+ const ca = (deps.ensurePickerCaImpl ?? ensurePickerCa)(configDir);
139
+ return {
140
+ caPath: pickerCaCertPath(configDir),
141
+ leafPath: pickerLeafCertPath(configDir),
142
+ sha1: pickerCaFingerprints(ca.certPem).sha1,
143
+ };
144
+ }
145
+
146
+ async function trustPickerLocally(deps: ApplyProfileDeps): Promise<{ ok: true; callerAddedTrust: boolean; caPath: string; sha1: string } | { ok: false; reason: string }> {
147
+ try {
148
+ const configDir = getConfigDir();
149
+ const paths = pickerTrustPaths(deps, configDir);
150
+ const inspect = deps.inspectPickerTrustImpl ?? inspectPickerTrust;
151
+ const before = await inspect(paths.leafPath, paths.sha1, deps.security, deps.platform);
152
+ if (before === "trusted") return { ok: true, callerAddedTrust: false, caPath: paths.caPath, sha1: paths.sha1 };
153
+ const trust = await (deps.trustPickerCaImpl ?? trustPickerCa)(paths.caPath, deps.security, deps.platform);
154
+ if (!trust.ok) return { ok: false, reason: trust.reason ?? "trust_declined" };
155
+ return { ok: true, callerAddedTrust: true, caPath: paths.caPath, sha1: paths.sha1 };
156
+ } catch (error) {
157
+ return { ok: false, reason: error instanceof Error ? error.message : "trust_failed" };
158
+ }
159
+ }
160
+
161
+ async function compensateLocalPickerTrust(
162
+ trust: { callerAddedTrust: boolean; caPath: string; sha1: string },
163
+ deps: ApplyProfileDeps,
164
+ ): Promise<void> {
165
+ if (!trust.callerAddedTrust) return;
166
+ await (deps.untrustPickerCaImpl ?? untrustPickerCa)(trust.caPath, trust.sha1, deps.security, deps.platform);
167
+ }
168
+
169
+ function printPickerStatus(status: DesktopPickerStatus | undefined, json = false): void {
170
+ if (!status) return;
171
+ if (json) console.log(JSON.stringify(status, null, 2));
172
+ else console.log(`picker: ${JSON.stringify(status)}`);
173
+ }
174
+
175
+ function isAmbiguousPickerTransport(error: unknown): boolean {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ return /timeout|timed out|abort|aborted|lost response|socket hang up|reset/i.test(message);
178
+ }
179
+
180
+ function isAnsweredPickerRefusal(error: unknown): boolean {
181
+ if (!(error instanceof RuntimeApiError)) return false;
182
+ const body = error.body;
183
+ return !!body && typeof body === "object" && ("picker" in body || (body as Record<string, unknown>).code === "picker_proxy_unavailable");
184
+ }
185
+
69
186
  /** Persist only the requested local profile, never an await-old whole configuration. */
70
187
  function saveLocalDesktopProfile(
71
188
  profile: DesktopProfile,
@@ -174,22 +291,24 @@ export type DesktopApplyTarget =
174
291
 
175
292
  /** Parse `ocx claude desktop apply` flags into a target; legacy gateway shape flags imply --gateway. */
176
293
  /**
177
- * Default mode when no flag is given: first-party wherever the local intercept proxy can
178
- * run; a connected client (proxy lives on the hub) or a disabled intercept falls back to
179
- * the gateway profile rather than pointing Claude Code at a proxy that does not exist.
294
+ * Default mode when no flag is given: gateway, unless this install is already first-party (saved
295
+ * mode or observed first-party settings). A connected client still uses gateway, because the
296
+ * intercept proxy lives on the hub.
180
297
  */
181
298
  export function defaultDesktopApplyMode(
182
- config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
299
+ config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
183
300
  connection: ClientConnectionState = readClientConnectionState(),
301
+ observed: ClaudeDesktopModeObservation = observeClaudeDesktopMode(config),
184
302
  ): ClaudeDesktopMode {
185
- const resolved = resolveClaudeDesktopApplyMode(config);
303
+ const resolved = resolveClaudeDesktopApplyMode(config, observed);
186
304
  if (resolved === "gateway") return resolved;
187
305
  return connection.kind === "connected" ? "gateway" : "first-party";
188
306
  }
189
307
 
190
308
  export function parseDesktopApplyArgs(
191
309
  flags: string[],
192
- config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
310
+ config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
311
+ observed?: ClaudeDesktopModeObservation,
193
312
  ): { target: DesktopApplyTarget } | { error: string } {
194
313
  const shapeFlags = flags.filter(arg => ["--static", "--hybrid", "--discovery-only"].includes(arg));
195
314
  const wantsFirstParty = flags.includes("--first-party");
@@ -197,7 +316,9 @@ export function parseDesktopApplyArgs(
197
316
  if (wantsFirstParty && wantsGateway) return { error: "--first-party cannot be combined with --gateway or gateway shape flags." };
198
317
  const unknown = flags.filter(arg => !["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"].includes(arg));
199
318
  if (unknown.length > 0) return { error: `알 수 없는 인자: ${unknown.join(" ")}` };
200
- const kind: ClaudeDesktopMode = wantsFirstParty ? "first-party" : wantsGateway ? "gateway" : defaultDesktopApplyMode(config);
319
+ const kind: ClaudeDesktopMode = wantsFirstParty
320
+ ? "first-party"
321
+ : wantsGateway ? "gateway" : defaultDesktopApplyMode(config, readClientConnectionState(), observed ?? observeClaudeDesktopMode(config));
201
322
  if (kind === "first-party") return { target: { kind } };
202
323
  const parsedMode = parseDesktop3pModeArgs(shapeFlags);
203
324
  if ("error" in parsedMode) return parsedMode;
@@ -205,18 +326,13 @@ export function parseDesktopApplyArgs(
205
326
  }
206
327
 
207
328
  /**
208
- * Why a gateway apply happened when the help text calls first-party the default.
329
+ * What an apply without a flag says after it lands on gateway, the default.
209
330
  *
210
- * `resolveClaudeDesktopMode` keeps an existing install where it is: an explicit
211
- * `claudeCode.desktopMode` wins, and a stored gateway apply marker keeps gateway. Both rules are
212
- * right — a working Desktop install must not flip underneath its user because a default moved.
213
- * Together they mean an existing gateway user never arrives at first-party without discovering
214
- * `--first-party` unaided, while `ocx claude desktop --help` tells them first-party is "(default)".
215
- *
216
- * The fix is not to change the resolution. It is to say, at the moment of the apply, that the
217
- * other mode exists and what selects it. Returns null when the user asked for gateway explicitly,
218
- * because they already know, and when first-party is simply unavailable here — a connected client
219
- * or a disabled intercept cannot run it, so offering it would be advice that fails.
331
+ * It names why gateway was chosen (the default, a saved gateway mode, or a previous gateway apply),
332
+ * that first-party exists and which command selects it, and the account risk that comes with it,
333
+ * so nobody switches without reading it. Returns nothing when the user asked for gateway
334
+ * explicitly, because they already chose, and when first-party cannot run here — a connected client
335
+ * or a disabled intercept — because offering it would be advice that fails.
220
336
  */
221
337
  export function gatewayModeExplanation(input: {
222
338
  requestedExplicitly: boolean;
@@ -226,19 +342,20 @@ export function gatewayModeExplanation(input: {
226
342
  if (input.requestedExplicitly) return [];
227
343
  const connection = input.connection ?? readClientConnectionState();
228
344
  if (connection.kind === "connected") return [];
229
- // Only a stored preference is worth explaining. Without one, gateway was chosen because
230
- // first-party cannot run here, and naming an unavailable alternative is advice that fails.
345
+ if (!claudeInterceptEnabled(input.config)) return [];
231
346
  const savedMode = input.config.claudeCode?.desktopMode;
232
347
  const hasSavedGateway = isClaudeDesktopMode(savedMode) && savedMode === "gateway";
233
348
  const hasApplyMarker = input.config.claudeCode?.desktopProfile?.appliedFingerprint !== undefined;
234
- if (!hasSavedGateway && !hasApplyMarker) return [];
235
349
  const reason = hasSavedGateway
236
- ? "this machine has claudeCode.desktopMode saved as gateway"
237
- : "this machine carries a previous gateway apply";
350
+ ? "because this machine has claudeCode.desktopMode saved as gateway; an existing install is never switched for you"
351
+ : hasApplyMarker
352
+ ? "because this machine carries a previous gateway apply; an existing install is never switched for you"
353
+ : "because gateway is the default for Claude Desktop";
238
354
  return [
239
- `Applied the gateway profile because ${reason}; an existing install is never switched for you.`,
355
+ `Applied the gateway profile ${reason}.`,
240
356
  "First-party keeps Desktop on your claude.ai account and routes only the Code tab through the local proxy:",
241
357
  " ocx claude desktop apply --first-party",
358
+ `Account risk: ${FIRST_PARTY_ACCOUNT_RISK.message}`,
242
359
  ];
243
360
  }
244
361
 
@@ -249,11 +366,23 @@ export function gatewayModeExplanation(input: {
249
366
  */
250
367
  async function applyFirstPartyDesktop(
251
368
  deps: ApplyProfileDeps,
252
- ): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
369
+ ): Promise<DesktopApplyResult> {
253
370
  try { assertNoClientDisconnectPending(); } catch { return { ok: false, path: "", reason: "client_disconnect_pending" }; }
254
371
  const connection = readClientConnectionState();
255
372
  if (connection.kind === "connected") return { ok: false, path: "", reason: "first_party_requires_local_hub" };
256
373
  if (connection.kind !== "disconnected") return { ok: false, path: "", reason: "client_connection_invalid" };
374
+ if (await liveDesktopProxy(deps)) {
375
+ try {
376
+ const applied = await pickerRuntimeRequest<DesktopApplyResult>(
377
+ "/api/claude-desktop/apply",
378
+ { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mode: "first-party" }) },
379
+ deps,
380
+ );
381
+ return { ...applied, path: applied.path ?? "", delegated: true };
382
+ } catch (error) {
383
+ return { ok: false, path: "", reason: error instanceof Error ? error.message : "daemon apply failed" };
384
+ }
385
+ }
257
386
  const config = loadConfig();
258
387
  const desired = setIntegrationEnabled("claude-desktop", true);
259
388
  if (!desired.ok) return { ok: false, path: "", reason: desired.message };
@@ -279,6 +408,7 @@ async function applyFirstPartyDesktop(
279
408
  return {
280
409
  ok: true,
281
410
  path: applied.path,
411
+ picker: offlinePickerStatus(loadConfig(), deps.platform),
282
412
  ...(saved ? {} : { warning: "desktop mode marker was not saved" }),
283
413
  };
284
414
  }
@@ -287,10 +417,11 @@ export async function applyDesktop(
287
417
  profile: DesktopProfile | undefined,
288
418
  target: DesktopApplyTarget,
289
419
  deps: ApplyProfileDeps = {},
290
- ): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
420
+ ): Promise<DesktopApplyResult> {
291
421
  if (target.kind === "first-party") return applyFirstPartyDesktop(deps);
292
422
  const result = await applyProfile(profile, target.mode, deps);
293
423
  if (!result.ok) return result;
424
+ if (result.delegated) return result;
294
425
  const modeSaved = saveDesktopMode("gateway", deps);
295
426
  const warning = [result.warning, modeSaved ? "" : "desktop mode marker was not saved"].filter(Boolean).join(" ");
296
427
  // The gateway mode is committed before retiring first-party settings.
@@ -305,7 +436,7 @@ export async function applyProfile(
305
436
  profile: DesktopProfile | undefined,
306
437
  mode: Desktop3pConfigMode,
307
438
  deps: ApplyProfileDeps = {},
308
- ): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
439
+ ): Promise<DesktopApplyResult> {
309
440
  try { assertNoClientDisconnectPending(); } catch { return { ok: false, path: "", reason: "client_disconnect_pending" }; }
310
441
  const connection = readClientConnectionState();
311
442
  if (connection.kind === "connected") return applyConnectedDesktopProfile(mode, connection, deps);
@@ -325,7 +456,7 @@ export async function applyProfile(
325
456
  // serving process installs the map there; a local-only write leaves the
326
457
  // daemon unable to decode aliases, and the provider rejects them (400).
327
458
  const post = deps.postApplyImpl ?? (async (m: Desktop3pConfigMode, p: DesktopProfile) =>
328
- runtimeRequest<{ ok?: boolean; path?: string; error?: string; saved?: boolean; warning?: string }>(
459
+ runtimeRequest<{ ok?: boolean; path?: string; error?: string; saved?: boolean; warning?: string; picker?: DesktopPickerStatus }>(
329
460
  "/api/claude-desktop/apply",
330
461
  // The daemon's config may be older than what we just saved, so the
331
462
  // profile travels with the request instead of being re-read there.
@@ -341,6 +472,8 @@ export async function applyProfile(
341
472
  return {
342
473
  ok: true,
343
474
  path: applied.path ?? "",
475
+ picker: applied.picker,
476
+ delegated: true,
344
477
  ...(warning ? { warning } : partial ? { warning: "applied marker was not saved" } : {}),
345
478
  };
346
479
  } catch (error) {
@@ -386,6 +519,130 @@ export async function applyProfile(
386
519
  };
387
520
  }
388
521
 
522
+ async function handleClaudeDesktopPickerCommand(
523
+ argv: string[],
524
+ config: OcxConfig,
525
+ deps: ApplyProfileDeps,
526
+ ): Promise<number> {
527
+ const action = argv[1];
528
+ const rest = argv.slice(2);
529
+ const usage = "Usage: ocx claude desktop picker on|off|status|trust [--json]";
530
+ if (!action || !["on", "off", "status", "trust"].includes(action)) throw new CliUsageError(usage);
531
+ const wantsJson = takeJsonFlag(rest);
532
+ if (rest.length > 0 || (action !== "status" && wantsJson)) throw new CliUsageError(usage);
533
+
534
+ const requestPicker = (body: Record<string, unknown>) => pickerRuntimeRequest<PickerRouteResponse>(
535
+ "/api/claude-desktop/picker",
536
+ { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(body) },
537
+ deps,
538
+ );
539
+
540
+ if (action === "status") {
541
+ if (await liveDesktopProxy(deps)) {
542
+ try {
543
+ const response = await pickerRuntimeRequest<{ ok?: boolean; picker?: DesktopPickerStatus }>("/api/claude-desktop/picker", {}, deps);
544
+ printPickerStatus(response.picker, wantsJson);
545
+ } catch (error) {
546
+ console.error(error instanceof Error ? error.message : String(error));
547
+ return 1;
548
+ }
549
+ } else {
550
+ printPickerStatus(offlinePickerStatus(config, deps.platform), wantsJson);
551
+ }
552
+ return 0;
553
+ }
554
+
555
+ if (action === "off") {
556
+ if (await liveDesktopProxy(deps)) {
557
+ try {
558
+ const response = await requestPicker({ enabled: false, persist: true });
559
+ printPickerStatus(response.picker, false);
560
+ } catch (error) {
561
+ if (isAnsweredPickerRefusal(error)) {
562
+ const body = (error as RuntimeApiError).body as PickerRouteResponse;
563
+ printPickerStatus(body.picker, false);
564
+ }
565
+ console.error(error instanceof Error ? error.message : String(error));
566
+ return 1;
567
+ }
568
+ } else {
569
+ if (!persistPickerPreference(false)) {
570
+ console.error("Could not persist claudeCode.intercept.picker=false");
571
+ return 1;
572
+ }
573
+ const removed = await (deps.removeDesktopPickerArtifacts ?? removeDesktopPickerArtifacts)({ configDir: getConfigDir(), security: deps.security, platform: deps.platform });
574
+ if (!removed.ok) console.error(`picker cleanup incomplete${removed.residual?.length ? `: ${removed.residual.join(", ")}` : ""}`);
575
+ printPickerStatus(offlinePickerStatus(loadConfig(), deps.platform), false);
576
+ if (!removed.ok) return 1;
577
+ }
578
+ console.log("Fully quit and reopen Claude Desktop");
579
+ return 0;
580
+ }
581
+
582
+ if (action === "on" && !(await liveDesktopProxy(deps))) {
583
+ console.error("proxy_unavailable");
584
+ return 1;
585
+ }
586
+
587
+ let localTrust: { callerAddedTrust: boolean; caPath: string; sha1: string } | undefined;
588
+ const sendEnable = async (trustedLocally = false): Promise<PickerRouteResponse> => requestPicker({
589
+ enabled: true,
590
+ persist: action === "on",
591
+ ...(trustedLocally ? { trustedLocally: true, callerAddedTrust: localTrust?.callerAddedTrust ?? false } : {}),
592
+ });
593
+
594
+ if (action === "trust") {
595
+ const trusted = await trustPickerLocally(deps);
596
+ if (!trusted.ok) {
597
+ console.error(trusted.reason);
598
+ return 1;
599
+ }
600
+ localTrust = trusted;
601
+ if (!(await liveDesktopProxy(deps))) {
602
+ await compensateLocalPickerTrust(localTrust, deps);
603
+ console.error("proxy_unavailable");
604
+ return 1;
605
+ }
606
+ }
607
+
608
+ let response: PickerRouteResponse;
609
+ try {
610
+ response = await sendEnable(action === "trust");
611
+ if (response.ok === false) {
612
+ printPickerStatus(response.picker, false);
613
+ console.error(response.reason ?? response.code ?? "picker_enable_refused");
614
+ return 1;
615
+ }
616
+ if (action === "on" && response.picker?.reason === "trust_pending") {
617
+ const trusted = await trustPickerLocally(deps);
618
+ if (!trusted.ok) {
619
+ console.error(trusted.reason);
620
+ return 1;
621
+ }
622
+ localTrust = trusted;
623
+ response = await sendEnable(true);
624
+ }
625
+ printPickerStatus(response.picker, false);
626
+ if (response.picker?.reason === "restart_required") console.log("Fully quit and reopen Claude Desktop");
627
+ const reason = response.picker?.reason;
628
+ return response.ok === false || (reason !== "active" && reason !== "restart_required") ? 1 : 0;
629
+ } catch (error) {
630
+ if (isAnsweredPickerRefusal(error)) {
631
+ const body = (error as RuntimeApiError).body as PickerRouteResponse;
632
+ printPickerStatus(body.picker, false);
633
+ console.error(body.reason ?? body.code ?? (error instanceof Error ? error.message : String(error)));
634
+ return 1;
635
+ }
636
+ if (isAmbiguousPickerTransport(error)) {
637
+ console.error("state unknown - run ocx claude desktop picker status");
638
+ return 1;
639
+ }
640
+ if (localTrust) await compensateLocalPickerTrust(localTrust, deps);
641
+ console.error(error instanceof Error ? error.message : String(error));
642
+ return 1;
643
+ }
644
+ }
645
+
389
646
  export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProfileDeps = {}): Promise<number> {
390
647
  const command = argv[0];
391
648
  if (command === "help" || command === "--help" || command === "-h") {
@@ -421,6 +678,8 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
421
678
  if (target.kind === "first-party") {
422
679
  console.log(`Claude Desktop first-party 설정을 적용했습니다: ${result.path}`);
423
680
  console.log("Desktop 앱 설정은 그대로이며, Code 탭의 Claude Code만 로컬 프록시를 거칩니다.");
681
+ console.warn(`⚠️ ${FIRST_PARTY_ACCOUNT_RISK.message}`);
682
+ printPickerStatus(result.picker, false);
424
683
  } else {
425
684
  console.log(`Claude Desktop gateway 설정을 적용했습니다: ${result.path}`);
426
685
  for (const line of gatewayModeExplanation({
@@ -454,6 +713,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
454
713
  console.warn("Local client profile only; connected Desktop apply uses the hub profile.");
455
714
  }
456
715
  const config = loadConfig();
716
+ if (command === "picker") return await handleClaudeDesktopPickerCommand(argv, config, deps);
457
717
  // `status` is API-backed and must NOT build local state first: the whole point of the
458
718
  // route the GUI polls (/api/claude-desktop/status) is the applied-vs-desired comparison,
459
719
  // including staleness, drift and health, which only the running proxy knows. `show`
@@ -471,6 +731,32 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
471
731
  }
472
732
  return 0;
473
733
  }
734
+ // Bindings are API-backed for the same reason as `status`: the running proxy routes with
735
+ // its live config, so the change must land there, not only in the file.
736
+ if (command === "bind" || command === "unbind") {
737
+ const [, pickerId, route, ...extra] = argv;
738
+ const usage = command === "bind"
739
+ ? "Usage: ocx claude desktop bind <picker-model-id> <provider/model|native/slug>"
740
+ : "Usage: ocx claude desktop unbind <picker-model-id>";
741
+ if (!pickerId || (command === "bind" ? !route || extra.length > 0 : route !== undefined)) throw new CliUsageError(usage);
742
+ const body = command === "bind" ? { set: { [pickerId]: route! } } : { remove: [pickerId] };
743
+ const result = await runtimeRequest<{ modelBindings?: Record<string, string> }>("/api/claude-desktop/first-party-bindings", {
744
+ method: "PUT",
745
+ headers: { "content-type": "application/json" },
746
+ body: JSON.stringify(body),
747
+ });
748
+ const bindings = result.modelBindings ?? {};
749
+ console.log(command === "bind"
750
+ ? `Code 탭 피커의 ${pickerId}를 ${route}로 연결했습니다. 다음 요청부터 적용됩니다.`
751
+ : `${pickerId} 연결을 해제했습니다.`);
752
+ const ids = Object.keys(bindings).sort();
753
+ if (ids.length === 0) console.log("현재 연결된 피커 모델이 없습니다.");
754
+ for (const id of ids) console.log(` ${id} -> ${bindings[id]}`);
755
+ if (resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) === "gateway") {
756
+ console.warn("⚠️ Desktop이 gateway 모드입니다. 바인딩은 first-party 모드(ocx claude desktop apply --first-party)의 Code 탭과 claude CLI에만 적용됩니다.");
757
+ }
758
+ return 0;
759
+ }
474
760
  const state = await buildClaudeDesktopState(config);
475
761
  if (command === "show") {
476
762
  const rest = argv.slice(1);
package/src/cli/claude.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import { spawn } from "node:child_process";
11
11
  import { loadConfig } from "../config";
12
12
  import { injectClaudeAgentDefs } from "../claude/agents-inject";
13
- import { CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
13
+ import { CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
14
14
  import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
15
15
  import { claudeConfigDir, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
16
16
  import { commandInvocation } from "../lib/win-exec";
@@ -30,7 +30,7 @@ import { readServiceApiTokenState, type ServiceApiTokenState } from "../lib/serv
30
30
  import { DEFAULT_CATALOG_PATH } from "../codex/paths";
31
31
  import { readFileSync } from "node:fs";
32
32
  import { join } from "node:path";
33
- import { aliasForNative, aliasForRoute } from "../claude/alias";
33
+ import { aliasForNative, aliasForRoute, legacyAliasForNative, legacyAliasForRoute } from "../claude/alias";
34
34
  import { desktop3pAlias } from "../claude/desktop-3p";
35
35
 
36
36
  export interface ClaudeLaunchEnv {
@@ -373,12 +373,13 @@ export function buildClaudeEnv(
373
373
  // worse than the problem. So this stays opt-in per config rather than
374
374
  // unconditional, and setDefault keeps an operator's own export.
375
375
  setDefault("ENABLE_TOOL_SEARCH", claudeToolSearchEnv(config.claudeCode?.toolSearch));
376
- // Context-window override: the official pair — MAX_CONTEXT_TOKENS alone is ignored
377
- // for recognized claude-shaped ids unless DISABLE_COMPACT=1 rides along (devlog 135).
378
376
  const maxCtx = config.claudeCode?.maxContextTokens;
379
377
  if (typeof maxCtx === "number" && Number.isFinite(maxCtx) && maxCtx > 0) {
380
378
  setDefault("CLAUDE_CODE_MAX_CONTEXT_TOKENS", String(Math.floor(maxCtx)));
381
- setDefault("DISABLE_COMPACT", "1");
379
+ // Claude Code 2.1.278 honors this without DISABLE_COMPACT when the model id
380
+ // does not start with "claude-" (gF). Current ocx-claude aliases qualify.
381
+ // A persisted claude-ocx id is still claude-shaped, so that one session keeps
382
+ // the 200k accounting until the picker is moved to the new id.
382
383
  }
383
384
  // Auto-context (devlog 260712 020): min(believed window, env) inside the CLI means
384
385
  // one global env acts as a per-model floor — [1m]-marked models compact here while
@@ -464,10 +465,15 @@ export function readConnectedClaudeContextWindows(path = DEFAULT_CATALOG_PATH):
464
465
  const id = slug.slice(slash + 1);
465
466
  const routeAlias = aliasForRoute(provider, id);
466
467
  if (routeAlias) put(routeAlias, contextWindow);
468
+ // A selector saved under the legacy claude-ocx spelling keeps its window here too.
469
+ const legacyRoute = legacyAliasForRoute(provider, id);
470
+ if (legacyRoute) put(legacyRoute, contextWindow);
467
471
  put(desktop3pAlias(provider, id), contextWindow);
468
472
  } else {
469
473
  const nativeAlias = aliasForNative(slug);
470
474
  if (nativeAlias) put(nativeAlias, contextWindow);
475
+ const legacyNative = legacyAliasForNative(slug);
476
+ if (legacyNative) put(legacyNative, contextWindow);
471
477
  put(desktop3pAlias("native", slug), contextWindow);
472
478
  }
473
479
  }
@@ -485,7 +491,8 @@ export async function ensureProxyForClaude(deps: ClaudeProxyEnsureDeps = {}): Pr
485
491
  // A proxy that has only just bound can miss a single probe while its event loop
486
492
  // is still settling startup work — the same just-started race the stop paths
487
493
  // already retry for (#764, SERVICE_STOP_LIVENESS). Only the attempts budget is
488
- // borrowed here; the probe timeout remains DEFAULT_PROBE_TIMEOUT_MS (750 ms).
494
+ // borrowed here; the probe timeout remains DEFAULT_PROBE_TIMEOUT_MS (750 ms unless
495
+ // OCX_PROBE_TIMEOUT_MS raises it).
489
496
  // Without this, `ocx claude` can spawn a second proxy while the first is serving.
490
497
  const live = await (deps.findLiveProxy ?? findLiveProxy)({ attempts: 3 });
491
498
  if (live) return live.port;
@@ -592,7 +599,8 @@ const DESKTOP_3P_ALIAS = /^claude-opus-4(?:-8)?-[a-z][0-9a-z]{2}$/;
592
599
  export function isProxyOnlyModelId(value: string, providerNames: readonly string[] = []): boolean {
593
600
  const id = value.trim().replace(/\[1m\]$/, "");
594
601
  if (!id) return false;
595
- if (id.startsWith(CLAUDE_ALIAS_PREFIX_V1) || id.startsWith(CLAUDE_ALIAS_PREFIX_V2) || DESKTOP_3P_ALIAS.test(id)) {
602
+ const aliasPrefixes = [CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2];
603
+ if (aliasPrefixes.some(prefix => id.startsWith(prefix)) || DESKTOP_3P_ALIAS.test(id)) {
596
604
  return true;
597
605
  }
598
606
  const slash = id.indexOf("/");
@@ -28,6 +28,7 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
28
28
  // The entire updater-inspection namespace is zero-effect, including malformed
29
29
  // or future actions. A later `apply` implementation must own its preflight.
30
30
  if (command === "system" && args[1] === "codex-cli-update") return true;
31
+ if (command === "__update-badge") return true;
31
32
  return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
32
33
  }
33
34