@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
@@ -22,6 +22,10 @@ const USAGE = `Usage:
22
22
  ocx models <enable|disable> <provider/model|native-model> [--native] [--json]
23
23
  ocx models provider <name> <on|off> [--json]
24
24
  ocx models selected <provider> [--set <id,id...>|--clear] [--json]
25
+ ocx models preset show [--provider <name>] [--json]
26
+ ocx models preset apply <provider> [--all] [--json]
27
+ ocx models new-policy [on|off] [--provider <name>] [--json]
28
+ ocx models new-arrivals [--json]
25
29
  ocx models context <status|value <tokens> [--set-all]|provider <name> on [--value <tokens>]|provider <name> off|all <on|off>> [--json]
26
30
  ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]`;
27
31
 
@@ -162,6 +166,94 @@ async function selected(argv: string[], deps: RuntimeApiDeps): Promise<void> {
162
166
  printData(result, wantsJson, [`${provider}: ${models.length ? models.join(", ") : "all models"}`]);
163
167
  }
164
168
 
169
+
170
+ interface ModelPresetView {
171
+ mode: string;
172
+ appliedVersion?: number;
173
+ availableVersion: number;
174
+ presetIds: string[];
175
+ presetCount: number;
176
+ totalCount: number;
177
+ fallback?: string;
178
+ }
179
+
180
+ function presetLine(name: string, view: ModelPresetView): string {
181
+ const parts = [`${name}: mode=${view.mode}`];
182
+ if (view.appliedVersion !== undefined && view.appliedVersion !== view.availableVersion) {
183
+ parts.push(`applied v${view.appliedVersion}, available v${view.availableVersion}`);
184
+ } else {
185
+ parts.push(`preset v${view.availableVersion}`);
186
+ }
187
+ parts.push(`(${view.presetCount} of ${view.totalCount} models)`);
188
+ if (view.fallback) parts.push(`fallback=${view.fallback}`);
189
+ return parts.join(" ");
190
+ }
191
+
192
+ async function preset(argv: string[], deps: RuntimeApiDeps): Promise<void> {
193
+ const args = [...argv];
194
+ const action = (args.shift() ?? "show").toLowerCase();
195
+ const wantsJson = takeFlag(args, "--json");
196
+ if (action === "show") {
197
+ const only = takeOption(args, "--provider")?.trim();
198
+ rejectArgs(args, USAGE);
199
+ const result = await runtimeRequest<{ providers?: Record<string, ModelPresetView> }>("/api/model-presets", {}, deps);
200
+ const providers = result.providers ?? {};
201
+ const entries = Object.entries(providers).filter(([name]) => !only || name === only);
202
+ const lines = entries.length > 0
203
+ ? entries.map(([name, view]) => presetLine(name, view))
204
+ // A provider with no shipped preset is not an error: it simply has nothing to curate.
205
+ : [only ? `${only}: no model preset is shipped for this provider` : "no providers have a shipped model preset"];
206
+ printData(only ? providers[only] ?? {} : result, wantsJson, lines);
207
+ return;
208
+ }
209
+ if (action !== "apply") throw new CliUsageError(`unknown preset action '${action}'`, USAGE);
210
+ const provider = args.shift()?.trim();
211
+ const all = takeFlag(args, "--all");
212
+ if (!provider) throw new CliUsageError("provider is required", USAGE);
213
+ rejectArgs(args, USAGE);
214
+ const mode = all ? "all" : "preset";
215
+ const result = await runtimeRequest<{ selected?: string[]; fallback?: string; appliedVersion?: number }>(
216
+ "/api/model-presets",
217
+ { method: "PUT", body: JSON.stringify({ provider, mode }) },
218
+ deps,
219
+ );
220
+ const selectedIds = result.selected ?? [];
221
+ const line = result.fallback === "preset-empty"
222
+ // Never silently narrow to nothing: empty means ALL, so a zero-match preset keeps what was
223
+ // there and says so.
224
+ ? `${provider}: preset matched no models — selection unchanged (fallback to all)`
225
+ : all
226
+ ? `${provider}: showing all models (allowlist cleared)`
227
+ : `${provider}: preset v${result.appliedVersion ?? "?"} applied — ${selectedIds.length} models selected`;
228
+ printData(result, wantsJson, [line]);
229
+ }
230
+
231
+ async function newPolicy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
232
+ const args = [...argv];
233
+ const state = args[0] && !args[0].startsWith("--") ? args.shift()!.toLowerCase() : undefined;
234
+ const provider = takeOption(args, "--provider")?.trim();
235
+ const wantsJson = takeFlag(args, "--json");
236
+ if (state !== undefined && state !== "on" && state !== "off") throw new CliUsageError("new policy must be on or off", USAGE);
237
+ rejectArgs(args, USAGE);
238
+ if (!state) {
239
+ const result = await runtimeRequest<{ policy: string; providers: Record<string, string> }>("/api/model-discovery", {}, deps);
240
+ const value = provider ? result.providers[provider] ?? "inherit" : result.policy;
241
+ printData(provider ? { provider, policy: value } : result, wantsJson, [`${provider ?? "global"}: ${value}`]);
242
+ return;
243
+ }
244
+ const result = await runtimeRequest<{ baselineBootstrapped?: boolean }>("/api/model-discovery", {
245
+ method: "PUT", body: JSON.stringify({ policy: state, provider: provider ?? null }),
246
+ }, deps);
247
+ printData(result, wantsJson, [`${provider ?? "global"}: ${state}${result.baselineBootstrapped ? " (current models recorded as known)" : ""}`]);
248
+ }
249
+
250
+ async function newArrivals(argv: string[], deps: RuntimeApiDeps): Promise<void> {
251
+ const args = [...argv]; const wantsJson = takeFlag(args, "--json"); rejectArgs(args, USAGE);
252
+ const result = await runtimeRequest<{ recentArrivals: Record<string, Array<{ id: string; at: string; state: string }>> }>("/api/model-discovery", {}, deps);
253
+ const lines = Object.entries(result.recentArrivals).flatMap(([provider, rows]) => rows.map(row => `${provider}/${row.id} [${row.state}] ${row.at}`));
254
+ printData(result.recentArrivals, wantsJson, lines.length ? lines : ["no recent model arrivals"]);
255
+ }
256
+
165
257
  async function context(argv: string[], deps: RuntimeApiDeps): Promise<void> {
166
258
  const args = [...argv];
167
259
  const action = (args.shift() ?? "status").toLowerCase();
@@ -236,6 +328,9 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
236
328
  else if (sub === "disable") action = () => visibility(false, argv, deps);
237
329
  else if (sub === "provider") action = () => providerVisibility(argv, deps);
238
330
  else if (sub === "selected") action = () => selected(argv, deps);
331
+ else if (sub === "preset") action = () => preset(argv, deps);
332
+ else if (sub === "new-policy") action = () => newPolicy(argv, deps);
333
+ else if (sub === "new-arrivals") action = () => newArrivals(argv, deps);
239
334
  else if (sub === "context") action = () => context(argv, deps);
240
335
  else if (sub === "shadow") action = () => shadow(argv, deps);
241
336
  if (!action) return null;
package/src/cli/models.ts CHANGED
@@ -5,8 +5,13 @@ import { randomUUID } from "node:crypto";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { syncModelsToCodex } from "../codex/sync";
7
7
  import { hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
8
- import { canonicalizeReasoningEfforts, isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
9
- import { encodedModelIdCollides, routedSlug, slugEquals } from "../providers/slug-codec";
8
+ import {
9
+ canonicalizeReasoningEfforts,
10
+ configuredReasoningEfforts,
11
+ isDeclaredReasoningEffort,
12
+ modelRecordValue,
13
+ } from "../reasoning-effort";
14
+ import { encodedModelIdCollides, resolveSlugSelection, routedSlug } from "../providers/slug-codec";
10
15
  import { knownModelIdsForProvider } from "../router";
11
16
  import { findLiveProxy } from "../server/proxy-liveness";
12
17
  import { modelInList, type OcxConfig, type OcxCustomModel } from "../types";
@@ -91,7 +96,6 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
91
96
  const seen = new Set<string>();
92
97
  const contextWindows = prov.modelContextWindows ?? {};
93
98
  const inputModalities = prov.modelInputModalities ?? {};
94
- const reasoningEfforts = prov.modelReasoningEfforts ?? {};
95
99
  const globalContext = prov.contextWindow ?? null;
96
100
 
97
101
  const addModel = (model: string, isDefault: boolean) => {
@@ -107,7 +111,13 @@ function collectModels(config: OcxConfig, providerFilter?: string): ModelEntry[]
107
111
  // an exact `gpt-oss:120b` entry that lists "image", and the proxy rejects the image.
108
112
  const noVision = modelInList(prov.noVisionModels, model);
109
113
  const modalities = noVision ? ["text"] : (modelRecordValue(inputModalities, model) ?? null);
110
- const efforts = modelRecordValue(reasoningEfforts, model) ?? prov.reasoningEfforts ?? null;
114
+ // Same reason, for the ladder: `configuredReasoningEfforts` is what the catalog
115
+ // (`provider-fetch`) and the effort cap (`effort-policy`) resolve through, and it
116
+ // does three things this expression did not — it returns [] for a noReasoningModels
117
+ // match, drops levels Codex does not declare, and re-adds tiers the wire map proves
118
+ // the model emits. Restating two of its five lines here reported a ladder the proxy
119
+ // strips, and unsanitized junk as a supported level.
120
+ const efforts = configuredReasoningEfforts(prov, model) ?? null;
111
121
 
112
122
  entries.push({
113
123
  provider: provName,
@@ -267,11 +277,17 @@ async function handleCustomRemove(args: string[]): Promise<void> {
267
277
 
268
278
  const config = loadConfig();
269
279
  const existing = config.customModels ?? [];
270
- const matchingIndexes = existing.flatMap((model, index) => (
271
- target.includes("/")
272
- ? slugEquals(target, model.provider, model.modelId)
273
- : model.id === target
274
- ) ? [index] : []);
280
+ // Slug matching goes through the shared resolver so this command sees the same collision
281
+ // class catalog filtering and persisted sync see (#2491). `slugEquals` compares the raw and
282
+ // encoded spellings of ONE id, so a selector written in the native slash form matched only
283
+ // that row while the dash form matched both — the two relations disagreed on the same
284
+ // config. Removal stays exact-or-refuse: an ambiguous selector still aborts below, which is
285
+ // the right default for a destructive command.
286
+ const matchingIndexes = existing.flatMap((model, index) => {
287
+ if (!target.includes("/")) return model.id === target ? [index] : [];
288
+ const resolved = resolveSlugSelection(model.provider, target, [model.modelId]);
289
+ return resolved.matched.length > 0 ? [index] : [];
290
+ });
275
291
  if (matchingIndexes.length === 0) fail(`custom model "${target}" not found`);
276
292
  if (matchingIndexes.length > 1) {
277
293
  fail(`custom model selector "${target}" is ambiguous; use the custom model id`);
@@ -412,7 +428,7 @@ export async function handleModels(args: string[]): Promise<void> {
412
428
  handleCustomList(rest);
413
429
  return;
414
430
  }
415
- if (["live", "edit", "enable", "disable", "provider", "selected", "context", "shadow"].includes(subcommand ?? "")) {
431
+ if (["live", "edit", "enable", "disable", "provider", "selected", "preset", "context", "shadow"].includes(subcommand ?? "")) {
416
432
  const { handleModelsRuntimeCommand } = await import("./models-runtime");
417
433
  const code = await handleModelsRuntimeCommand(subcommand!, rest);
418
434
  if (code !== null) process.exitCode = code;
@@ -10,14 +10,17 @@ import {
10
10
  takeOption,
11
11
  type RuntimeApiDeps,
12
12
  } from "./runtime-api";
13
+ import { formatUsageReport } from "./usage-report";
14
+ import { USAGE_RANGES, USAGE_SURFACES } from "../usage/summary";
13
15
 
14
16
  const USAGE = `Usage:
15
17
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
16
- [--limit <n>] [--follow] [--json|--jsonl]
18
+ [--conversation <id>] [--limit <n>] [--follow] [--json|--jsonl]
17
19
  ocx logs explain <request-id> [--json]
18
20
  ocx logs rebuild-index
19
21
  ocx logs index-status
20
- ocx observe usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]
22
+ ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
23
+ [--provider <name>] [--model <id>] [--json]
21
24
  ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
22
25
  ocx observe memory [--json]
23
26
  ocx observe debug [--json]
@@ -47,7 +50,12 @@ function formatLog(row: LogEntry): string {
47
50
  const route = [row.provider, row.model].filter(Boolean).join("/");
48
51
  const status = row.status ?? row.statusCode ?? "?";
49
52
  const duration = row.durationMs !== undefined ? `${String(row.durationMs)}ms` : "";
50
- return [time, String(status), route, duration].filter(Boolean).join(" ");
53
+ // The conversation id is shown because a conversation FILTER whose output never names the
54
+ // conversation is hard to trust: an empty result and a wrong-id result look identical (#2704).
55
+ const conversation = typeof row.conversationId === "string" && row.conversationId.length > 0
56
+ ? `conv=${row.conversationId}`
57
+ : "";
58
+ return [time, String(status), route, duration, conversation].filter(Boolean).join(" ");
51
59
  }
52
60
 
53
61
  async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -58,13 +66,16 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
58
66
  const provider = takeOption(args, "--provider");
59
67
  const model = takeOption(args, "--model");
60
68
  const status = takeOption(args, "--status");
69
+ // Both spellings, because the server accepts both (`request-log.ts:1032`) and an operator
70
+ // should not have to remember which one this surface wanted.
71
+ const conversationId = takeOption(args, "--conversation") ?? takeOption(args, "--conversationId");
61
72
  const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? 200;
62
73
  rejectArgs(args, USAGE);
63
74
  if (wantsJson && wantsJsonl) throw new CliUsageError("--json and --jsonl cannot be combined", USAGE);
64
75
  if (follow && wantsJson) throw new CliUsageError("--follow uses --jsonl, not --json", USAGE);
65
76
  let seen = new Set<string>();
66
77
  do {
67
- const data = await runtimeRequest(`/api/logs${query({ provider, model, status, limit })}`, {}, deps);
78
+ const data = await runtimeRequest(`/api/logs${query({ provider, model, status, conversationId, limit })}`, {}, deps);
68
79
  const rows = logRows(data);
69
80
  if (!follow && wantsJson) printData(data, true);
70
81
  else {
@@ -131,11 +142,23 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
131
142
  const wantsJson = takeFlag(args, "--json");
132
143
  const range = takeOption(args, "--range") ?? "30d";
133
144
  const surface = takeOption(args, "--surface") ?? "all";
134
- if (!["7d", "30d", "all"].includes(range)) throw new CliUsageError("--range must be 7d, 30d, or all", USAGE);
135
- if (!["all", "codex", "claude", "grok"].includes(surface)) throw new CliUsageError("--surface must be all, codex, claude, or grok", USAGE);
145
+ const provider = takeOption(args, "--provider");
146
+ const model = takeOption(args, "--model");
147
+ // `1d` is accepted here as well as server-side so the CLI does not reject an
148
+ // alias the API would have understood.
149
+ const ranges = [...USAGE_RANGES, "1d"];
150
+ if (!ranges.includes(range)) throw new CliUsageError(`--range must be one of ${USAGE_RANGES.join(", ")} (1d aliases today)`, USAGE);
151
+ if (!USAGE_SURFACES.includes(surface as (typeof USAGE_SURFACES)[number])) {
152
+ throw new CliUsageError(`--surface must be one of ${USAGE_SURFACES.join(", ")}`, USAGE);
153
+ }
136
154
  rejectArgs(args, USAGE);
137
- const result = await runtimeRequest(`/api/usage${query({ range, surface })}`, {}, deps);
138
- printData(result, wantsJson, summaryLines(result));
155
+ const result = await runtimeRequest(`/api/usage${query({ range, surface, provider, model })}`, {}, deps);
156
+ // Built only when it will be printed: JavaScript evaluates arguments before
157
+ // the call, so passing formatUsageReport(...) inline would run the human
158
+ // renderer during --json and let its assumptions affect a path that is meant
159
+ // to bypass it entirely.
160
+ if (wantsJson) printData(result, true);
161
+ else printData(result, false, formatUsageReport(result as Parameters<typeof formatUsageReport>[0]));
139
162
  }
140
163
 
141
164
  async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -11,6 +11,13 @@ import {
11
11
  takeOption,
12
12
  type RuntimeApiDeps,
13
13
  } from "./runtime-api";
14
+ import { providerQuotaLine } from "./account-extended";
15
+ import type { ProviderQuotaReportDto } from "./account-api";
16
+
17
+ interface ProviderQuotasDto {
18
+ generatedAt?: number;
19
+ reports?: ProviderQuotaReportDto[];
20
+ }
14
21
 
15
22
  const USAGE = `Usage:
16
23
  ocx provider edit <name> [--adapter <id>] [--base-url <url>] [--default-model <id|->]
@@ -107,8 +114,15 @@ async function quota(argv: string[], deps: RuntimeApiDeps): Promise<void> {
107
114
  const wantsJson = takeFlag(args, "--json");
108
115
  const refresh = takeFlag(args, "--refresh");
109
116
  rejectArgs(args, USAGE);
110
- const result = await runtimeRequest(`/api/provider-quotas${refresh ? "?refresh=1" : ""}`, {}, deps);
111
- printData(result, wantsJson, summaryLines(result));
117
+ const result = await runtimeRequest<ProviderQuotasDto>(`/api/provider-quotas${refresh ? "?refresh=1" : ""}`, {}, deps);
118
+ // `summaryLines` is a depth-1 flattener: it renders a non-scalar array as "N item(s)", which
119
+ // collapsed the whole report to a count and made the command useless for its stated purpose
120
+ // (#2565). Render one line per report with the same formatter `ocx account refresh` uses.
121
+ const reports = Array.isArray(result?.reports) ? result.reports : [];
122
+ const lines = reports.length > 0
123
+ ? reports.map(report => providerQuotaLine(report.provider, report))
124
+ : ["no quota reports available"];
125
+ printData(result, wantsJson, lines);
112
126
  }
113
127
 
114
128
  async function presets(argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -8,7 +8,8 @@
8
8
  * show <name> Show provider config details (secrets masked)
9
9
  * set-default <name> Change the default provider
10
10
  */
11
- import { apiKeyTransportConfigError, hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
11
+ import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
12
+ import { apiKeyTransportConfigError } from "../config/provider-validation";
12
13
  import { hasHelpFlag } from "./help";
13
14
  import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
14
15
  import { providerConfigSeed } from "../providers/derive";
@@ -36,8 +36,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
36
36
  },
37
37
  {
38
38
  name: "recover-history",
39
- usage: "ocx recover-history --legacy-openai",
40
- summary: "Explicitly recover pre-backup syncResumeHistory rows.",
39
+ usage: "ocx recover-history --legacy-openai --yes",
40
+ summary: "Force all user-message opencodex rows to OpenAI for legacy recovery.",
41
41
  },
42
42
  {
43
43
  name: "uninstall",
@@ -88,20 +88,22 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
88
88
  { name: "ensure", usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
89
89
  {
90
90
  name: "sync",
91
- usage: "ocx sync [--restart-codex]",
91
+ usage: "ocx sync [--restart-codex] [--restart-desktop-app]",
92
92
  summary: "Fetch provider models and inject them into Codex config.",
93
93
  details: [
94
94
  "After writing the catalog, warns if long-lived Codex app-server processes are still running.",
95
95
  "--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
96
+ "--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
96
97
  ],
97
98
  },
98
99
  {
99
100
  name: "sync-cache",
100
- usage: "ocx sync-cache [--restart-codex]",
101
+ usage: "ocx sync-cache [--restart-codex] [--restart-desktop-app]",
101
102
  summary: "Refresh Codex's model cache from the active catalog.",
102
103
  details: [
103
104
  "Warns when Codex app-server processes still hold an in-memory model list.",
104
105
  "--restart-codex sends SIGTERM only to matching app-server / code-mode-host processes (may interrupt active turns).",
106
+ "--restart-desktop-app (Windows only, opt-in) fully restarts the Codex desktop app so its model picker re-reads the catalog. Never implied by --restart-codex: it ends live conversations.",
105
107
  ],
106
108
  },
107
109
  { name: "status", usage: "ocx status", summary: "Check proxy server status." },
@@ -162,10 +164,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
162
164
  "A selection-order change applies from the next unbound request and never moves a bound thread.",
163
165
  ],
164
166
  },
167
+ {
168
+ name: "alias",
169
+ usage: "ocx alias <list|set|rm|defaults> ...",
170
+ summary: "Manage short provider and model names.",
171
+ },
165
172
  {
166
173
  name: "models",
167
174
  aliases: ["model"],
168
- usage: "ocx models <list|live|add|edit|remove|enable|disable|provider|selected|context|shadow> ...",
175
+ usage: "ocx models <list|live|add|edit|remove|enable|disable|provider|selected|preset|context|shadow> ...",
169
176
  summary: "List models and manage custom (manually registered) models.",
170
177
  details: [
171
178
  "List available models from static config with no subcommand (liveModels may add more at runtime).",
@@ -205,13 +212,29 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
205
212
  usage: "ocx observe <logs|usage|storage|memory|debug|claude-inbound|injection> ...",
206
213
  summary: "Inspect proxy requests, usage, storage, memory, and debug data.",
207
214
  },
215
+ {
216
+ name: "inspect",
217
+ usage: "ocx inspect <config|catalog|routing-analytics|pacing|key-providers|codex-prompt|client-config|star|windows-tray> ...",
218
+ summary: "Read effective config, catalog, analytics, pacing, and the generated client-config snippet.",
219
+ details: [
220
+ "`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
221
+ ],
222
+ },
208
223
  { name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl]", summary: "Alias of ocx observe logs." },
209
224
  {
210
225
  name: "usage",
211
- usage: "ocx usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]",
226
+ usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--json]",
212
227
  summary: "Alias of ocx observe usage.",
213
228
  },
214
- { name: "storage", usage: "ocx storage [--json]", summary: "Alias of ocx observe storage." },
229
+ {
230
+ name: "storage",
231
+ usage: "ocx storage <report|cleanup|trash|policy> ...",
232
+ summary: "Storage report, archived-session cleanup, trash restore, and the cleanup policy.",
233
+ details: [
234
+ "A bare `ocx storage` prints the report, as it did when this was an alias of `observe storage`.",
235
+ "`cleanup` previews by default and only deletes under --yes; `trash restore` and `policy run` also require --yes.",
236
+ ],
237
+ },
215
238
  { name: "memory", usage: "ocx memory [--json]", summary: "Alias of ocx observe memory." },
216
239
  {
217
240
  name: "access",
@@ -221,8 +244,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
221
244
  { name: "api-key", usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
222
245
  {
223
246
  name: "export",
224
- usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime> [--json] [--out <path>] [--force]",
225
- summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent) wired to the running proxy.",
247
+ usage: "ocx export --client <opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|prime|cline> [--json] [--out <path>] [--force]",
248
+ summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, Gajae Code, DeepSeek Harness, MiniMax Code, ZCode, Prime Agent, Cline) wired to the running proxy.",
226
249
  details: [
227
250
  "--json prints the generated document as JSON on stdout; use --out for the client's native format.",
228
251
  "--out <path> writes the native config there and refuses to replace an existing file without --force.",
@@ -235,7 +258,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
235
258
  usage: "ocx grok <status|exclude|include|set|clear|apply> ...",
236
259
  summary: "Manage and apply the Grok Build model fence.",
237
260
  },
238
- { name: "integration", usage: "ocx integration <claude|grok|client> ...", summary: "Manage supported client integrations." },
261
+ {
262
+ name: "integration",
263
+ usage: "ocx integration <claude|grok|client|native> ...",
264
+ summary: "Manage supported client integrations, and the native client toggles.",
265
+ details: [
266
+ "`native` shows or flips the native Claude/Claude Desktop/Codex/Grok toggles; the other subcommands manage the reversible file integrations.",
267
+ ],
268
+ },
239
269
  {
240
270
  name: "system",
241
271
  usage: "ocx system <status|settings|startup|diagnostics|sync|update> ...",
@@ -342,6 +372,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
342
372
  summary: "Check proxy health. Exits 0 if healthy, 1 otherwise.",
343
373
  details: ["Use --json for structured output: {ok, pid, port}."],
344
374
  },
375
+ {
376
+ name: "capabilities",
377
+ usage: "ocx capabilities [--json] [--mutating-only] [--route <path>]",
378
+ summary: "List the declared CLI capabilities and the management routes they drive.",
379
+ details: [
380
+ "The machine-readable surface index: start here when driving ocx programmatically instead of parsing help text.",
381
+ "--route <path> answers the inverse question: which commands drive this management route.",
382
+ ],
383
+ },
345
384
  {
346
385
  name: "ready",
347
386
  usage: "ocx ready [--json] [--wait [--timeout <seconds>]]",
@@ -47,15 +47,37 @@ export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string>
47
47
  return `http://${probeHostname(live.hostname)}:${live.port}`;
48
48
  }
49
49
 
50
+ function stringField(record: Record<string, unknown>, key: string): string | undefined {
51
+ const value = record[key];
52
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
53
+ }
54
+
55
+ /**
56
+ * Compose the operator-facing message from a management error body.
57
+ *
58
+ * The server states WHY a request was refused under `reason` and WHAT TO DO under
59
+ * `hint` (see management-auth.ts, which sets both on a 503 when the management plane
60
+ * is unavailable). Both were dropped here, so a fenced management plane was
61
+ * indistinguishable from a generic failure and an operator had no way to tell a port
62
+ * collision from an ACL refusal from a stopped proxy (#2698).
63
+ */
50
64
  function responseMessage(body: unknown, status: number): string {
51
- if (body && typeof body === "object") {
52
- const record = body as Record<string, unknown>;
53
- for (const key of ["error", "message", "detail"]) {
54
- if (typeof record[key] === "string" && record[key]) return record[key];
55
- }
56
- }
57
65
  if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
58
- return `Management request failed (${status})`;
66
+ if (!body || typeof body !== "object") return `Management request failed (${status})`;
67
+ const record = body as Record<string, unknown>;
68
+ let primary: string | undefined;
69
+ for (const key of ["error", "message", "detail"]) {
70
+ primary = stringField(record, key);
71
+ if (primary) break;
72
+ }
73
+ const parts = [primary ?? `Management request failed (${status})`];
74
+ const reason = stringField(record, "reason");
75
+ // A body of {ok:false, reason:"…"} with no `error` key used to degrade to the
76
+ // generic line, discarding the only actionable field.
77
+ if (reason && reason !== primary) parts.push(`reason: ${reason}`);
78
+ const hint = stringField(record, "hint");
79
+ if (hint && hint !== primary) parts.push(`hint: ${hint}`);
80
+ return parts.join("\n").slice(0, 1200);
59
81
  }
60
82
 
61
83
  export async function runtimeRequest<T = unknown>(
@@ -94,6 +116,28 @@ export function takeFlag(args: string[], flag: string): boolean {
94
116
  return true;
95
117
  }
96
118
 
119
+ /** Unicode dashes that copy-paste and IME input substitute for ASCII `-`. */
120
+ const DASH_CLASS = /[\u2010-\u2015\u2212]/g;
121
+
122
+ /**
123
+ * True for `--json`, `--json=true`, `-json`, and Unicode-dash spellings.
124
+ * Matching only the exact token `--json` is the same defect logout had: each
125
+ * spelling that slips through is a silent success (or, for doctor, prose on
126
+ * stdout after the caller asked for JSON).
127
+ */
128
+ export function isJsonOption(arg: string): boolean {
129
+ const body = arg.replace(DASH_CLASS, "-").replace(/^-+/, "");
130
+ return body === "json" || body.startsWith("json=");
131
+ }
132
+
133
+ /** Remove one JSON-request spelling from `args`. Returns whether one was present. */
134
+ export function takeJsonFlag(args: string[]): boolean {
135
+ const index = args.findIndex(isJsonOption);
136
+ if (index === -1) return false;
137
+ args.splice(index, 1);
138
+ return true;
139
+ }
140
+
97
141
  export function takeOption(args: string[], flag: string): string | undefined {
98
142
  const index = args.indexOf(flag);
99
143
  if (index === -1) return undefined;
package/src/cli/status.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { durableBunRuntime } from "../lib/bun-runtime";
2
- import { codexAutoStartEnabled, getConfigPath, getPidPath, readConfigDiagnostics, readPid, readRuntimePort, type RuntimePortState } from "../config";
2
+ import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
3
+ import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
3
4
  import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
4
5
  import { findLiveProxy, isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
5
6
  import { directLocalHttpFetch } from "../server/direct-local-http";
@@ -9,6 +10,8 @@ import { collectStartupHealth, type StartupHealth } from "../codex/autostart-hea
9
10
  import { getCodexRoutingKind } from "../codex/inject";
10
11
  import { diagnoseCodexShim } from "../codex/shim";
11
12
  import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime";
13
+ import { packageVersion } from "./help";
14
+ import { computeVersionSkew, type VersionSkew } from "./version-skew";
12
15
  import { redactSecretString, redactUserPath } from "../lib/redact";
13
16
  import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home";
14
17
  import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
@@ -69,6 +72,13 @@ export type CliStatusJson = {
69
72
  };
70
73
  };
71
74
  codexHome: OrcaCodexHomeDiagnostic;
75
+ /**
76
+ * This CLI's version against the running proxy's (#2701).
77
+ *
78
+ * Additive and optional-by-value, so `schemaVersion` stays 1: an existing consumer that
79
+ * ignores the key is unaffected, and `proxyVersion` is null when nothing is live.
80
+ */
81
+ versionSkew: VersionSkew;
72
82
  };
73
83
 
74
84
  export type CliStatusView = {
@@ -117,6 +127,29 @@ export function proxyHealthFailureReason(error: unknown, signal: AbortSignal): "
117
127
  : "unreachable";
118
128
  }
119
129
 
130
+ /**
131
+ * `ocx status` greens on process liveness alone, so a proxy that answers
132
+ * /healthz reads healthy even when Codex is not pointed at it and every routed
133
+ * request goes to OpenAI instead (#2411). The proxy line is not wrong — the
134
+ * listener really is up — so it keeps its check, and this supplies the signal
135
+ * that was missing rather than corrupting the one that was already honest.
136
+ *
137
+ * Only `native` warns. `custom-local` and `unknown` are also "this proxy is
138
+ * unused", but startupHealthSummary already renders both as AT RISK with a
139
+ * remedy command, and `custom-remote` is a deliberate operator choice. Warning
140
+ * on all four would teach operators to skip the line that matters.
141
+ */
142
+ export function unusedProxyWarningLines(input: {
143
+ proxyUp: boolean;
144
+ routingKind: StartupHealth["routingKind"];
145
+ }): string[] {
146
+ if (!input.proxyUp || input.routingKind !== "native") return [];
147
+ return [
148
+ "⚠️ Codex routing is native — the running proxy is unused.",
149
+ " Codex requests go to OpenAI, not this proxy. Re-point with: ocx start",
150
+ ];
151
+ }
152
+
120
153
  async function checkProxyHealth(target: ListenTarget): Promise<HealthCheck> {
121
154
  const url = target.healthUrl;
122
155
  const controller = new AbortController();
@@ -156,6 +189,9 @@ export async function collectStatus(): Promise<CliStatusView> {
156
189
  const pidFile = readPid();
157
190
  // Preserve an authoritative null from orphan/legacy liveness — do not restore pidFile.
158
191
  const pid = resolveStatusPid(live, pidFile);
192
+ // No extra request: findLiveProxy's identity probe already parsed and validated the
193
+ // healthz body, so the version came back with the liveness result.
194
+ const versionSkew = computeVersionSkew(packageVersion(), live?.version);
159
195
  const listen = live
160
196
  ? {
161
197
  port: live.port,
@@ -323,6 +359,10 @@ export async function collectStatus(): Promise<CliStatusView> {
323
359
  codexPlugins,
324
360
  codexRuntime,
325
361
  codexHome,
362
+ // Own field rather than a line in `codexRuntime.warning`: a stale ocx on PATH is a
363
+ // fact about this install, not about the Codex runtime, and filing it there would
364
+ // print it under the wrong heading (#2701).
365
+ versionSkew,
326
366
  },
327
367
  };
328
368
  }