@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,54 @@
1
+ /** A management route a capability drives. Path text only; never a handler reference. */
2
+ export interface CapabilityRoute {
3
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+ readonly path: string;
5
+ }
6
+
7
+ export interface CapabilityFlag {
8
+ readonly name: string;
9
+ readonly value?: "string" | "number" | "boolean";
10
+ readonly required?: boolean;
11
+ readonly summary: string;
12
+ }
13
+
14
+ /**
15
+ * How a capability emits JSON.
16
+ *
17
+ * - `payload`: the API payload, largely unwrapped.
18
+ * - `envelope`: a CLI-shaped object with its own schema.
19
+ * - `none`: no `--json` mode.
20
+ */
21
+ export type CapabilityJsonMode = "payload" | "envelope" | "none";
22
+
23
+ export interface Capability {
24
+ /** Command path, e.g. `["account", "pause"]`. */
25
+ readonly command: readonly string[];
26
+ readonly summary: string;
27
+ readonly routes: readonly CapabilityRoute[];
28
+ readonly flags: readonly CapabilityFlag[];
29
+ readonly mutates: boolean;
30
+ readonly json: CapabilityJsonMode;
31
+ /** Complete CLI usage, including operands, when verified for this leaf. */
32
+ readonly usage?: string;
33
+ readonly details?: readonly string[];
34
+ /**
35
+ * Extra banner rows this capability owns, for surfaces the banner shows separately
36
+ * from the bare command (`ocx restore back`, `ocx doctor --reclaim-response-temps`).
37
+ * Without this the banner cannot equal the capability set: it legitimately carries more
38
+ * rows than there are commands.
39
+ */
40
+ readonly bannerLines?: readonly string[];
41
+ }
42
+
43
+ /**
44
+ * Surfaces resolved in the CLI head, before dispatch.
45
+ *
46
+ * They belong in `ocx capabilities` output and in the banner, but not in `CLI_COMMANDS`:
47
+ * `--version`, `-v`, and `version` are answered at `root.ts` and exit, so none of them is
48
+ * a runner key to parity-check against.
49
+ */
50
+ export interface HeadCapability {
51
+ readonly invocations: readonly string[];
52
+ readonly summary: string;
53
+ readonly bannerLine: string;
54
+ }
@@ -0,0 +1,57 @@
1
+ import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
2
+ import { CliUsageError, RuntimeApiError, PROXY_NOT_RUNNING_MESSAGE, printData, terminalSafeText } from "./runtime-api";
3
+
4
+ function own(value: unknown, key: string): unknown {
5
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
6
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
7
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
8
+ }
9
+
10
+ /** A small error/exit boundary; routes and domain DTO validation belong to callers. */
11
+ export async function runCatalogAction(
12
+ action: () => Promise<number | void>, knownErrors: Readonly<Record<string, string>> = {},
13
+ ): Promise<number> {
14
+ try {
15
+ return (await action()) ?? 0;
16
+ } catch (error) {
17
+ if (error instanceof CliUsageError) {
18
+ // New domain parsers use static messages instead of raw JSON/validator exceptions.
19
+ console.error(`Error: ${terminalSafeText(error.message)}`);
20
+ if (error.usage) for (const line of error.usage.split("\n")) console.error(terminalSafeText(line));
21
+ return 2;
22
+ }
23
+ if (error instanceof RuntimeApiError) {
24
+ if (error.code) {
25
+ console.error(`Error: ${error.code === "proxy_not_running" ? PROXY_NOT_RUNNING_MESSAGE
26
+ : "This listener has no management API. Run management commands on the connected Hub. No request was sent."}`);
27
+ return 1;
28
+ }
29
+ const code = own(error.body, "code") ?? own(own(error.body, "error"), "code");
30
+ const known = typeof code === "string" && Object.hasOwn(knownErrors, code) ? knownErrors[code] : undefined;
31
+ const message = error.status === 404 ? "The requested record or operation was not found on the selected target."
32
+ : error.status === 409 ? "The selected target refused a conflicting change. Read back before trying again."
33
+ : error.status === 503 ? "Management API is unavailable. Use the intended running proxy; connected clients must make management changes on their Hub. A write outcome may be unknown."
34
+ : "The management request failed. Inspect the selected target before retrying; the write outcome may be unknown.";
35
+ console.error(`Error: ${terminalSafeText(known ?? message)}`);
36
+ return error.status === 404 ? 4 : error.status === 409 ? 5 : 1;
37
+ }
38
+ console.error("Error: Management operation did not return a usable outcome. Inspect the selected target before retrying; no rollback is implied.");
39
+ return 1;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Caller supplies a validated, rebuilt public DTO, not an arbitrary server body.
45
+ * This helper normalizes only the shared catalog disposition and preserves its exit.
46
+ */
47
+ export function printCatalogResult(
48
+ data: Record<string, unknown>, refresh: unknown, wantsJson: boolean, lines: readonly string[],
49
+ ): number {
50
+ const catalogRefresh = normalizeCatalogDisposition(refresh);
51
+ if (catalogRefresh === null) throw new Error("Invalid catalog outcome");
52
+ const converged = catalogRefresh.status === "committed" && !catalogRefresh.degraded;
53
+ const outcome = converged ? "Catalog committed."
54
+ : "Change saved, but catalog convergence is incomplete. Read back before retrying.";
55
+ printData({ ...data, catalogRefresh }, wantsJson, [...lines, outcome]);
56
+ return converged ? 0 : 1;
57
+ }
@@ -2,6 +2,8 @@ import { spawnSync } from "node:child_process";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { loadConfig } from "../config";
5
+ import { readConfigFileSnapshot } from "../config/diagnostics";
6
+ import { chatgptDesktopConfigIssue } from "../config/schema/chatgpt-desktop";
5
7
  import {
6
8
  chatgptShimLauncherPath,
7
9
  resolveChatgptCodexBinary,
@@ -21,6 +23,20 @@ function run(command: string, args: string[]) {
21
23
  return { ok: result.status === 0, output: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() };
22
24
  }
23
25
 
26
+ /**
27
+ * Why the config file's `chatgptDesktop` block reads as absent, or null when it is valid, absent
28
+ * or the file cannot be parsed (the config loader reports that case itself).
29
+ */
30
+ function chatgptDesktopIssueInFile(): string | null {
31
+ const { raw } = readConfigFileSnapshot();
32
+ if (raw === undefined) return null;
33
+ try {
34
+ return chatgptDesktopConfigIssue(JSON.parse(raw.replace(/^\uFEFF/, "")));
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
24
40
  /**
25
41
  * The installed app, found and confirmed by bundle identifier (com.openai.codex) the same way the
26
42
  * desktop restart adapter does. "ChatGPT" is a display name another app can share, so quitting,
@@ -90,7 +106,10 @@ export async function handleChatgptCommand(args: string[], platform: NodeJS.Plat
90
106
  const install = discoverApp();
91
107
  if (sub === "status") {
92
108
  const app = install ? appState(install, launcher) : { running: false, shim: false };
93
- console.log(`app-server shim (experimental): ${config.chatgptDesktop?.appServerShim === true ? "on" : "off"}
109
+ const configIssue = config.chatgptDesktop?.appServerShim === true ? null : chatgptDesktopIssueInFile();
110
+ const flag = config.chatgptDesktop?.appServerShim === true ? "on"
111
+ : configIssue ? `off (config.json ${configIssue}; the whole chatgptDesktop block is ignored)` : "off";
112
+ console.log(`app-server shim (experimental): ${flag}
94
113
  launcher: ${existsSync(launcher) ? "present" : "absent"}
95
114
  app: ${install ? (app.running ? "running" : "not running") : "not installed"}
96
115
  CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
@@ -101,23 +120,30 @@ CODEX_CLI_PATH launcher: ${app.shim ? "yes" : "no"}`);
101
120
  console.error("ChatGPT (com.openai.codex) was not found; install or open it once, then retry.");
102
121
  return 1;
103
122
  }
123
+ let binary: string | undefined;
104
124
  if (sub === "launch") {
105
125
  if (config.chatgptDesktop?.appServerShim !== true) {
106
- console.error('Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.');
126
+ // Read the file only to explain a refusal; restore and a valid launch never touch it.
127
+ const configIssue = chatgptDesktopIssueInFile();
128
+ console.error(configIssue
129
+ ? `Experimental shim disabled: config.json ${configIssue}. The whole chatgptDesktop block is ignored until that is fixed.`
130
+ : "Experimental shim disabled; set chatgptDesktop.appServerShim: true before launching.");
107
131
  return 1;
108
132
  }
109
- const binary = resolveChatgptCodexBinary(install.root);
133
+ binary = resolveChatgptCodexBinary(install.root) ?? undefined;
110
134
  if (!binary) {
111
135
  console.error(`No bundled app-server binary was found in ${install.root}; the shim cannot launch this build.`);
112
136
  return 1;
113
137
  }
114
- const untrusted = untrustedChatgptBundleReason(install.root, binary);
115
- if (untrusted) {
116
- console.error(`Refusing to launch the shim: ${untrusted}.`);
117
- return 1;
118
- }
119
- writeChatgptShimLauncher(undefined, binary);
120
138
  }
139
+ // Both relaunch paths execute the discovered bundle. Restore validates the app
140
+ // shell without requiring an app-server binary or the experimental opt-in flag.
141
+ const untrusted = untrustedChatgptBundleReason(install.root, binary);
142
+ if (untrusted) {
143
+ console.error(`Refusing to ${sub === "launch" ? "launch the shim" : "restore ChatGPT"}: ${untrusted}.`);
144
+ return 1;
145
+ }
146
+ if (binary) writeChatgptShimLauncher(undefined, binary);
121
147
  if (!(await quitApp(install, launcher))) {
122
148
  console.error("ChatGPT did not quit; quit it manually and retry.");
123
149
  return 1;
@@ -64,6 +64,23 @@ export async function reconcileClientStartupBeforeReady<T>(
64
64
  return result;
65
65
  }
66
66
 
67
+ /** Establish catalog observation before any unrelated startup reconciliation awaits. */
68
+ export async function syncCodexBeforeCatalogObservation<T>(
69
+ gate: ReadinessGate,
70
+ syncCodex: (gate: ReadinessGate) => Promise<T>,
71
+ startObservation: () => void,
72
+ ): Promise<T> {
73
+ let ready = false;
74
+ const forwardingGate: ReadinessGate = {
75
+ getStatus: () => gate.getStatus(),
76
+ markReady: () => { ready = true; gate.markReady(); },
77
+ markFailed: () => { ready = false; gate.markFailed(); },
78
+ };
79
+ const result = await syncCodex(forwardingGate);
80
+ if (ready) startObservation();
81
+ return result;
82
+ }
83
+
67
84
  /**
68
85
  * Reconcile the generated Claude Code roster after the proxy listener is live.
69
86
  *
@@ -0,0 +1,100 @@
1
+ /** Runtime-owned Desktop profile editing. Saving never applies a native configuration. */
2
+ import { DESKTOP_FAMILIES, parseDesktopProfile, type DesktopProfile } from "../claude/desktop-profile";
3
+ import { runCatalogAction } from "./catalog-command-result";
4
+ import { readJsonInput, serializeManagementJson } from "./json-input";
5
+ import { CliUsageError, printData, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
6
+
7
+ export interface DesktopProfileCliDeps extends RuntimeApiDeps {
8
+ runtimeRequestImpl?: typeof runtimeRequest;
9
+ }
10
+
11
+ const USAGE = "Usage: ocx claude desktop profile show [--json]\n ocx claude desktop profile import FILE|- [--json]";
12
+
13
+ function record(value: unknown): Record<string, unknown> {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Desktop state");
15
+ return value as Record<string, unknown>;
16
+ }
17
+
18
+ function text(value: unknown): string {
19
+ if (typeof value !== "string" || !value.length) throw new Error("Invalid Desktop text");
20
+ return value;
21
+ }
22
+
23
+ function boolean(value: unknown): boolean {
24
+ if (typeof value !== "boolean") throw new Error("Invalid Desktop flag");
25
+ return value;
26
+ }
27
+
28
+ function modelIdentity(row: Record<string, unknown>) {
29
+ const route = text(row.route);
30
+ if (!route.includes("/")) throw new Error("Invalid Desktop route");
31
+ if (row.contextWindow !== undefined && (typeof row.contextWindow !== "number"
32
+ || !Number.isSafeInteger(row.contextWindow) || row.contextWindow <= 0)) throw new Error("Invalid Desktop context window");
33
+ return { route, label: text(row.label), ...(row.contextWindow === undefined ? {} : { contextWindow: row.contextWindow as number }) };
34
+ }
35
+
36
+ function desktopState(raw: unknown, saved: boolean) {
37
+ const value = record(raw);
38
+ if (saved && value.ok !== true) throw new Error("Desktop save was not confirmed");
39
+ const profile = parseDesktopProfile(value.profile);
40
+ if (!Array.isArray(value.models) || !Array.isArray(value.rendered)
41
+ || !Number.isSafeInteger(value.port) || (value.port as number) < 1 || (value.port as number) > 65535) {
42
+ throw new Error("Invalid Desktop state");
43
+ }
44
+ const seen = new Set<string>();
45
+ const models = value.models.map(rawModel => {
46
+ const row = record(rawModel), identity = modelIdentity(row), assignment = record(row.assignment);
47
+ const expected = profile.assignments[identity.route];
48
+ if (!expected || seen.has(identity.route) || assignment.family !== expected.family || assignment.alias !== expected.alias) {
49
+ throw new Error("Invalid Desktop assignment");
50
+ }
51
+ seen.add(identity.route);
52
+ return { ...identity, available: boolean(row.available), effortSupported: boolean(row.effortSupported),
53
+ supports1m: boolean(row.supports1m), assignment: { family: expected.family, alias: expected.alias } };
54
+ });
55
+ if (seen.size !== Object.keys(profile.assignments).length) throw new Error("Incomplete Desktop models");
56
+ const renderedSeen = new Set<string>();
57
+ const rendered = value.rendered.map(rawModel => {
58
+ const row = record(rawModel), identity = modelIdentity(row);
59
+ if (!DESKTOP_FAMILIES.includes(row.family as typeof DESKTOP_FAMILIES[number])
60
+ || profile.assignments[identity.route]?.family !== row.family || renderedSeen.has(identity.route)
61
+ || !models.some(model => model.route === identity.route && model.available)) throw new Error("Invalid rendered Desktop model");
62
+ renderedSeen.add(identity.route);
63
+ return { ...identity, name: text(row.name), family: row.family as typeof DESKTOP_FAMILIES[number],
64
+ isFamilyDefault: boolean(row.isFamilyDefault), supports1m: boolean(row.supports1m) };
65
+ });
66
+ if (renderedSeen.size !== models.filter(model => model.available).length) throw new Error("Incomplete rendered Desktop models");
67
+ return { ...(saved ? { ok: true as const } : {}), profile, models, rendered, port: value.port as number };
68
+ }
69
+
70
+ /** Arguments start immediately after `claude desktop profile`. */
71
+ export async function handleClaudeDesktopProfileCommand(argv: string[], deps: DesktopProfileCliDeps = {}): Promise<number> {
72
+ return runCatalogAction(async () => {
73
+ const args = [...argv], wantsJson = takeFlag(args, "--json");
74
+ const action = args.shift();
75
+ if ((action !== "show" && action !== "import") || (action === "show" && args.length !== 0)
76
+ || (action === "import" && (args.length !== 1 || !args[0]?.trim() || (args[0]!.startsWith("-") && args[0] !== "-")))) {
77
+ throw new CliUsageError("Desktop profile accepts show or import with an explicit file or '-'. Saving and applying are separate actions.", USAGE);
78
+ }
79
+ let body: string | undefined;
80
+ if (action === "import") {
81
+ const input = await readJsonInput(args[0]!, deps);
82
+ let profile: DesktopProfile;
83
+ try { profile = parseDesktopProfile(input); }
84
+ catch { throw new CliUsageError("Desktop profile input is invalid. Supply a version-1 profile with valid assignments and family defaults."); }
85
+ body = serializeManagementJson({ profile });
86
+ }
87
+ const raw = await (deps.runtimeRequestImpl ?? runtimeRequest)("/api/claude-desktop", {
88
+ method: action === "show" ? "GET" : "PUT", redirect: "error",
89
+ ...(body === undefined ? {} : { headers: { "content-type": "application/json" }, body }),
90
+ }, deps);
91
+ const state = desktopState(raw, action === "import");
92
+ printData(state, wantsJson, [
93
+ action === "import" ? "Desktop profile saved on the selected proxy. It has not been applied by this command." : "Desktop profile on the selected proxy.",
94
+ `Models: ${state.models.length}; available: ${state.models.filter(model => model.available).length}; port: ${state.port}`,
95
+ ...DESKTOP_FAMILIES.map(family => `${family}: ${state.profile.defaults[family] ?? "none"}`),
96
+ ...(action === "import" ? ["Next: review with ocx claude desktop profile show; apply separately with ocx claude desktop apply."] : []),
97
+ ]);
98
+ return 0;
99
+ });
100
+ }
@@ -321,6 +321,27 @@ export function defaultDesktopApplyMode(
321
321
  return connection.kind === "connected" ? "gateway" : "first-party";
322
322
  }
323
323
 
324
+ const DESKTOP_APPLY_FLAGS = ["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"] as const;
325
+
326
+ /**
327
+ * A rejected argument is never echoed: an option-shaped token can carry a value (`-tVALUE`,
328
+ * `--opt=VALUE`, a dash-prefixed operand) and any token can carry terminal control characters, so
329
+ * the error counts them and lists the options that do exist instead.
330
+ */
331
+ function unknownApplyArgsError(count: number): string {
332
+ return `알 수 없는 인자 ${count}개 (값은 표시하지 않습니다). 사용 가능한 옵션: ${DESKTOP_APPLY_FLAGS.join(" ")}`;
333
+ }
334
+
335
+ /** A rejected route is not echoed: whatever sits in that position may be a misplaced credential. */
336
+ const UNAVAILABLE_ROUTE = "현재 사용할 수 없는 모델입니다. 사용 가능한 모델은 ocx claude desktop show로 확인하세요.";
337
+
338
+ /** Profile files are user-chosen paths; a failure reports the operation and error code only. */
339
+ function profileFileError(operation: "export" | "import", error: unknown): Error {
340
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
341
+ const reason = typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : error instanceof SyntaxError ? "invalid JSON" : "failed";
342
+ return new Error(`Claude Desktop 프로필 ${operation === "export" ? "내보내기" : "가져오기"} 실패: ${reason}`);
343
+ }
344
+
324
345
  export function parseDesktopApplyArgs(
325
346
  flags: string[],
326
347
  config: Pick<OcxConfig, "claudeCode" | "port" | "runtimeRole">,
@@ -330,8 +351,8 @@ export function parseDesktopApplyArgs(
330
351
  const wantsFirstParty = flags.includes("--first-party");
331
352
  const wantsGateway = flags.includes("--gateway") || shapeFlags.length > 0;
332
353
  if (wantsFirstParty && wantsGateway) return { error: "--first-party cannot be combined with --gateway or gateway shape flags." };
333
- const unknown = flags.filter(arg => !["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"].includes(arg));
334
- if (unknown.length > 0) return { error: `알 수 없는 인자: ${unknown.join(" ")}` };
354
+ const unknown = flags.filter(arg => !(DESKTOP_APPLY_FLAGS as readonly string[]).includes(arg));
355
+ if (unknown.length > 0) return { error: unknownApplyArgsError(unknown.length) };
335
356
  const kind: ClaudeDesktopMode = wantsFirstParty
336
357
  ? "first-party"
337
358
  : wantsGateway ? "gateway" : defaultDesktopApplyMode(config, readClientConnectionState(), observed ?? observeClaudeDesktopMode(config));
@@ -679,6 +700,12 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
679
700
  printDesktopHelp();
680
701
  return 0;
681
702
  }
703
+ if (command === "profile") {
704
+ const { handleClaudeDesktopProfileCommand } = await import("./claude-desktop-profile");
705
+ return handleClaudeDesktopProfileCommand(argv.slice(1), {
706
+ runtimeRequestImpl: deps.runtimeRequestImpl, findLiveProxy: deps.findLiveProxyImpl,
707
+ });
708
+ }
682
709
 
683
710
  // Mode flags remain apply aliases and are parsed before subcommands.
684
711
  const applyFlags = argv.filter(arg => APPLY_FLAGS.includes(arg));
@@ -806,7 +833,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
806
833
  if (command === "move") {
807
834
  const [, route, familyRaw, ...flags] = argv;
808
835
  if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new CliUsageError("Usage: ocx claude desktop move <route> <family> [--default]");
809
- if (!state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
836
+ if (!state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
810
837
  const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
811
838
  saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
812
839
  console.log(`${route} 모델을 ${familyRaw} 그룹으로 옮겼습니다.`);
@@ -816,7 +843,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
816
843
  const [, familyRaw, routeRaw] = argv;
817
844
  if (!isFamily(familyRaw) || !routeRaw || argv.length !== 3) throw new CliUsageError("Usage: ocx claude desktop default <family> <route|none>");
818
845
  const route = routeRaw === "none" ? null : routeRaw;
819
- if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
846
+ if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(UNAVAILABLE_ROUTE);
820
847
  const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
821
848
  saveLocalDesktopProfile(profile, config.claudeCode?.desktopProfile, connection, deps);
822
849
  console.log(`${familyRaw} 기본 모델을 ${route ?? "없음"}으로 지정했습니다.`);
@@ -827,14 +854,20 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
827
854
  if (!target || argv.length !== 2) throw new CliUsageError("Usage: ocx claude desktop export <path|->");
828
855
  const json = JSON.stringify(state.profile, null, 2) + "\n";
829
856
  if (target === "-") process.stdout.write(json);
830
- else writeFileSync(resolve(target), json, { encoding: "utf8", mode: 0o600 });
857
+ else {
858
+ try { writeFileSync(resolve(target), json, { encoding: "utf8", mode: 0o600 }); }
859
+ catch (error) { throw profileFileError("export", error); }
860
+ }
831
861
  return 0;
832
862
  }
833
863
  if (command === "import") {
834
864
  const source = argv[1];
835
865
  const flags = argv.slice(2);
836
866
  if (!source || flags.some(flag => flag !== "--apply")) throw new CliUsageError("Usage: ocx claude desktop import <path> [--apply]");
837
- const profile = parseDesktopProfile(JSON.parse(readFileSync(resolve(source), "utf8")));
867
+ let raw: unknown;
868
+ try { raw = JSON.parse(readFileSync(resolve(source), "utf8")); }
869
+ catch (error) { throw profileFileError("import", error); }
870
+ const profile = parseDesktopProfile(raw);
838
871
  const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
839
872
  if (flags.includes("--apply")) assertNoClientDisconnectPending();
840
873
  if (flags.includes("--apply") && readClientConnectionState().kind !== "disconnected") {
package/src/cli/claude.ts CHANGED
@@ -7,12 +7,13 @@
7
7
  * `claude` CLI with stdio inherited. User-exported env wins except when a stale
8
8
  * loopback opencodex base URL points at a different proxy port.
9
9
  */
10
+ import { resolveSubagentForceModel } from "../claude/subagent-model";
10
11
  import { spawn } from "node:child_process";
11
12
  import { loadConfig } from "../config";
12
13
  import { injectClaudeAgentDefs } from "../claude/agents-inject";
13
14
  import { CLAUDE_ALIAS_PREFIX_CURRENT, CLAUDE_ALIAS_PREFIX_CURRENT_V2, CLAUDE_ALIAS_PREFIX_V1, CLAUDE_ALIAS_PREFIX_V2 } from "../claude/alias";
14
15
  import { claudeToolSearchEnv, effectiveModelEnv, resolveAutoContext } from "../claude/context-windows";
15
- import { claudeConfigDir, refreshGatewayModelCacheFromProxy } from "../claude/gateway-cache";
16
+ import { claudeConfigDir, fetchGatewayModels, writeGatewayModelCache } from "../claude/gateway-cache";
16
17
  import { commandInvocation } from "../lib/win-exec";
17
18
  import { isProxyAdmissionSecret } from "../server/auth-cors";
18
19
  import { findLiveProxy } from "../server/proxy-liveness";
@@ -50,6 +51,8 @@ export interface ClaudeRoutingTarget {
50
51
  * launch base so detection and the spawned process can never disagree (audit R3-3).
51
52
  */
52
53
  export type ClaudeEnvDeps = {
54
+ forceAvailable?: readonly string[];
55
+ forceAvailableSelectors?: readonly string[];
53
56
  authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
54
57
  /** Test seam; production uses the authenticated Node-launcher context. */
55
58
  preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
@@ -213,6 +216,15 @@ export function buildClaudeEnv(
213
216
  if (env[name] !== undefined && env[name] !== "") return; // user wins
214
217
  env[name] = value;
215
218
  };
219
+ if (config.claudeCode?.subagentModelForce !== undefined) {
220
+ const forced = resolveSubagentForceModel(config, contextWindows, deps.forceAvailableSelectors !== undefined ? { selectors: deps.forceAvailableSelectors } : { entries: deps.forceAvailable ?? [] });
221
+ if (forced) {
222
+ setDefault("CLAUDE_CODE_SUBAGENT_MODEL", forced);
223
+ setDefault("CLAUDE_CODE_SUBAGENT_MODEL_FORCE", "1");
224
+ } else {
225
+ (deps.warn ?? console.warn)("⚠ Claude subagent force target is invalid or unavailable; launch override skipped.");
226
+ }
227
+ }
216
228
  if (deps.allowRootSkipPermissions === true) {
217
229
  setDefault("IS_SANDBOX", "1");
218
230
  }
@@ -420,6 +432,7 @@ export function buildClaudeEnv(
420
432
  * decides to launch natively, so a wrong destination here silently downgrades every launch.
421
433
  */
422
434
  export interface ClaudeCodeLiveState {
435
+ forceAvailable?: string[];
423
436
  contextWindows: Record<string, number>;
424
437
  enabled?: boolean;
425
438
  }
@@ -434,9 +447,10 @@ export async function fetchClaudeCodeState(config: OcxConfig, port: number, time
434
447
  signal: AbortSignal.timeout(timeoutMs),
435
448
  });
436
449
  if (!res.ok) return { contextWindows: {} };
437
- const body = await res.json() as { contextWindows?: Record<string, number>; enabled?: boolean };
450
+ const body = await res.json() as { contextWindows?: Record<string, number>; enabled?: boolean; forceAvailable?: string[] };
438
451
  return {
439
452
  contextWindows: body.contextWindows && typeof body.contextWindows === "object" ? body.contextWindows : {},
453
+ ...(Array.isArray(body.forceAvailable) ? { forceAvailable: body.forceAvailable.filter(value => typeof value === "string") } : {}),
440
454
  ...(typeof body.enabled === "boolean" ? { enabled: body.enabled } : {}),
441
455
  };
442
456
  } catch {
@@ -774,6 +788,7 @@ export async function cmdClaude(args: string[]): Promise<number> {
774
788
  if (preflight.kind === "native") return launchNativeClaude(config, args, preflight.notice);
775
789
  let route: number | ClaudeRoutingTarget;
776
790
  let contextWindows: Record<string, number>;
791
+ let forceAvailable: readonly string[] | undefined;
777
792
  /** The hub's featured roster on a connected client; undefined means "use local config". */
778
793
  let hubRoster: readonly string[] | undefined;
779
794
  if (clientState.kind === "connected") {
@@ -792,18 +807,19 @@ export async function cmdClaude(args: string[]): Promise<number> {
792
807
  if (plan.kind === "native") return launchNativeClaude(config, args, plan.notice);
793
808
  route = port;
794
809
  contextWindows = liveState.contextWindows;
810
+ forceAvailable = liveState.forceAvailable ?? [];
795
811
  }
812
+ const gateway = await fetchGatewayModels(route, { admissionConfig: config });
813
+ const forceAvailableSelectors = typeof route === "number" ? undefined : gateway?.models.map(model => model.id) ?? [];
796
814
  const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
797
- const env = buildClaudeEnv(config, route, process.env, contextWindows, { allowRootSkipPermissions });
815
+ const env = buildClaudeEnv(config, route, process.env, contextWindows, { allowRootSkipPermissions, forceAvailable, forceAvailableSelectors });
798
816
  if (allowRootSkipPermissions) {
799
817
  console.error(rootSkipPermissionsNotice(env));
800
818
  }
801
819
  // Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
802
820
  // never refreshes it, so the picker would keep showing yesterday's aliases.
803
821
  try {
804
- const cachePath = typeof route === "number"
805
- ? await refreshGatewayModelCacheFromProxy(route, { admissionConfig: config })
806
- : await refreshGatewayModelCacheFromProxy(route, { admissionConfig: config });
822
+ const cachePath = gateway ? writeGatewayModelCache(gateway.baseUrl, gateway.models) : null;
807
823
  if (cachePath === null) {
808
824
  console.error("⚠ Gateway model cache could not be refreshed; the model picker may be stale.");
809
825
  }
@@ -17,6 +17,7 @@ const DEFAULT_DEPS: CodexShimAutoRestoreCliDeps = {
17
17
 
18
18
  export function skipsCodexShimAutoRestore(command: string | undefined, args: string[]): boolean {
19
19
  if (command === "uninstall" || command === "remove") return true;
20
+ if (command === "status" || command === "doctor") return true;
20
21
  // `lab` is read-only inspection; it must not trigger shim side effects.
21
22
  if (command === "lab") return true;
22
23
  // `resolve` is read-only inspection for embedding shells: a lookup made to populate
@@ -29,7 +30,7 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
29
30
  // or future actions. A later `apply` implementation must own its preflight.
30
31
  if (command === "system" && args[1] === "codex-cli-update") return true;
31
32
  if (command === "__update-badge") return true;
32
- return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
33
+ return command === "codex-shim" && ["install", "status", "uninstall", "remove"].includes(args[1] ?? "");
33
34
  }
34
35
 
35
36
  export function maybeAutoRestoreCodexShim(
@@ -3,6 +3,9 @@ import {
3
3
  getCodexRoutingKind,
4
4
  type CodexRoutingKind,
5
5
  } from "../codex/inject";
6
+ import { diagnoseCodexShim, type CodexShimDiagnostic } from "../codex/shim";
7
+ import { overlayActivationHint } from "../codex/shim-overlay";
8
+ import { findFirstCodexOnPath, type CodexPathCandidate } from "../codex/shim-path-resolution";
6
9
  import { loadConfig, resolveEnvValue } from "../config";
7
10
 
8
11
  const PROXY_ENV_KEYS = [
@@ -21,6 +24,112 @@ export interface CodexShimReadinessInputs {
21
24
  configuredProxyResolved: boolean;
22
25
  }
23
26
 
27
+ export type CodexConnectShimStatus = "ready" | "missing" | "unhealthy" | "unverified";
28
+
29
+ export interface CodexConnectShimReadiness {
30
+ status: CodexConnectShimStatus;
31
+ /** Secret-free, actionable text rendered by ocx connect. */
32
+ message: string;
33
+ }
34
+
35
+ export interface CodexConnectShimInspectionDeps {
36
+ diagnose?: () => Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
37
+ findOnPath?: () => CodexPathCandidate | null;
38
+ }
39
+
40
+ const CODEX_TOKEN_NOTE = "The connected Codex config uses OPENCODEX_API_AUTH_TOKEN; "
41
+ + "without a working shim or when a PATH wrapper replaces it, Codex may fail with "
42
+ + '"Missing environment variable".';
43
+ const CODEX_TOKEN_ACTION = `${CODEX_TOKEN_NOTE} Run 'ocx codex-shim install' to repair it.`;
44
+
45
+ export function codexConnectShimReadiness(inputs: {
46
+ diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
47
+ commandPath: string | null;
48
+ commandIsShim?: boolean;
49
+ }): CodexConnectShimReadiness {
50
+ if (inputs.diagnosis.runnable && inputs.diagnosis.active !== undefined) {
51
+ if (inputs.diagnosis.active === null) {
52
+ return {
53
+ status: "unverified",
54
+ message: `PATH activation could not be verified. Shim state: ${inputs.diagnosis.summary}. `
55
+ + `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
56
+ };
57
+ }
58
+ if (!inputs.diagnosis.active) {
59
+ return {
60
+ status: "missing",
61
+ message: `installed but not active; ${inputs.commandPath
62
+ ? `PATH resolves 'codex' to ${inputs.commandPath}`
63
+ : "no 'codex' executable was found on PATH"}. `
64
+ + `Shim state: ${inputs.diagnosis.summary}. ${overlayActivationHint()} ${CODEX_TOKEN_NOTE}`,
65
+ };
66
+ }
67
+ return { status: "ready", message: "installed and healthy" };
68
+ }
69
+ if (inputs.diagnosis.installed && !inputs.diagnosis.healthy) {
70
+ return {
71
+ status: "unhealthy",
72
+ message: `installed but unhealthy: ${inputs.diagnosis.summary}. ${CODEX_TOKEN_ACTION}`,
73
+ };
74
+ }
75
+ if (inputs.diagnosis.healthy && inputs.commandPath && !inputs.commandIsShim) {
76
+ // The shim is installed and healthy, so reinstalling it cannot fix this: only PATH
77
+ // order (or the wrapper in front of it) can.
78
+ return {
79
+ status: "missing",
80
+ message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. `
81
+ + `Shim state: ${inputs.diagnosis.summary}. Put the directory of the tracked shim ahead of that command on PATH, `
82
+ + `or remove that wrapper; re-running 'ocx codex-shim install' will not change PATH order. ${CODEX_TOKEN_NOTE}`,
83
+ };
84
+ }
85
+ if (inputs.diagnosis.installed && inputs.diagnosis.healthy) {
86
+ if (!inputs.commandPath) {
87
+ return {
88
+ status: "missing",
89
+ message: `installed but not active; no 'codex' executable was found on PATH. `
90
+ + `Shim state: ${inputs.diagnosis.summary}. Add the directory of the tracked shim to PATH. ${CODEX_TOKEN_NOTE}`,
91
+ };
92
+ }
93
+ return { status: "ready", message: "installed and healthy" };
94
+ }
95
+ if (inputs.commandPath) {
96
+ return {
97
+ status: "missing",
98
+ message: `not active; PATH resolves 'codex' to ${inputs.commandPath}, not an OpenCodex shim. ${CODEX_TOKEN_ACTION}`,
99
+ };
100
+ }
101
+ return {
102
+ status: "missing",
103
+ message: `not installed and no 'codex' executable was found on PATH. ${CODEX_TOKEN_ACTION}`,
104
+ };
105
+ }
106
+
107
+ export function inspectCodexShimForConnect(
108
+ deps: CodexConnectShimInspectionDeps = {},
109
+ ): CodexConnectShimReadiness {
110
+ let diagnosis: Pick<CodexShimDiagnostic, "installed" | "healthy" | "summary" | "runnable" | "active">;
111
+ try {
112
+ diagnosis = (deps.diagnose ?? diagnoseCodexShim)();
113
+ } catch {
114
+ diagnosis = { installed: true, healthy: false, summary: "diagnostic state could not be read" };
115
+ }
116
+ let command: CodexPathCandidate | null = null;
117
+ try {
118
+ command = (deps.findOnPath ?? findFirstCodexOnPath)();
119
+ } catch {
120
+ return {
121
+ status: "unverified",
122
+ message: `PATH activation could not be verified. Shim state: ${diagnosis.summary}. `
123
+ + `Check PATH and retry 'ocx connect'. ${CODEX_TOKEN_NOTE}`,
124
+ };
125
+ }
126
+ return codexConnectShimReadiness({
127
+ diagnosis,
128
+ commandPath: command?.path ?? null,
129
+ commandIsShim: command?.isShim ?? false,
130
+ });
131
+ }
132
+
24
133
  function externalProviderLabel(provider: string | null): string {
25
134
  return provider
26
135
  ? `external model_provider ${JSON.stringify(provider)}`