@iislee/opencodex 2.31.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -9,6 +9,7 @@
9
9
  * never needs to import the entry module back (no cycle).
10
10
  */
11
11
  import { CLI_COMMANDS } from "./registry";
12
+ import { isValidProviderName } from "../config/provider-name";
12
13
  import type { CliHead } from "./root";
13
14
  import type { ReadyArgs } from "./ready";
14
15
  import type { LiveProxy } from "../server/proxy-liveness";
@@ -21,6 +22,7 @@ import { restoreNativeCodexAsync } from "../codex/inject";
21
22
  import { stripGrokConfig } from "../grok/inject";
22
23
  import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
23
24
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
25
+ import { isJsonOption, takeFlag } from "./runtime-api";
24
26
 
25
27
  export interface CliDispatchDeps {
26
28
  args: string[];
@@ -69,33 +71,37 @@ const commandRunners: Record<string, CommandRunner> = {
69
71
  return Number(process.exitCode ?? 0);
70
72
  },
71
73
  restore: async deps => {
72
- const restoreJson = deps.args[1] === "--json";
73
- if (deps.args[1] === "back") {
74
+ const restoreArgs = deps.args.slice(1);
75
+ const restoreJson = takeFlag(restoreArgs, "--json");
76
+ if (restoreArgs[0] === "back") {
74
77
  // Reverse switch: re-point plain `codex` at the RUNNING proxy without touching its
75
78
  // lifecycle — the counterpart of `ocx restore`. Start/stop triggers are unchanged;
76
79
  // this only re-runs the same inject (config + catalog + history) `ocx start` does.
80
+ // takeFlag above makes `ocx restore --json back` restore-back, not eject.
81
+ const { skippedRestoreEnvelope } = await import("../codex/inject");
82
+ const emitBack = (success: boolean, message: string, code: number): number => {
83
+ if (restoreJson) console.log(JSON.stringify(skippedRestoreEnvelope(success, message)));
84
+ else if (code === 0) console.log(message);
85
+ else console.error(message);
86
+ return code;
87
+ };
77
88
  const live = await deps.findLiveProxy();
78
89
  if (!live) {
79
- console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
80
- return 1;
90
+ return emitBack(false, "No running proxy found. Run 'ocx start' — it injects opencodex automatically.", 1);
81
91
  }
82
92
  const desired = setIntegrationEnabled("codex", true);
83
93
  if (!desired.ok) {
84
- console.error(`Codex desired state was not saved (${desired.reason}).`);
85
- return desired.reason === "conflict" ? 2 : 1;
94
+ return emitBack(false, `Codex desired state was not saved (${desired.reason}).`, desired.reason === "conflict" ? 2 : 1);
86
95
  }
87
96
  const synced = await syncModelsToCodex(live.port);
88
97
  if (synced.status === "skipped") {
89
- console.error("Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.");
90
- return 2;
98
+ return emitBack(false, "Codex integration is OFF; restore back did not change Codex. Retry after the competing integration change finishes.", 2);
91
99
  }
92
100
  if (!synced.ok) {
93
- console.error("Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.");
94
- return 1;
101
+ return emitBack(false, "Plain `codex` was not switched back to opencodex. Fix the reported Codex config issue and retry.", 1);
95
102
  }
96
103
  const target = collectOrcaCodexHomeDiagnostic();
97
- console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
98
- return 0;
104
+ return emitBack(true, `Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`, 0);
99
105
  }
100
106
  const desired = setIntegrationEnabled("codex", false);
101
107
  if (!desired.ok) {
@@ -172,14 +178,35 @@ const commandRunners: Record<string, CommandRunner> = {
172
178
  },
173
179
  doctor: async deps => {
174
180
  const doctorArgs = deps.args.slice(1);
175
- const { RECOVER_ZERO_BYTE_COORDINATOR_FLAG, runDoctor } = await import("./doctor");
181
+ // `--json` was silently ignored here: runDoctor scans for its own flags and prints human
182
+ // output regardless, so a caller that asked for JSON got prose and exit 0 -- and the skill
183
+ // recipes recommended exactly that invocation. Refusing it is worse than supporting it and
184
+ // better than lying about it.
185
+ //
186
+ // Not implemented rather than deferred silently: runDoctor has no report collection at all
187
+ // (a module-level failure bit plus ~90 direct console emissions), and this runner appends
188
+ // the Codex Log Guard's human output after it returns, so emitting a JSON document here
189
+ // would interleave prose with JSON on one stdout -- unparseable, which is worse than the
190
+ // ignored flag. The structured-report refactor is tracked as its own work-phase.
191
+ if (doctorArgs.some(isJsonOption)) {
192
+ console.error("ocx doctor does not support --json yet. Run `ocx doctor` for the human report, or use `ocx status --json` and `ocx ready --json` for machine-readable health.");
193
+ return 2;
194
+ }
195
+ const { RECOVER_ZERO_BYTE_COORDINATOR_FLAG, runDoctor, doctorFailed } = await import("./doctor");
176
196
  await runDoctor(doctorArgs);
177
197
  if (!doctorArgs.includes("--fix-codex-runtime") && !doctorArgs.includes(RECOVER_ZERO_BYTE_COORDINATOR_FLAG)) {
178
198
  console.log("");
179
199
  const { printCodexLogGuardDoctor } = await import("./codex-log-guard-doctor");
180
200
  printCodexLogGuardDoctor();
181
201
  }
182
- return 0;
202
+ // A diagnostic that always exits 0 cannot gate a script. `runDoctor` reports by direct
203
+ // console.log with no checks collection, and signals its own special-flag failures
204
+ // through process.exitCode, so honour both: an explicit exitCode wins, otherwise a
205
+ // FAIL-level check fails the command. This is a BREAKING change for pipelines that ran
206
+ // `ocx doctor` and ignored the result; a diagnostic that cannot fail is worse.
207
+ const explicit = Number(process.exitCode ?? 0);
208
+ if (explicit !== 0) return explicit;
209
+ return doctorFailed() ? 1 : 0;
183
210
  },
184
211
  debug: async deps => {
185
212
  const { handleDebugCommand } = await import("./debug");
@@ -196,14 +223,69 @@ const commandRunners: Record<string, CommandRunner> = {
196
223
  return 0;
197
224
  },
198
225
  logout: async deps => {
226
+ // Argv is parsed BEFORE any store access, which is the whole point of this shape.
227
+ // Previously `args[1]` was taken as the provider name with no parsing, so
228
+ // `ocx logout --json` called removeCredential("--json"), printed "Logged out of
229
+ // --json." and exited 0 -- a silent false success, the worst outcome for a caller
230
+ // that can only see the exit code.
231
+ //
232
+ // That is not merely a wasted call. `normalizeAuthStore` copies every top-level key
233
+ // it finds, so a hand-edited, legacy, or corrupted auth.json containing a `--json`
234
+ // key would have its active account deleted -- and the key dropped entirely if that
235
+ // was its last account. A flag must never reach the store as a provider name.
236
+ const logoutArgs = deps.args.slice(1);
237
+ const wantsJson = logoutArgs.includes("--json");
238
+ // Any leading dash is an option, not a provider. Matching only `--` left the same defect
239
+ // one dash shorter: `ocx logout -j` treated `-j` as the provider name and, with a `-j` key
240
+ // present in the store, deleted it and exited 0.
241
+ const isOption = (arg: string): boolean => arg.startsWith("-");
242
+ const positionals = logoutArgs.filter(arg => !isOption(arg));
243
+ const unknownFlags = logoutArgs.filter(arg => isOption(arg) && arg !== "--json");
244
+ const name = (positionals[0] ?? "").trim().toLowerCase();
245
+
246
+ // Usage failures exit 2 and touch nothing. A missing provider is a usage error; a
247
+ // provider that simply has no credential is a not-found (4) further down, because the
248
+ // vocabulary distinguishes "you called this wrong" from "the thing is not there".
249
+ //
250
+ // The shape check is `isValidProviderName`, not another dash test. Rejecting a leading
251
+ // ASCII `-` fixed `-j` and still let `logout —json` through with a Unicode dash, which is
252
+ // the same defect a third time: each patch named one spelling instead of the class. The
253
+ // canonical validator states the rule positively -- start and end alphanumeric, internal
254
+ // `._-` allowed -- so `github-copilot` and `google-antigravity` pass while every dash
255
+ // variant, empty string, and reserved name fails. Anything that is not a possible
256
+ // provider id cannot reach the store at all.
257
+ const malformedName = Boolean(name) && !isValidProviderName(name);
258
+ if (unknownFlags.length > 0 || positionals.length > 1 || !name || malformedName) {
259
+ const problem = unknownFlags.length > 0
260
+ ? `unknown option ${unknownFlags[0]}`
261
+ : positionals.length > 1 ? "too many arguments"
262
+ : malformedName ? `not a valid provider name: ${name}`
263
+ : "missing provider";
264
+ console.error(`Usage: ocx logout <provider> [--json] (${problem})`);
265
+ return 2;
266
+ }
267
+
268
+ // The disposition comes from inside the store mutation, not from a read-then-remove
269
+ // preflight. `mutateStore` serializes writes, so a preflight leaves a window where a
270
+ // concurrent logout removes the same account and BOTH callers exit 0 claiming a removal --
271
+ // a false success again, just a narrower one than the flag bug above.
199
272
  const { removeCredential } = await import("../oauth/store");
200
- const name = (deps.args[1] ?? "").trim().toLowerCase();
201
- await removeCredential(name);
202
- console.log(`Logged out of ${name || "(none)"}.`);
273
+ const outcome = await removeCredential(name);
274
+ if (outcome === "not-found") {
275
+ if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: false, provider: name, removed: false, reason: "not_found" }, null, 2));
276
+ else console.error(`No stored credential for '${name}'.`);
277
+ return 4;
278
+ }
279
+ if (wantsJson) console.log(JSON.stringify({ schemaVersion: 1, ok: true, provider: name, removed: true }, null, 2));
280
+ else console.log(`Logged out of ${name}.`);
203
281
  return 0;
204
282
  },
205
283
  sync: async deps => {
206
- const restartCodex = deps.args.slice(1).includes("--restart-codex");
284
+ const syncArgs = deps.args.slice(1);
285
+ const restartCodex = syncArgs.includes("--restart-codex");
286
+ // Separate flag on purpose: --restart-codex promises app-server-only scope,
287
+ // and quitting the desktop app ends live conversations.
288
+ const restartDesktopApp = syncArgs.includes("--restart-desktop-app");
207
289
  const live = await deps.findLiveProxy();
208
290
  const synced = await syncModelsToCodex(
209
291
  live?.port,
@@ -229,6 +311,7 @@ const commandRunners: Record<string, CommandRunner> = {
229
311
  // exactly when a long-lived app-server is holding the stale list.
230
312
  if (synced.catalogWritten || synced.cacheSynced) {
231
313
  afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
314
+ if (restartDesktopApp) await handleDesktopAppRestart(console);
232
315
  }
233
316
  // `ocx sync` is a direct CLI path; it does not call the management
234
317
  // `/api/sync` route. Refresh the already-connected MCode block here too,
@@ -259,21 +342,82 @@ const commandRunners: Record<string, CommandRunner> = {
259
342
  return await cmdV2(deps.args.slice(1), {}, async () => (await deps.findLiveProxy())?.port);
260
343
  },
261
344
  "sync-cache": async deps => {
262
- const restartCodex = deps.args.slice(1).includes("--restart-codex");
345
+ const cacheArgs = deps.args.slice(1);
346
+ const restartCodex = cacheArgs.includes("--restart-codex");
347
+ const restartDesktopApp = cacheArgs.includes("--restart-desktop-app");
263
348
  const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
264
349
  const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
265
350
  const { getCodexHome } = await import("../codex/paths");
351
+ const { readCodexCatalogPathForHome } = await import("../codex/catalog/parsing");
352
+ const { existsSync } = await import("node:fs");
266
353
  const owningCodexHome = getCodexHome();
267
354
  const desiredDisabled = !shouldSyncCodexOnStart(deps.loadConfig());
268
355
  const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
269
356
  invalidateCodexModelsCacheWithPermit(permit, owningCodexHome, { allowWhenDesiredDisabled: true }));
357
+ const cacheJson = cacheArgs.includes("--json");
358
+ const jsonSafeLog = cacheJson
359
+ ? { log: (...values: unknown[]) => console.error(...values), error: (...values: unknown[]) => console.error(...values) }
360
+ : console;
270
361
  // Only warn/restart when models_cache was actually rewritten from a readable catalog.
271
362
  if (invalidated.kind === "completed" && invalidated.value) {
272
- afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
273
- } else if (desiredDisabled) {
274
- console.log("Codex integration is OFF; cache sync skipped (no catalog or cache write).");
363
+ afterCatalogWriteHandleAppServers({ restart: restartCodex, log: jsonSafeLog });
364
+ if (restartDesktopApp) await handleDesktopAppRestart(jsonSafeLog);
365
+ } else if (desiredDisabled && !cacheJson) {
366
+ // Worth saying in the human path, because it explains why nothing was written.
367
+ // Under --json this belongs on the envelope, not as a second stdout line.
368
+ console.log("Codex integration is OFF; no catalog or cache write resulted.");
275
369
  }
276
- return 0;
370
+ // `completed` with a falsy value means the cache was NOT rewritten. Previously every
371
+ // outcome exited 0, so a script could not tell a refreshed cache from a skipped one.
372
+ //
373
+ // Losing the catalog write lock to another process is a skip, not a failure:
374
+ // serialization working as designed is the expected outcome under concurrency, and a
375
+ // proxy startup holding the permit would otherwise make a perfectly healthy
376
+ // `ocx sync-cache` exit 1 and fail the pipeline that called it -- intermittently, so it
377
+ // would read as a flake rather than a bug. `codex-retained-root-serialization.test.ts`
378
+ // pins exactly that: contended lock, no cache write, exit 0.
379
+ //
380
+ // `desiredDisabled` is deliberately NOT part of the success test, which is the subtle
381
+ // part. This call passes `allowWhenDesiredDisabled: true`, so the OFF gate inside the
382
+ // refresh never fires and the work is genuinely attempted -- an explicit `ocx sync-cache`
383
+ // means the user asked for it regardless of the toggle. Treating OFF as automatic success
384
+ // would report exit 0 and `skipped: true` for a refresh that actually failed.
385
+ //
386
+ // But `invalidateCodexModelsCacheWithPermit` returns a bare boolean for four different
387
+ // situations -- wrote it, no catalog file exists, the OFF gate fired, or it threw -- so
388
+ // `false` alone cannot be read as failure either. `!existsSync(catalogPath)` is a
389
+ // legitimate nothing-to-do: with no catalog there is no cache to derive, which is the
390
+ // normal state of a fully native home and the case
391
+ // `codex-composed-acceptance.test.ts` pins at exit 0. It is checked here rather than by
392
+ // widening that function's return type, because its boolean is consumed by a dozen
393
+ // management routes that have no use for the distinction.
394
+ const wrote = invalidated.kind === "completed" && Boolean(invalidated.value);
395
+ const contended = invalidated.kind === "unavailable" && invalidated.reason === "busy";
396
+ const noCatalog = !wrote && !existsSync(readCodexCatalogPathForHome(owningCodexHome));
397
+ const ok = wrote || contended || noCatalog;
398
+ if (cacheJson) {
399
+ console.log(JSON.stringify({
400
+ schemaVersion: 1,
401
+ ok,
402
+ wrote,
403
+ skipped: contended || noCatalog,
404
+ outcome: invalidated.kind,
405
+ // `outcome` alone cannot separate a contended lock from a hard serialization
406
+ // failure -- both are `unavailable`. Carry the reason so a caller can.
407
+ reason: invalidated.kind === "unavailable" ? invalidated.reason : undefined,
408
+ // Which of the two benign skips this was, so `skipped: true` is never opaque.
409
+ skippedReason: contended ? "contended" : noCatalog ? "no_catalog" : undefined,
410
+ desiredDisabled,
411
+ codexHome: owningCodexHome,
412
+ }, null, 2));
413
+ } else if (contended) {
414
+ console.log("Another process owns the catalog write; cache sync skipped.");
415
+ } else if (noCatalog) {
416
+ console.log("No Codex catalog to derive a cache from; nothing to sync.");
417
+ } else if (!ok) {
418
+ console.error(`Cache refresh did not complete (${invalidated.kind}). The Codex model cache was not rewritten.`);
419
+ }
420
+ return ok ? 0 : 1;
277
421
  },
278
422
  gui: async deps => {
279
423
  const config = deps.loadConfig();
@@ -307,8 +451,12 @@ const commandRunners: Record<string, CommandRunner> = {
307
451
  },
308
452
  tray: async deps => {
309
453
  const { windowsTrayCommand } = await import("../tray/windows");
454
+ // windowsTrayCommand reports failure through process.exitCode (tray/windows.ts sets
455
+ // it for bad usage and for a failed install/start/stop/uninstall) and returns void,
456
+ // so a literal 0 here made `ocx tray install` print an error and exit 0 (#2697).
457
+ process.exitCode = 0;
310
458
  await windowsTrayCommand(deps.args.slice(1));
311
- return 0;
459
+ return Number(process.exitCode ?? 0);
312
460
  },
313
461
  "codex-shim": async deps => {
314
462
  const { codexShimStatus, diagnoseCodexShim, installCodexShim, uninstallCodexShim } = await import("../codex/shim");
@@ -387,6 +535,10 @@ const commandRunners: Record<string, CommandRunner> = {
387
535
  await deps.handleProxyRestart(deps.handleRestartStartWhenStopped);
388
536
  return Number(process.exitCode ?? 0);
389
537
  },
538
+ capabilities: async deps => {
539
+ const { runCapabilities } = await import("./capabilities-command");
540
+ return await runCapabilities(deps.args.slice(1));
541
+ },
390
542
  health: async deps => {
391
543
  const healthArgs = deps.args.slice(1);
392
544
  const wantsHealthJson = healthArgs.includes("--json");
@@ -410,8 +562,15 @@ const commandRunners: Record<string, CommandRunner> = {
410
562
  },
411
563
  provider: async deps => {
412
564
  const { handleProviderCommand } = await import("./provider");
565
+ // Reset first, like the service runner below: reading process.exitCode only
566
+ // reports THIS command's outcome if nothing earlier in the process set it.
567
+ process.exitCode = 0;
413
568
  await handleProviderCommand(deps.args.slice(1));
414
- return 0;
569
+ // handleProviderCommand reports failure through process.exitCode, which it sets
570
+ // from handleProviderRuntimeCommand. Returning a literal 0 here made index.ts
571
+ // call process.exit(0) and erase it, so `ocx provider quota` against a stopped
572
+ // proxy printed an error and still exited 0 (#2697).
573
+ return Number(process.exitCode ?? 0);
415
574
  },
416
575
  account: async deps => {
417
576
  const { cmdAccount } = await import("./account");
@@ -419,8 +578,15 @@ const commandRunners: Record<string, CommandRunner> = {
419
578
  },
420
579
  models: async deps => {
421
580
  const { handleModels } = await import("./models");
581
+ process.exitCode = 0;
422
582
  await handleModels(deps.args.slice(1));
423
- return 0;
583
+ // Same as the provider runner above: handleModels sets process.exitCode from
584
+ // handleModelsRuntimeCommand, and a literal 0 discarded it (#2697).
585
+ return Number(process.exitCode ?? 0);
586
+ },
587
+ alias: async deps => {
588
+ const { handleAliasCommand } = await import("./alias");
589
+ return await handleAliasCommand(deps.args.slice(1));
424
590
  },
425
591
  combo: async deps => {
426
592
  const { handleComboCommand } = await import("./combo");
@@ -447,6 +613,10 @@ const commandRunners: Record<string, CommandRunner> = {
447
613
  const { handleObserveCommand } = await import("./observe");
448
614
  return await handleObserveCommand(deps.args.slice(1));
449
615
  },
616
+ inspect: async deps => {
617
+ const { handleInspectCommand } = await import("./inspect");
618
+ return await handleInspectCommand(deps.args.slice(1));
619
+ },
450
620
  logs: async deps => {
451
621
  const { handleObserveCommand } = await import("./observe");
452
622
  return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
@@ -456,8 +626,12 @@ const commandRunners: Record<string, CommandRunner> = {
456
626
  return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
457
627
  },
458
628
  storage: async deps => {
459
- const { handleObserveCommand } = await import("./observe");
460
- return await handleObserveCommand([deps.command!, ...deps.args.slice(1)]);
629
+ // `ocx storage` used to be a pure alias of `observe storage`, which reached only the report
630
+ // route. wp7 gave it cleanup, trash, and policy subcommands, so it dispatches to its own
631
+ // module -- with `report` as the default subcommand, so a bare `ocx storage` still prints
632
+ // the same thing it printed before.
633
+ const { handleStorageCommand } = await import("./storage");
634
+ return await handleStorageCommand(deps.args.slice(1));
461
635
  },
462
636
  memory: async deps => {
463
637
  const { handleObserveCommand } = await import("./observe");
@@ -484,6 +658,12 @@ const commandRunners: Record<string, CommandRunner> = {
484
658
  if (integration === "grok") {
485
659
  const { handleGrokCommand } = await import("./integrations");
486
660
  return await handleGrokCommand(deps.args.slice(2));
661
+ } else if (integration === "native") {
662
+ // The native client toggles are a separate server surface from the reversible file
663
+ // integrations `client` manages, so they get their own subcommand rather than being
664
+ // folded into one that means something else.
665
+ const { handleIntegrationCommand } = await import("./inspect");
666
+ return await handleIntegrationCommand(deps.args.slice(1));
487
667
  } else if (integration === "claude") {
488
668
  const { handleClaudeConfigCommand } = await import("./integrations");
489
669
  return await handleClaudeConfigCommand(deps.args.slice(2));
@@ -591,3 +771,51 @@ export async function dispatchCommand(head: CliHead, deps: CliDispatchDeps): Pro
591
771
  }
592
772
  return await runner(deps);
593
773
  }
774
+
775
+ /**
776
+ * Report the outcome of an opt-in desktop-app restart. Kept next to the two
777
+ * callers so `sync` and `sync-cache` cannot drift in what they tell the user.
778
+ */
779
+ async function handleDesktopAppRestart(log: Pick<Console, "log" | "error">): Promise<void> {
780
+ const { restartCodexDesktopApp } = await import("../codex/desktop-app-restart");
781
+ const result = restartCodexDesktopApp();
782
+ switch (result.reason) {
783
+ case "windows_only":
784
+ log.error("--restart-desktop-app is supported on Windows only; nothing was stopped.");
785
+ return;
786
+ case "package_discovery_failed":
787
+ log.error(
788
+ "Could not identify the installed Codex desktop package. Quit and relaunch the desktop app "
789
+ + "manually to refresh the model picker.",
790
+ );
791
+ return;
792
+ case "self_ancestry":
793
+ log.error(
794
+ "Refusing to restart the desktop app because this command is running inside it. "
795
+ + "Run 'ocx sync --restart-desktop-app' from an external terminal instead.",
796
+ );
797
+ return;
798
+ case "process_probe_failed":
799
+ // Distinct from `no_targets`: we could not look, which is not the same as looking and
800
+ // finding nothing. Saying "not running" here sent users away believing there was nothing
801
+ // to restart (#2557).
802
+ log.error(
803
+ "Could not enumerate Codex desktop processes, so the app was not restarted. "
804
+ + "Quit and relaunch the desktop app manually to refresh the model picker.",
805
+ );
806
+ return;
807
+ case "no_targets":
808
+ log.log("Codex desktop app is not running; nothing to restart.");
809
+ return;
810
+ case "targets_survived":
811
+ log.error(
812
+ `Codex desktop app PID(s) ${result.surviving.join(", ")} did not exit, so it was not relaunched. `
813
+ + "Quit the desktop app manually to refresh the model picker.",
814
+ );
815
+ return;
816
+ default:
817
+ if (result.relaunch === "started") {
818
+ log.log("Codex desktop app restarted; its model picker will re-read the catalog.");
819
+ }
820
+ }
821
+ }