@bitkyc08/opencodex 2.60.0 → 2.61.0-preview.20260922

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 (249) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +5 -1
  13. package/src/adapters/anthropic.ts +16 -0
  14. package/src/adapters/coding-agent/protocol.ts +36 -6
  15. package/src/adapters/coding-agent/turn.ts +10 -2
  16. package/src/adapters/command-code.ts +2 -1
  17. package/src/adapters/cursor/catalog.ts +51 -7
  18. package/src/adapters/cursor/protobuf-request.ts +6 -3
  19. package/src/adapters/cursor/request-builder.ts +13 -3
  20. package/src/adapters/cursor.ts +11 -2
  21. package/src/adapters/declaration-carrier.ts +45 -0
  22. package/src/adapters/devin.ts +75 -23
  23. package/src/adapters/google-antigravity-wire.ts +5 -2
  24. package/src/adapters/google-errors.ts +7 -1
  25. package/src/adapters/google.ts +29 -5
  26. package/src/adapters/image.ts +4 -1
  27. package/src/adapters/input-media-guard.ts +21 -9
  28. package/src/adapters/kiro/usage.ts +3 -2
  29. package/src/adapters/kiro-tool-fallback.ts +1 -1
  30. package/src/adapters/ollama-native.ts +6 -0
  31. package/src/adapters/openai-chat/developer-role.ts +61 -0
  32. package/src/adapters/openai-chat/messages.ts +46 -27
  33. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  34. package/src/adapters/openai-chat/passthrough.ts +33 -9
  35. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  36. package/src/adapters/openai-chat.ts +18 -57
  37. package/src/adapters/openai-responses/passthrough.ts +2 -0
  38. package/src/adapters/registry.ts +3 -2
  39. package/src/adapters/run-turn-queue.ts +178 -29
  40. package/src/adapters/xai-web-search.ts +16 -1
  41. package/src/bridge/errors.ts +8 -2
  42. package/src/bridge/response-json.ts +9 -1
  43. package/src/bridge/sse.ts +10 -0
  44. package/src/chat/inbound.ts +141 -5
  45. package/src/claude/desktop-3p.ts +7 -1
  46. package/src/claude/desktop-first-party.ts +183 -0
  47. package/src/claude/desktop-gateway-state.ts +41 -0
  48. package/src/claude/inbound-content-options.ts +6 -0
  49. package/src/claude/inbound.ts +32 -6
  50. package/src/claude/intercept/connect-proxy.ts +179 -0
  51. package/src/claude/intercept/listener.ts +122 -0
  52. package/src/claude/intercept/local-ca.ts +298 -0
  53. package/src/claude/intercept/runtime.ts +98 -0
  54. package/src/claude/intercept/settings.ts +189 -0
  55. package/src/cli/access.ts +87 -0
  56. package/src/cli/account-auth.ts +19 -0
  57. package/src/cli/capabilities.ts +31 -0
  58. package/src/cli/claude-desktop.ts +206 -16
  59. package/src/cli/codex-shim-autorestore.ts +3 -0
  60. package/src/cli/companion.ts +56 -0
  61. package/src/cli/dispatch.ts +43 -4
  62. package/src/cli/ensure-desired-integrations.ts +43 -5
  63. package/src/cli/help.ts +7 -9
  64. package/src/cli/index.ts +200 -61
  65. package/src/cli/init.ts +8 -0
  66. package/src/cli/integrations.ts +7 -1
  67. package/src/cli/registry.ts +41 -2
  68. package/src/cli/resolve.ts +230 -0
  69. package/src/cli/root.ts +24 -1
  70. package/src/cli/start-ownership-publication.ts +56 -0
  71. package/src/cli/status-probes.ts +2 -18
  72. package/src/cli/status.ts +62 -0
  73. package/src/cli/stop-report.ts +143 -0
  74. package/src/cli/uninstall-plan.ts +9 -0
  75. package/src/client/machine-listener.ts +2 -5
  76. package/src/clients/aside-profiles.ts +4 -0
  77. package/src/clients/config-export/zcode-store.ts +157 -0
  78. package/src/clients/config-export.ts +36 -0
  79. package/src/codex/app-server-processes.ts +72 -40
  80. package/src/codex/auth-api/login-flow.ts +6 -1
  81. package/src/codex/autostart-health.ts +28 -0
  82. package/src/codex/catalog/build-entries.ts +2 -2
  83. package/src/codex/catalog/effort.ts +3 -3
  84. package/src/codex/catalog/provider-models.ts +24 -15
  85. package/src/codex/catalog/retained-sync.ts +2 -2
  86. package/src/codex/convergence.ts +2 -2
  87. package/src/codex/history-provider.ts +12 -1
  88. package/src/codex/inject/config-toml.ts +41 -6
  89. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  90. package/src/codex/inject.ts +18 -15
  91. package/src/codex/injected-marker.ts +18 -0
  92. package/src/codex/main-account.ts +6 -0
  93. package/src/codex/model-cache.ts +52 -6
  94. package/src/codex/model-entitlement-admission.ts +59 -0
  95. package/src/codex/model-entitlements.ts +87 -44
  96. package/src/codex/native-main-admission.ts +83 -0
  97. package/src/codex/routing/health-store.ts +39 -0
  98. package/src/codex/routing/selection.ts +37 -1
  99. package/src/codex/routing.ts +5 -41
  100. package/src/codex/shim-templates.ts +29 -3
  101. package/src/companion/settings.ts +132 -0
  102. package/src/config/atomic-write.ts +117 -5
  103. package/src/config/load-degrade.ts +34 -7
  104. package/src/config/process-state.ts +1 -1
  105. package/src/config/schema/config-schema.ts +27 -1
  106. package/src/config/schema/leaf-validators.ts +47 -0
  107. package/src/config.ts +1 -1
  108. package/src/generated/compatibility-version.json +418 -174
  109. package/src/integrations/config-io.ts +44 -10
  110. package/src/integrations/merge.ts +120 -13
  111. package/src/integrations/mutation-plan.ts +124 -18
  112. package/src/integrations/registry.ts +38 -0
  113. package/src/integrations/state.ts +78 -45
  114. package/src/integrations/target.ts +208 -0
  115. package/src/integrations/writer.ts +49 -11
  116. package/src/lab/conformance/fixture-provider.ts +5 -0
  117. package/src/lib/browser-launch-notice.ts +59 -0
  118. package/src/lib/bun-runtime.ts +6 -2
  119. package/src/lib/debug.ts +40 -0
  120. package/src/lib/open-url.ts +51 -7
  121. package/src/lib/package-tree-integrity.ts +2 -1
  122. package/src/lib/package-version.ts +8 -0
  123. package/src/lib/provider-egress.ts +310 -0
  124. package/src/lib/provider-outbound.ts +59 -14
  125. package/src/lib/proxy-env.ts +82 -7
  126. package/src/lib/request-execution-budget.ts +72 -0
  127. package/src/lib/request-failure-attribution.ts +183 -0
  128. package/src/lib/request-failure-model.ts +236 -0
  129. package/src/lib/request-resend-gate.ts +138 -0
  130. package/src/lib/standalone.ts +16 -0
  131. package/src/lib/upstream-retry.ts +167 -16
  132. package/src/lib/winsw.ts +2 -2
  133. package/src/oauth/index.ts +24 -1
  134. package/src/oauth/login-cli.ts +80 -29
  135. package/src/providers/api-key-resolve.ts +133 -0
  136. package/src/providers/api-key-selection.ts +5 -1
  137. package/src/providers/key-failover.ts +31 -1
  138. package/src/providers/key-store.ts +34 -110
  139. package/src/providers/model-rename-fields.ts +147 -0
  140. package/src/providers/model-rename-migration.ts +124 -37
  141. package/src/providers/quota/vendor-probes-key.ts +37 -22
  142. package/src/providers/reasoning-metadata.ts +43 -18
  143. package/src/providers/registry/entries-core.ts +9 -4
  144. package/src/providers/registry/entries-extended.ts +29 -4
  145. package/src/providers/registry/model-seeds.ts +47 -10
  146. package/src/providers/xai-transport.ts +12 -1
  147. package/src/reasoning-effort.ts +8 -0
  148. package/src/responses/function-call-compat.ts +38 -1
  149. package/src/responses/inline-document.ts +65 -0
  150. package/src/responses/input-media.ts +42 -8
  151. package/src/responses/muse-tool-name-alias.ts +19 -0
  152. package/src/responses/parser-content.ts +8 -2
  153. package/src/responses/parser-tools.ts +3 -0
  154. package/src/responses/parser.ts +3 -1
  155. package/src/responses/schema.ts +3 -0
  156. package/src/router.ts +17 -2
  157. package/src/server/admission-model-scope.ts +219 -0
  158. package/src/server/audio-live.ts +9 -3
  159. package/src/server/audio-upstream.ts +18 -0
  160. package/src/server/auth-cors.ts +26 -0
  161. package/src/server/chat-completions.ts +55 -2
  162. package/src/server/chat-native.ts +19 -4
  163. package/src/server/claude-messages.ts +55 -17
  164. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  165. package/src/server/gui-freshness.ts +103 -0
  166. package/src/server/gui-static.ts +7 -9
  167. package/src/server/images.ts +59 -6
  168. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  169. package/src/server/index/serve-options.ts +56 -10
  170. package/src/server/index/spend-ledger-lifecycle.ts +34 -8
  171. package/src/server/index/startup-warnings.ts +24 -0
  172. package/src/server/index.ts +21 -28
  173. package/src/server/lifecycle.ts +4 -4
  174. package/src/server/live-call-bindings.ts +6 -0
  175. package/src/server/live.ts +88 -3
  176. package/src/server/management/agent-settings-routes.ts +121 -36
  177. package/src/server/management/companion-routes.ts +77 -0
  178. package/src/server/management/logs-usage-routes.ts +19 -0
  179. package/src/server/management/native-integration-routes.ts +103 -6
  180. package/src/server/management/oauth-account-routes.ts +45 -7
  181. package/src/server/management/route-registry.ts +6 -0
  182. package/src/server/management/shared.ts +18 -1
  183. package/src/server/management/usage-timeline-routes.ts +44 -0
  184. package/src/server/management-api.ts +8 -9
  185. package/src/server/proxy-liveness.ts +75 -0
  186. package/src/server/relay.ts +19 -2
  187. package/src/server/request-log-failure-attribution.ts +99 -0
  188. package/src/server/request-log.ts +114 -0
  189. package/src/server/request-metrics.ts +92 -30
  190. package/src/server/responses/codex-ws-wire.ts +34 -8
  191. package/src/server/responses/combo-stream-preflight.ts +168 -6
  192. package/src/server/responses/compact.ts +11 -0
  193. package/src/server/responses/core-opaque-recovery.ts +90 -0
  194. package/src/server/responses/fetch-helpers.ts +124 -8
  195. package/src/server/responses/input-admission.ts +10 -0
  196. package/src/server/responses/passthrough-delivery.ts +14 -1
  197. package/src/server/responses/passthrough-dispatch.ts +179 -35
  198. package/src/server/responses/passthrough-error.ts +27 -8
  199. package/src/server/responses/request-prepare.ts +42 -1
  200. package/src/server/responses/request-send-budget.ts +12 -0
  201. package/src/server/responses/request-transport.ts +24 -4
  202. package/src/server/responses/reset-replay.ts +108 -0
  203. package/src/server/responses-request-tool-scope.ts +214 -0
  204. package/src/server/responses-undeclared-tool-guard.ts +4 -1
  205. package/src/server/search.ts +25 -1
  206. package/src/server/usage-ledger-retention.ts +73 -0
  207. package/src/service/cli.ts +48 -2
  208. package/src/service/health.ts +3 -2
  209. package/src/service/install-state-contract.d.mts +27 -0
  210. package/src/service/install-state-contract.mjs +34 -0
  211. package/src/service/launchd.ts +1 -1
  212. package/src/service/orchestration.ts +2 -4
  213. package/src/service/ownership-compatibility.ts +164 -0
  214. package/src/service/ownership-mutation-lease.d.mts +32 -0
  215. package/src/service/ownership-mutation-lease.mjs +211 -0
  216. package/src/service/repair.ts +45 -1
  217. package/src/service/state-lock.ts +269 -0
  218. package/src/service/state-record.d.mts +36 -0
  219. package/src/service/state-record.mjs +138 -0
  220. package/src/service/state.ts +582 -68
  221. package/src/service/windows-taskxml.ts +11 -10
  222. package/src/service.ts +7 -3
  223. package/src/tray/windows-tray.ps1 +1 -1
  224. package/src/types/config.ts +37 -0
  225. package/src/types/provider.ts +73 -0
  226. package/src/types/request.ts +28 -2
  227. package/src/types/tools.ts +19 -0
  228. package/src/types.ts +3 -0
  229. package/src/update/index.ts +207 -63
  230. package/src/update/job.ts +9 -5
  231. package/src/update/ownership-transaction.ts +47 -0
  232. package/src/update/restart-ownership.ts +54 -0
  233. package/src/update/runtime-ownership.d.mts +40 -0
  234. package/src/update/runtime-ownership.mjs +122 -0
  235. package/src/usage/attempt-delivery.ts +198 -0
  236. package/src/usage/cache-diagnostic.ts +305 -0
  237. package/src/usage/failure-fingerprint.ts +118 -0
  238. package/src/usage/failure-projection-cache.ts +174 -0
  239. package/src/usage/failure-projection.ts +174 -0
  240. package/src/usage/ledger-retention.ts +165 -0
  241. package/src/usage/log.ts +126 -79
  242. package/src/usage/request-outcome.ts +150 -0
  243. package/src/usage/retention-contract.ts +28 -0
  244. package/src/usage/summary.ts +2 -2
  245. package/src/usage/telemetry-contract.ts +237 -0
  246. package/src/usage/timeline.ts +236 -0
  247. package/src/web-search/alpha-search.ts +21 -1
  248. package/gui/dist/assets/index-BTuCbqQd.css +0 -1
  249. package/gui/dist/assets/index-DoBVdPHP.js +0 -134
@@ -1,3 +1,4 @@
1
+ import { recordCommittedDesktopGateway } from "../claude/desktop-gateway-state";
1
2
  import { readFileSync, writeFileSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { loadConfig, mutatePersistedConfig, withConfigMutationLockSync } from "../config";
@@ -15,13 +16,25 @@ import {
15
16
  type DesktopFamily,
16
17
  type DesktopProfile,
17
18
  } from "../claude/desktop-profile";
18
- import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p";
19
+ import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs } from "../claude/desktop-3p";
20
+ import {
21
+ applyDesktopFirstParty,
22
+ captureDesktopFirstPartyRollback,
23
+ isClaudeDesktopMode,
24
+ recordClaudeDesktopMode,
25
+ removeDesktopFirstParty,
26
+ resolveClaudeDesktopApplyMode,
27
+ type ClaudeDesktopMode,
28
+ } from "../claude/desktop-first-party";
19
29
  import { claudeDesktopPolicyWarning, probeClaudeDesktopPolicy } from "../claude/desktop-policy";
20
30
  import { filterCatalogVisibleModels, desktopVisibleNativeSlugs, nativeContextLimits } from "../codex/catalog";
21
31
  import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
22
32
  import { findLiveProxy } from "../server/proxy-liveness";
23
33
  import { CliUsageError, runtimeRequest, takeJsonFlag } from "./runtime-api";
24
34
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
35
+ import type { OcxConfig } from "../types";
36
+
37
+ const APPLY_FLAGS = ["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"];
25
38
 
26
39
  function isFamily(value: string | undefined): value is DesktopFamily {
27
40
  return !!value && (DESKTOP_FAMILIES as readonly string[]).includes(value);
@@ -29,7 +42,10 @@ function isFamily(value: string | undefined): value is DesktopFamily {
29
42
 
30
43
  function printDesktopHelp(): void {
31
44
  console.log(`Usage:
32
- ocx claude desktop [apply] [--static|--hybrid|--discovery-only]
45
+ 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
33
49
  ocx claude desktop show [--json]
34
50
  ocx claude desktop status [--json]
35
51
  ocx claude desktop move <provider/model> <opus|fable|sonnet|haiku> [--default]
@@ -56,6 +72,7 @@ function saveLocalDesktopProfile(
56
72
  expectedProfile: DesktopProfile | undefined,
57
73
  expectedConnection: ClientConnectionState,
58
74
  deps: ApplyProfileDeps,
75
+ gatewayWrite?: { fingerprint?: string },
59
76
  ): void {
60
77
  withClientLifecycleSync(() => {
61
78
  const outcome = mutatePersistedConfig(current => {
@@ -73,6 +90,10 @@ function saveLocalDesktopProfile(
73
90
  if (JSON.stringify(current.claudeCode?.desktopProfile) !== JSON.stringify(expectedProfile)) {
74
91
  throw new Error("desktop_profile_changed");
75
92
  }
93
+ if (gatewayWrite) {
94
+ recordCommittedDesktopGateway(current, profile, gatewayWrite.fingerprint, new Date().toISOString());
95
+ return { changed: true, value: undefined };
96
+ }
76
97
  const changed = JSON.stringify(current.claudeCode?.desktopProfile) !== JSON.stringify(profile);
77
98
  if (changed) current.claudeCode = { ...(current.claudeCode ?? {}), desktopProfile: structuredClone(profile) };
78
99
  return { changed, value: undefined };
@@ -135,6 +156,151 @@ async function applyConnectedDesktopProfile(
135
156
  }
136
157
  }
137
158
 
159
+ /** Persist the operator's Desktop mode choice; a failed marker save never undoes the apply. */
160
+ function saveDesktopMode(mode: ClaudeDesktopMode, deps: ApplyProfileDeps): boolean {
161
+ try {
162
+ return withClientLifecycleSync(() => {
163
+ const outcome = mutatePersistedConfig(current => recordClaudeDesktopMode(current, mode));
164
+ return outcome.status !== "unavailable";
165
+ }, deps.lifecycleLockDeps);
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ export type DesktopApplyTarget =
172
+ | { kind: "first-party" }
173
+ | { kind: "gateway"; mode: Desktop3pConfigMode };
174
+
175
+ /** Parse `ocx claude desktop apply` flags into a target; legacy gateway shape flags imply --gateway. */
176
+ /**
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.
180
+ */
181
+ export function defaultDesktopApplyMode(
182
+ config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
183
+ connection: ClientConnectionState = readClientConnectionState(),
184
+ ): ClaudeDesktopMode {
185
+ const resolved = resolveClaudeDesktopApplyMode(config);
186
+ if (resolved === "gateway") return resolved;
187
+ return connection.kind === "connected" ? "gateway" : "first-party";
188
+ }
189
+
190
+ export function parseDesktopApplyArgs(
191
+ flags: string[],
192
+ config: Pick<OcxConfig, "claudeCode" | "runtimeRole">,
193
+ ): { target: DesktopApplyTarget } | { error: string } {
194
+ const shapeFlags = flags.filter(arg => ["--static", "--hybrid", "--discovery-only"].includes(arg));
195
+ const wantsFirstParty = flags.includes("--first-party");
196
+ const wantsGateway = flags.includes("--gateway") || shapeFlags.length > 0;
197
+ if (wantsFirstParty && wantsGateway) return { error: "--first-party cannot be combined with --gateway or gateway shape flags." };
198
+ const unknown = flags.filter(arg => !["--first-party", "--gateway", "--static", "--hybrid", "--discovery-only"].includes(arg));
199
+ if (unknown.length > 0) return { error: `알 수 없는 인자: ${unknown.join(" ")}` };
200
+ const kind: ClaudeDesktopMode = wantsFirstParty ? "first-party" : wantsGateway ? "gateway" : defaultDesktopApplyMode(config);
201
+ if (kind === "first-party") return { target: { kind } };
202
+ const parsedMode = parseDesktop3pModeArgs(shapeFlags);
203
+ if ("error" in parsedMode) return parsedMode;
204
+ return { target: { kind, mode: parsedMode.mode } };
205
+ }
206
+
207
+ /**
208
+ * Why a gateway apply happened when the help text calls first-party the default.
209
+ *
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.
220
+ */
221
+ export function gatewayModeExplanation(input: {
222
+ requestedExplicitly: boolean;
223
+ config: Pick<OcxConfig, "claudeCode" | "runtimeRole">;
224
+ connection?: ClientConnectionState;
225
+ }): string[] {
226
+ if (input.requestedExplicitly) return [];
227
+ const connection = input.connection ?? readClientConnectionState();
228
+ 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.
231
+ const savedMode = input.config.claudeCode?.desktopMode;
232
+ const hasSavedGateway = isClaudeDesktopMode(savedMode) && savedMode === "gateway";
233
+ const hasApplyMarker = input.config.claudeCode?.desktopProfile?.appliedFingerprint !== undefined;
234
+ if (!hasSavedGateway && !hasApplyMarker) return [];
235
+ const reason = hasSavedGateway
236
+ ? "this machine has claudeCode.desktopMode saved as gateway"
237
+ : "this machine carries a previous gateway apply";
238
+ return [
239
+ `Applied the gateway profile because ${reason}; an existing install is never switched for you.`,
240
+ "First-party keeps Desktop on your claude.ai account and routes only the Code tab through the local proxy:",
241
+ " ocx claude desktop apply --first-party",
242
+ ];
243
+ }
244
+
245
+ /**
246
+ * First-party apply: settings.json env only. The intercept pair the env points at runs inside
247
+ * the hub process, so this is a local-hub operation — a connected client machine cannot reach
248
+ * a loopback proxy on the hub and is refused rather than left with a dead `HTTPS_PROXY`.
249
+ */
250
+ async function applyFirstPartyDesktop(
251
+ deps: ApplyProfileDeps,
252
+ ): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
253
+ try { assertNoClientDisconnectPending(); } catch { return { ok: false, path: "", reason: "client_disconnect_pending" }; }
254
+ const connection = readClientConnectionState();
255
+ if (connection.kind === "connected") return { ok: false, path: "", reason: "first_party_requires_local_hub" };
256
+ if (connection.kind !== "disconnected") return { ok: false, path: "", reason: "client_connection_invalid" };
257
+ const config = loadConfig();
258
+ const desired = setIntegrationEnabled("claude-desktop", true);
259
+ if (!desired.ok) return { ok: false, path: "", reason: desired.message };
260
+ // Establish the replacement before deleting the working gateway. A refused
261
+ // cleanup restores only our managed env keys, preserving unrelated settings.
262
+ const rollback = captureDesktopFirstPartyRollback(config);
263
+ const applied = applyDesktopFirstParty(config);
264
+ if (!applied.ok) return { ok: false, path: applied.path, reason: applied.reason };
265
+ const appliedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
266
+ const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
267
+ if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
268
+ const removed = removeDesktop3pStandardPivot({ appliedFingerprint, replaceWhileEnabled: true });
269
+ if (!removed.ok) {
270
+ const restored = removed.changed || !applied.changed || rollback();
271
+ const modeSaved = !removed.changed || saveDesktopMode("first-party", deps);
272
+ const warning = [restored ? "" : "first-party settings rollback did not complete",
273
+ modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
274
+ return { ok: false, path: library.selectedProfilePath ?? "", reason: removed.kind === "cleanup_incomplete" ? "gateway_cleanup_incomplete" : `gateway_profile_active:${removed.reason ?? removed.kind}`,
275
+ ...(warning ? { warning } : {}) };
276
+ }
277
+ }
278
+ const saved = saveDesktopMode("first-party", deps);
279
+ return {
280
+ ok: true,
281
+ path: applied.path,
282
+ ...(saved ? {} : { warning: "desktop mode marker was not saved" }),
283
+ };
284
+ }
285
+
286
+ export async function applyDesktop(
287
+ profile: DesktopProfile | undefined,
288
+ target: DesktopApplyTarget,
289
+ deps: ApplyProfileDeps = {},
290
+ ): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
291
+ if (target.kind === "first-party") return applyFirstPartyDesktop(deps);
292
+ const result = await applyProfile(profile, target.mode, deps);
293
+ if (!result.ok) return result;
294
+ const modeSaved = saveDesktopMode("gateway", deps);
295
+ const warning = [result.warning, modeSaved ? "" : "desktop mode marker was not saved"].filter(Boolean).join(" ");
296
+ // The gateway mode is committed before retiring first-party settings.
297
+ const removed = removeDesktopFirstParty();
298
+ if (!removed.ok) return { ok: false, path: removed.path, reason: "first_party_settings_unreadable",
299
+ warning: ["gateway applied; first-party cleanup remains incomplete", warning].filter(Boolean).join(" ") };
300
+ if (warning) return { ...result, warning };
301
+ return result;
302
+ }
303
+
138
304
  export async function applyProfile(
139
305
  profile: DesktopProfile | undefined,
140
306
  mode: Desktop3pConfigMode,
@@ -205,8 +371,13 @@ export async function applyProfile(
205
371
  nativeContextLimits(config),
206
372
  deps.lifecycleLockDeps,
207
373
  );
374
+ let stateWarning: string | undefined;
375
+ if (result.written) {
376
+ try { saveLocalDesktopProfile(state.profile, state.profile, connection, deps, { fingerprint: result.fingerprint }); }
377
+ catch { stateWarning = "gateway applied but its committed mode/profile state was not saved"; }
378
+ }
208
379
  const policyState = (deps.probeClaudeDesktopPolicy ?? probeClaudeDesktopPolicy)();
209
- const warning = result.written ? claudeDesktopPolicyWarning(policyState) : undefined;
380
+ const warning = [result.written ? claudeDesktopPolicyWarning(policyState) : undefined, stateWarning].filter(Boolean).join(" ");
210
381
  return {
211
382
  ok: result.written,
212
383
  path: result.path,
@@ -222,24 +393,42 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
222
393
  return 0;
223
394
  }
224
395
 
225
- // Legacy mode flags remain apply aliases and are parsed before subcommands.
226
- const legacyFlags = argv.filter(arg => ["--static", "--hybrid", "--discovery-only"].includes(arg));
227
- const applyInvocation = argv.length === 0 || command === "apply" || legacyFlags.length > 0;
396
+ // Mode flags remain apply aliases and are parsed before subcommands.
397
+ const applyFlags = argv.filter(arg => APPLY_FLAGS.includes(arg));
398
+ const applyInvocation = argv.length === 0 || command === "apply" || applyFlags.length > 0;
228
399
  if (applyInvocation) {
229
- const nonMode = argv.filter(arg => !["apply", "--static", "--hybrid", "--discovery-only"].includes(arg));
230
- if (nonMode.length > 0) {
231
- console.error(`알 수 없는 인자: ${nonMode.join(" ")}`);
232
- return 2;
233
- }
234
- const parsedMode = parseDesktop3pModeArgs(legacyFlags);
235
- if ("error" in parsedMode) { console.error(parsedMode.error); return 2; }
400
+ const rest = argv.filter(arg => arg !== "apply");
401
+ const parsedTarget = parseDesktopApplyArgs(rest, loadConfig());
402
+ if ("error" in parsedTarget) { console.error(parsedTarget.error); return 2; }
403
+ const { target } = parsedTarget;
236
404
  try {
237
- const result = await applyProfile(undefined, parsedMode.mode, deps);
405
+ const result = await applyDesktop(undefined, target, deps);
238
406
  if (!result.ok) {
239
407
  console.error(`설정 적용 실패: ${result.reason ?? "unknown error"}`);
408
+ if (result.warning) console.warn(result.warning);
409
+ if (result.reason?.startsWith("gateway_")) {
410
+ console.error("The gateway profile could not be removed safely, so first-party mode was not applied. Turn the integration off (dashboard toggle) and retry, or keep gateway with `ocx claude desktop apply --gateway`.");
411
+ } else if (result.reason === "foreign_env") {
412
+ console.error(`~/.claude/settings.json already sets HTTPS_PROXY or NODE_EXTRA_CA_CERTS to a value opencodex does not own (${result.path}). Remove them or use --gateway.`);
413
+ } else if (result.reason === "intercept_disabled") {
414
+ console.error("First-party mode needs the Claude intercept proxy (claudeCode.intercept.enabled on a hub). Use --gateway instead.");
415
+ } else if (result.reason === "first_party_requires_local_hub") {
416
+ console.error("First-party mode runs on the hub machine only; on a connected client use --gateway.");
417
+ }
240
418
  return 1;
241
419
  }
242
- console.log(`Claude Desktop 설정을 적용했습니다: ${result.path}`);
420
+ if (target.kind === "first-party") {
421
+ console.log(`Claude Desktop first-party 설정을 적용했습니다: ${result.path}`);
422
+ console.log("Desktop 앱 설정은 그대로이며, Code 탭의 Claude Code만 로컬 프록시를 거칩니다.");
423
+ } else {
424
+ console.log(`Claude Desktop gateway 설정을 적용했습니다: ${result.path}`);
425
+ for (const line of gatewayModeExplanation({
426
+ requestedExplicitly: applyFlags.some(flag => flag !== "--first-party"),
427
+ config: loadConfig(),
428
+ })) {
429
+ console.log(line);
430
+ }
431
+ }
243
432
  // The write landed; only the bookkeeping marker did not. Saying nothing
244
433
  // would leave the saved-vs-applied display wrong with no explanation.
245
434
  if (result.warning) console.warn(`⚠️ ${result.warning}`);
@@ -336,7 +525,8 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
336
525
  }
337
526
  saveLocalDesktopProfile(reconciled, config.claudeCode?.desktopProfile, connection, deps);
338
527
  if (flags.includes("--apply")) {
339
- const result = await applyProfile(reconciled, "static", deps);
528
+ // A routing profile is a gateway-mode artifact: importing with --apply installs it there.
529
+ const result = await applyDesktop(reconciled, { kind: "gateway", mode: "static" }, deps);
340
530
  if (!result.ok) { console.error(`프로필은 저장했지만 Desktop 적용에 실패했습니다: ${result.reason ?? "unknown error"}`); return 1; }
341
531
  if (result.warning) console.warn(`⚠️ ${result.warning}`);
342
532
  }
@@ -19,6 +19,9 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
19
19
  if (command === "uninstall" || command === "remove") return true;
20
20
  // `lab` is read-only inspection; it must not trigger shim side effects.
21
21
  if (command === "lab") return true;
22
+ // `resolve` is read-only inspection for embedding shells: a lookup made to populate
23
+ // a consent surface must not trigger a shim repair side effect first.
24
+ if (command === "resolve") return true;
22
25
  // The entire updater-inspection namespace is zero-effect, including malformed
23
26
  // or future actions. A later `apply` implementation must own its preflight.
24
27
  if (command === "system" && args[1] === "codex-cli-update") return true;
@@ -0,0 +1,56 @@
1
+ import { CliUsageError, printData, rejectArgs, runCliAction, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
2
+
3
+ const USAGE = `Usage:
4
+ ocx companion [show] [--json]
5
+ ocx companion set <key>=<value> [...] [--json]
6
+ ocx companion reset [--json]`;
7
+
8
+ function parseValue(raw: string): unknown {
9
+ if (raw === "null") return null;
10
+ try { return JSON.parse(raw); } catch { return raw; }
11
+ }
12
+
13
+ async function show(argv: string[], deps: RuntimeApiDeps): Promise<void> {
14
+ const args = [...argv];
15
+ const wantsJson = takeFlag(args, "--json");
16
+ rejectArgs(args, USAGE);
17
+ printData(await runtimeRequest("/api/companion/settings", {}, deps), wantsJson);
18
+ }
19
+
20
+ async function set(argv: string[], deps: RuntimeApiDeps): Promise<void> {
21
+ const args = [...argv];
22
+ const wantsJson = takeFlag(args, "--json");
23
+ if (args.length === 0) throw new CliUsageError("companion set requires key=value assignments", USAGE);
24
+ const patch: Record<string, unknown> = {};
25
+ for (const assignment of args) {
26
+ const separator = assignment.indexOf("=");
27
+ if (separator <= 0) throw new CliUsageError(`invalid companion setting "${assignment}"; use key=value`, USAGE);
28
+ patch[assignment.slice(0, separator)] = parseValue(assignment.slice(separator + 1));
29
+ }
30
+ printData(await runtimeRequest("/api/companion/settings", {
31
+ method: "PUT",
32
+ body: JSON.stringify({ settings: patch }),
33
+ }, deps), wantsJson, ["Companion settings saved."]);
34
+ }
35
+
36
+ async function reset(argv: string[], deps: RuntimeApiDeps): Promise<void> {
37
+ const args = [...argv];
38
+ const wantsJson = takeFlag(args, "--json");
39
+ rejectArgs(args, USAGE);
40
+ printData(await runtimeRequest("/api/companion/settings", {
41
+ method: "PUT",
42
+ body: JSON.stringify({ reset: true }),
43
+ }, deps), wantsJson, ["Companion settings reset."]);
44
+ }
45
+
46
+ export async function handleCompanionCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
47
+ return runCliAction(async () => {
48
+ const [sub = "show", ...rest] = argv;
49
+ if (sub === "show") await show(rest, deps);
50
+ else if (sub === "set") await set(rest, deps);
51
+ else if (sub === "reset") await reset(rest, deps);
52
+ else throw new CliUsageError(`unknown companion command ${sub}`, USAGE);
53
+ });
54
+ }
55
+
56
+ export const COMPANION_USAGE = USAGE;
@@ -29,6 +29,8 @@ import { stripGrokConfig } from "../grok/inject";
29
29
  import { handleRestartScopeAfterWrite, readRestartScope, type RestartScope } from "./restart-scope";
30
30
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
31
31
  import { isJsonOption, takeFlag, terminalSafeError } from "./runtime-api";
32
+ import { printStopSummary, type StopOutcome } from "./stop-report";
33
+ import type { ResolveArgs } from "./resolve";
32
34
  import type { ClientConnectionState } from "../client/state";
33
35
  import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
34
36
 
@@ -44,8 +46,9 @@ export interface CliDispatchDeps {
44
46
  /** Spawn a detached proxy child (stdio ignore, unref'd, provenance env). */
45
47
  spawnDetached: (argv: readonly string[]) => void;
46
48
  handleStart: () => Promise<void>;
47
- handleStop: () => Promise<boolean>;
49
+ handleStop: () => Promise<StopOutcome>;
48
50
  handleEnsure: (options?: { existingIsSuccess?: boolean }) => Promise<boolean>;
51
+ handleResolve: (args: ResolveArgs) => Promise<number>;
49
52
  handleTrayProxyStart: (existingIsSuccess?: boolean) => Promise<boolean>;
50
53
  handleTrayProxyRestart: () => Promise<void>;
51
54
  handleRestartStartWhenStopped: () => Promise<boolean | "skipped">;
@@ -98,11 +101,39 @@ const commandRunners: Record<string, CommandRunner> = {
98
101
  stop: async deps => {
99
102
  // Downtime warning lives HERE, not in handleStop: `restart`/tray-restart callers
100
103
  // re-start the proxy immediately, so warning there would contradict the next line.
101
- if (await deps.handleStop()) {
102
- console.log("⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').");
104
+ const warning = "⚠️ Codex/Claude requests through the proxy will fail until it is restarted ('ocx start' or 'ocx service start').";
105
+ if (!takeFlag(deps.args.slice(1), "--json")) {
106
+ // handleStop returns the structured outcome now; an object is always truthy, so
107
+ // the warning must key on .ok — otherwise a failed stop would still claim downtime.
108
+ if ((await deps.handleStop()).ok) console.log(warning);
109
+ return Number(process.exitCode ?? 0);
103
110
  }
111
+ // --json is a reporting layer over the SAME stop path: the receipt, the drain, the
112
+ // respawn verification and the client-config restore run unchanged. Human output
113
+ // still prints, but on stderr, so stdout carries exactly one JSON summary document.
114
+ // The exit code (0/1/79/80) crosses the process boundary untouched — the shell reads
115
+ // it from the child, and the stop-contract codes must survive the JSON mode.
116
+ const humanLog = console.log;
117
+ console.log = console.error;
118
+ let outcome: StopOutcome | undefined;
119
+ try {
120
+ outcome = await deps.handleStop();
121
+ if (outcome.ok) console.log(warning);
122
+ } finally {
123
+ console.log = humanLog;
124
+ }
125
+ // A throw above propagates after the finally restores the console, so reaching here
126
+ // with an undefined outcome cannot happen; the guard keeps the assignment provable.
127
+ if (outcome) printStopSummary(outcome.summary);
104
128
  return Number(process.exitCode ?? 0);
105
129
  },
130
+ resolve: async deps => {
131
+ // Same fail-closed shape as `ready`: parseCliHead pre-parsed the verb before any
132
+ // preflight side effect, so a missing resolveArgs means dispatch diverged. Refuse
133
+ // with code 64 and perform NO I/O.
134
+ if (!deps.head.resolveArgs) return 64;
135
+ return await deps.handleResolve(deps.head.resolveArgs);
136
+ },
106
137
  restore: async deps => {
107
138
  const restoreArgs = deps.args.slice(1);
108
139
  const restoreJson = takeFlag(restoreArgs, "--json");
@@ -615,7 +646,11 @@ const commandRunners: Record<string, CommandRunner> = {
615
646
  const guiUrl = selectDefaultGuiUrl(config, live, deps.probeHostname);
616
647
  console.log(`Opening ${guiUrl}`);
617
648
  const { openUrl } = await import("../lib/open-url");
618
- openUrl(guiUrl);
649
+ // Awaited so a launcher that never opened anything is said out loud (#5261). Still exit
650
+ // 0: the proxy is serving and the URL above is reachable, only the launch did not happen.
651
+ if ((await openUrl(guiUrl)).status === "failed") {
652
+ console.error("⚠️ No browser could be opened here; open the URL above yourself.");
653
+ }
619
654
  return 0;
620
655
  },
621
656
  });
@@ -782,6 +817,10 @@ const commandRunners: Record<string, CommandRunner> = {
782
817
  const { handleComboCommand } = await import("./combo");
783
818
  return await handleComboCommand(deps.args.slice(1));
784
819
  },
820
+ companion: async deps => {
821
+ const { handleCompanionCommand } = await import("./companion");
822
+ return await handleCompanionCommand(deps.args.slice(1));
823
+ },
785
824
  route: async deps => {
786
825
  if (deps.args[1] !== "combo" && deps.args[1] !== "policy") {
787
826
  console.error("Usage: ocx route <combo|policy> <subcommand>");
@@ -10,7 +10,13 @@
10
10
  */
11
11
  import { loadConfig } from "../config";
12
12
  import { stripGrokConfig, type GrokInjectResult } from "../grok/inject";
13
- import { removeDesktop3pStandardPivot } from "../claude/desktop-3p";
13
+ import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot } from "../claude/desktop-3p";
14
+ import {
15
+ applyDesktopFirstParty,
16
+ inspectDesktopFirstParty,
17
+ removeDesktopFirstParty,
18
+ resolveClaudeDesktopMode,
19
+ } from "../claude/desktop-first-party";
14
20
  import {
15
21
  claudeDesktopIntegrationEnabled,
16
22
  grokIntegrationEnabled,
@@ -35,6 +41,10 @@ export interface EnsureDesiredIntegrationsDeps {
35
41
  opts?: { hostname?: string },
36
42
  ) => Promise<GrokInjectResult>;
37
43
  removeDesktop3pStandardPivot: typeof removeDesktop3pStandardPivot;
44
+ removeDesktopFirstParty?: typeof removeDesktopFirstParty;
45
+ applyDesktopFirstParty?: typeof applyDesktopFirstParty;
46
+ inspectDesktopFirstParty?: typeof inspectDesktopFirstParty;
47
+ inspectDesktop3pConfigLibrary?: typeof inspectDesktop3pConfigLibrary;
38
48
  log?: (message: string) => void;
39
49
  error?: (message: string) => void;
40
50
  }
@@ -113,16 +123,44 @@ export async function ensureGrokFenceMatchesDesired(
113
123
  }
114
124
 
115
125
  /**
116
- * When Claude Desktop is durably OFF, clear any leftover owned gateway profile.
117
- * ensure/update used to leave Claude-3p residue in place after a failed disable
118
- * (drifted fingerprint), so the Integrations card kept looking applied/stale.
126
+ * When Claude Desktop is durably OFF, clear any leftover owned gateway profile and the
127
+ * first-party settings env. ensure/update used to leave Claude-3p residue in place after a
128
+ * failed disable (drifted fingerprint), so the Integrations card kept looking applied/stale.
129
+ *
130
+ * When it is ON in first-party mode, refresh a stale env (the intercept port follows the
131
+ * public port, so a port change would otherwise leave Claude Code pointed at a dead proxy).
119
132
  */
120
133
  export function ensureClaudeDesktopMatchesDesired(
121
134
  deps: EnsureDesiredIntegrationsDeps = productionDeps,
122
135
  ): void {
123
136
  const config = deps.loadConfig();
124
137
  const { log, error } = io(deps);
125
- if (claudeDesktopIntegrationEnabled(config)) return;
138
+ if (claudeDesktopIntegrationEnabled(config)) {
139
+ if (resolveClaudeDesktopMode(config) !== "first-party") return;
140
+ const library = (deps.inspectDesktop3pConfigLibrary ?? inspectDesktop3pConfigLibrary)({
141
+ appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
142
+ });
143
+ if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
144
+ // The mode marker and the disk disagree. Replacing a live Desktop profile is an operator
145
+ // action, not something an update hook should do silently.
146
+ error("⚠️ Claude Desktop mode is first-party but a gateway profile is still applied; run `ocx claude desktop apply --first-party` (or `--gateway`) to reconcile.");
147
+ return;
148
+ }
149
+ const seen = (deps.inspectDesktopFirstParty ?? inspectDesktopFirstParty)(config);
150
+ if (!seen.stale) return;
151
+ const applied = (deps.applyDesktopFirstParty ?? applyDesktopFirstParty)(config);
152
+ if (applied.ok && applied.changed) log(` + Claude Desktop first-party env refreshed (${applied.path})`);
153
+ else if (!applied.ok) error(`⚠️ Claude Desktop first-party env refresh skipped: ${applied.reason}.`);
154
+ return;
155
+ }
156
+ try {
157
+ const env = (deps.removeDesktopFirstParty ?? removeDesktopFirstParty)();
158
+ if (env.ok && env.changed) log(" ↩️ Claude Desktop first-party env removed.");
159
+ else if (!env.ok) error(`⚠️ Claude Desktop first-party env cleanup skipped: ${env.reason} (${env.path}).`);
160
+ } catch (err) {
161
+ const detail = err instanceof Error ? err.message : String(err);
162
+ error(`⚠️ Claude Desktop first-party env cleanup failed: ${detail}.`);
163
+ }
126
164
  try {
127
165
  const removed = deps.removeDesktop3pStandardPivot({
128
166
  appliedFingerprint: config.claudeCode?.desktopProfile?.appliedFingerprint ?? null,
package/src/cli/help.ts CHANGED
@@ -1,9 +1,5 @@
1
- import { readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
1
  import { findCommand } from "./registry";
5
-
6
- const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
2
+ import { packageVersion as readPackageVersion } from "../lib/package-version";
7
3
 
8
4
  /**
9
5
  * Version of the `ocx` bundle this process is running from.
@@ -13,9 +9,7 @@ const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta
13
9
  * rather than throwing; callers must treat that as "cannot compare", not as a mismatch.
14
10
  */
15
11
  export function packageVersion(): string {
16
- const raw = readFileSync(join(repoRoot, "package.json"), "utf8");
17
- const parsed = JSON.parse(raw) as { version?: unknown };
18
- return typeof parsed.version === "string" ? parsed.version : "unknown";
12
+ return readPackageVersion();
19
13
  }
20
14
 
21
15
  export function printVersion(): void {
@@ -29,9 +23,11 @@ Usage:
29
23
  ocx setup Interactive setup (alias: init)
30
24
  ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]
31
25
  Start the proxy; SOCKS5 defaults to 127.0.0.1:10808
32
- ocx stop Stop the proxy AND restore native Codex (plain codex works again)
26
+ ocx stop [--json] Stop the proxy AND restore native Codex (plain codex works again)
33
27
  ocx restore Restore native Codex without stopping (alias: eject)
34
28
  ocx restore back Re-point codex at the running proxy (undo restore)
29
+ ocx restore --remove-codex-provider-table
30
+ Also drop [model_providers.opencodex] that a paginated restore kept
35
31
  ocx recover-history --legacy-openai --yes
36
32
  Force all user-message opencodex rows to OpenAI (legacy recovery)
37
33
  ocx recover-history --ocx-compaction <thread-id> --yes
@@ -67,6 +63,7 @@ Usage:
67
63
  ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
68
64
  ocx capabilities [--json] List declared capabilities and the API routes they drive
69
65
  ocx ready [--json] [--wait [--timeout <s>]] Check post-sync readiness (exit 0 only when ready)
66
+ ocx resolve [--json] Config home, effective port, and liveness (JSON for shells)
70
67
  ocx provider <sub> Providers, connectivity, quota, and selected models
71
68
  ocx account <sub> Accounts, login/reauth, key pools, and quota controls
72
69
  ocx models <sub> Live/custom models, visibility, context, and shadow calls
@@ -89,6 +86,7 @@ Usage:
89
86
  ocx grok <sub> Grok Build model selection and apply
90
87
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
91
88
  ocx config <sub> Validated configuration show/get/set/import/export
89
+ ocx companion <show|set|reset> Menu-bar and widget companion usage settings
92
90
  ocx lab <sub> Read-only Compatibility Lab projection inspection
93
91
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
94
92
  ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile