@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * Rules, in order:
12
12
  * - A running Desktop app is the supervisor. Wait for its proxy; never start a competing one.
13
- * - A durable Desktop claim with no running app: start Desktop (hidden, as a login launch would)
14
- * where that is possible, then wait. Elsewhere, fail with guidance. A missing Desktop process
13
+ * - A durable Desktop claim with no running app: start Desktop (hidden, by the path its own
14
+ * login registration recorded — LaunchServices never picks the bundle) where that is
15
+ * verifiable, then wait. Elsewhere, fail with guidance. A missing Desktop process
15
16
  * never authorizes a CLI proxy by itself; handing ownership to the CLI stays an explicit
16
17
  * `ocx service install` consent step.
17
18
  * - Ownership or the process list that cannot be read is not evidence of absence: refuse.
@@ -22,9 +23,9 @@
22
23
  */
23
24
  import { execFileSync } from "node:child_process";
24
25
  import { randomUUID } from "node:crypto";
25
- import { existsSync, linkSync, readdirSync, readFileSync, readlinkSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
+ import { accessSync, constants, existsSync, linkSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
26
27
  import { homedir } from "node:os";
27
- import { basename, dirname, join, win32 } from "node:path";
28
+ import { basename, dirname, join, posix, win32 } from "node:path";
28
29
  import { getConfigDir } from "../config/paths";
29
30
  import { resolveServiceOwnership } from "../service/state";
30
31
 
@@ -220,11 +221,63 @@ export function inspectDesktopProcess(options: DesktopProcessOptions = {}): Desk
220
221
  }
221
222
  }
222
223
 
223
- /** Start the macOS app hidden, the same way its login item does. */
224
- export function launchDesktopApp(options: { platform?: NodeJS.Platform; run?: Execute } = {}): boolean {
224
+ const DESKTOP_BUNDLE_ID = "com.opencodex.desktop";
225
+ const DESKTOP_PLIST_LABEL = "OpenCodex";
226
+
227
+ /**
228
+ * The app bundle the consented installation itself registered as its Start-at-Login program —
229
+ * not whichever bundle LaunchServices would resolve for our identifier. `open -b` answers a
230
+ * mutable per-user registry: any planted bundle claiming {@link DESKTOP_BUNDLE_ID} could be
231
+ * selected and executed in the real app's place. The recorded claim, this app's own install-id
232
+ * store, and the login registration it wrote are the durable evidence of which installation
233
+ * that is; a claim whose registration is absent, malformed, stale or points off a Desktop
234
+ * executable refuses the launch instead of guessing.
235
+ */
236
+ function consentedDesktopBundle(home: string, installId: string, run: Execute): string | null {
237
+ const localId = readFileSync(
238
+ join(home, "Library", "Application Support", DESKTOP_BUNDLE_ID, "install-id"), "utf8",
239
+ ).trim();
240
+ if (!localId || localId !== installId) return null;
241
+ const plistPath = realpathSync(join(home, "Library", "LaunchAgents", `${DESKTOP_PLIST_LABEL}.plist`));
242
+ const registration = JSON.parse(
243
+ run("/usr/bin/plutil", ["-convert", "json", "-o", "-", plistPath], 3_000),
244
+ ) as { Label?: unknown; ProgramArguments?: unknown; Program?: unknown };
245
+ const args = registration.ProgramArguments;
246
+ // The shapes auto-launch writes: [program] or [program, "--autostart"]; see the migration
247
+ // rules in desktop/src-tauri/src/login_registration.rs, which classify anything else Leave.
248
+ // This is a macOS plist path, regardless of the host running an injected launch probe.
249
+ if (registration.Label !== DESKTOP_PLIST_LABEL || !Array.isArray(args)
250
+ || (args.length !== 1 && args.length !== 2) || typeof args[0] !== "string" || !posix.isAbsolute(args[0])
251
+ || (args.length === 2 && args[1] !== "--autostart")
252
+ || (registration.Program !== undefined && registration.Program !== args[0])) return null;
253
+ const executable = realpathSync(args[0]);
254
+ if (!isDesktopExecutable(executable, "darwin") || !statSync(executable).isFile()) return null;
255
+ accessSync(executable, constants.X_OK);
256
+ const bundle = dirname(dirname(dirname(executable)));
257
+ if (!bundle.endsWith(".app")) return null;
258
+ const info = JSON.parse(
259
+ run("/usr/bin/plutil", ["-convert", "json", "-o", "-", join(bundle, "Contents", "Info.plist")], 3_000),
260
+ ) as { CFBundleIdentifier?: unknown; CFBundleExecutable?: unknown };
261
+ // `open` uses this selector, so checking only the registration and identifier would
262
+ // validate one binary while launching another. The registered path is already canonical.
263
+ if (info.CFBundleIdentifier !== DESKTOP_BUNDLE_ID || info.CFBundleExecutable !== "opencodex-desktop") return null;
264
+ return realpathSync(join(bundle, "Contents", "MacOS", info.CFBundleExecutable)) === executable ? bundle : null;
265
+ }
266
+
267
+ /** Start the consented macOS app hidden, by the path its own login registration recorded. */
268
+ export function launchDesktopApp(options: {
269
+ platform?: NodeJS.Platform;
270
+ run?: Execute;
271
+ home?: string;
272
+ ownership?: () => ReturnType<typeof resolveServiceOwnership>;
273
+ } = {}): boolean {
225
274
  if ((options.platform ?? process.platform) !== "darwin") return false;
226
275
  try {
227
- (options.run ?? execute)("/usr/bin/open", ["-g", "-b", "com.opencodex.desktop", "--args", "--autostart"], 10_000);
276
+ const owner = (options.ownership ?? resolveServiceOwnership)();
277
+ if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return false;
278
+ const bundle = consentedDesktopBundle(options.home ?? homedir(), owner.ownership.installId, options.run ?? execute);
279
+ if (bundle === null) return false;
280
+ (options.run ?? execute)("/usr/bin/open", ["-g", bundle, "--args", "--autostart"], 10_000);
228
281
  return true;
229
282
  } catch { return false; }
230
283
  }
package/src/cli/help.ts CHANGED
@@ -85,14 +85,14 @@ Usage:
85
85
  ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
86
86
  ocx route <sub> Routing features (combo, policy)
87
87
  ocx logs [filters] Alias of ocx observe logs
88
- ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>]
88
+ ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>] [--top <1-1000>]
89
89
  Token and estimated-cost report (alias of ocx observe usage)
90
90
  ocx storage <sub> Storage report, cleanup, trash, and the cleanup policy
91
91
  ocx memory [--json] Alias of ocx observe memory
92
92
  ocx api-key <sub> Alias of ocx access key
93
93
  ocx access <sub> External API keys and endpoint information
94
94
  ocx api <sub> Protocol paths: vocabulary, request-path preview, and policy
95
- ocx export --client <id> Print a client config wired to the running proxy (19 clients)
95
+ ocx export --client <id> Print a client config wired to the running proxy (20 clients)
96
96
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
97
97
  ocx grok <sub> Grok Build model selection and apply
98
98
  ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
@@ -100,6 +100,7 @@ Usage:
100
100
  ocx advisor <status|on|off|set|consent> Advisor sidecar settings and context-sharing consent
101
101
  ocx companion <show|set|reset> Menu-bar and widget companion usage settings
102
102
  ocx lab <sub> Inspect Lab evidence and control local automation
103
+ ocx codex-provider <sub> Confirm custom Codex provider routing: status|allow|revoke
103
104
  ocx chatgpt <sub> Experimental app-server shim: launch|restore|status (macOS)
104
105
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
105
106
  ocx claude desktop [sub] Manage and apply Claude Desktop's four-family profile
package/src/cli/index.ts CHANGED
@@ -660,7 +660,7 @@ async function handleStart(options: { block?: boolean } = {}) {
660
660
  }
661
661
  removePid(process.pid);
662
662
  removeRuntimePort(process.pid);
663
- if (teardown.restoreNativeCodex && !currentExternalCodexModelProvider()) {
663
+ if (teardown.restoreNativeCodex) {
664
664
  try {
665
665
  if (!codexHomeIsAbsent(getCodexHome())) {
666
666
  const restored = restoreNativeCodex();
@@ -27,6 +27,7 @@ export const FILE_INTEGRATION_CLIENTS = [
27
27
  "kilo",
28
28
  "droid",
29
29
  "qoder",
30
+ "gentle-shell",
30
31
  ] as const;
31
32
 
32
33
  export const INTEGRATION_REFUSAL_REASONS: ReadonlySet<string> = new Set<IntegrationRefusalReason>([
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Indexed request-history reads: page the derived ledger, or fetch one canonical row.
3
+ *
4
+ * Distinct from `ocx logs` (the recent ring) and `ocx logs explain` (route-decision).
5
+ */
6
+ import { REQUEST_HISTORY_DEFAULT_PAGE_SIZE, REQUEST_HISTORY_MAX_PAGE_SIZE } from "../routing/history/indexer";
7
+ import { parseUsageTimeWindow } from "../usage/time-range";
8
+ import {
9
+ CliUsageError,
10
+ printData,
11
+ rejectArgs,
12
+ runtimeRequest,
13
+ takeFlag,
14
+ takeIntegerOption,
15
+ takeOption,
16
+ type RuntimeApiDeps,
17
+ } from "./runtime-api";
18
+
19
+ type LogRow = Record<string, unknown>;
20
+
21
+ export const LOG_HISTORY_USAGE = `Usage:
22
+ ocx logs history [--provider <name>] [--model <id>] [--requested-model <id>]
23
+ [--status <code>] [--conversation <id>] [--surface <name>] [--protocol <responses|chat|messages>]
24
+ [--api-key-id <id>] [--profile <id>] [--fallback <true|false>]
25
+ [--from <epoch-ms|ISO-datetime>] [--to <epoch-ms|ISO-datetime>]
26
+ [--cursor <token>] [--limit <1-${REQUEST_HISTORY_MAX_PAGE_SIZE}>] [--json|--jsonl]
27
+ ocx logs get <request-id> [--json]`;
28
+
29
+ const GET_USAGE = "Usage: ocx logs get <request-id> [--json]";
30
+ const PROTOCOLS = ["responses", "chat", "messages"] as const;
31
+
32
+ function query(params: Record<string, string | number | boolean | undefined>): string {
33
+ const search = new URLSearchParams();
34
+ for (const [key, value] of Object.entries(params)) {
35
+ if (value !== undefined) search.set(key, String(value));
36
+ }
37
+ const encoded = search.toString();
38
+ return encoded ? `?${encoded}` : "";
39
+ }
40
+
41
+ function timestampOption(args: string[], flag: string): number | undefined {
42
+ const raw = takeOption(args, flag);
43
+ if (raw === undefined) return undefined;
44
+ try {
45
+ return parseUsageTimeWindow(raw, raw)!.since;
46
+ } catch (error) {
47
+ throw new CliUsageError(error instanceof Error ? `${flag}: ${error.message}` : `${flag} is invalid`, LOG_HISTORY_USAGE);
48
+ }
49
+ }
50
+
51
+ function record(value: unknown): value is Record<string, unknown> {
52
+ return value !== null && typeof value === "object" && !Array.isArray(value);
53
+ }
54
+
55
+ function formatCost(row: Record<string, unknown>): string {
56
+ const metrics = record(row.displayMetrics) ? row.displayMetrics : undefined;
57
+ const cost = metrics && record(metrics.cost) ? metrics.cost : undefined;
58
+ if (!cost || cost.kind !== "value" || !record(cost.estimate) || !record(cost.estimate.cost)) return "";
59
+ const total = cost.estimate.cost.total;
60
+ return typeof total === "number" && Number.isFinite(total) ? `~$${total.toFixed(4)}` : "";
61
+ }
62
+
63
+ function formatHistoryRow(row: LogRow, formatRow: (row: LogRow) => string): string {
64
+ const tokens = typeof row.totalTokens === "number" && Number.isFinite(row.totalTokens)
65
+ ? `${row.totalTokens}tok`
66
+ : "";
67
+ return [formatRow(row), tokens, formatCost(row)].filter(Boolean).join(" ");
68
+ }
69
+
70
+ function historyPage(raw: unknown): {
71
+ entries: Record<string, unknown>[];
72
+ nextCursor?: string;
73
+ hasMore: boolean;
74
+ index?: Record<string, unknown>;
75
+ } {
76
+ if (!record(raw) || !Array.isArray(raw.entries) || typeof raw.hasMore !== "boolean") {
77
+ throw new Error("Invalid request-history page. Upgrade the proxy and retry.");
78
+ }
79
+ const entries = raw.entries;
80
+ if (!entries.every(record)) throw new Error("Invalid request-history page. Upgrade the proxy and retry.");
81
+ const nextCursor = typeof raw.nextCursor === "string" && raw.nextCursor.length > 0 ? raw.nextCursor : undefined;
82
+ const index = record(raw.index) ? raw.index : undefined;
83
+ return { entries, nextCursor, hasMore: raw.hasMore, index };
84
+ }
85
+
86
+ /** Page the derived request-history index through GET /api/request-history. */
87
+ export async function handleLogHistoryCommand(
88
+ argv: string[],
89
+ deps: RuntimeApiDeps,
90
+ formatRow: (row: LogRow) => string,
91
+ ): Promise<void> {
92
+ const args = [...argv];
93
+ const wantsJson = takeFlag(args, "--json");
94
+ const wantsJsonl = takeFlag(args, "--jsonl");
95
+ const provider = takeOption(args, "--provider");
96
+ const model = takeOption(args, "--model");
97
+ const requestedModel = takeOption(args, "--requested-model");
98
+ const conversationId = takeOption(args, "--conversation") ?? takeOption(args, "--conversationId");
99
+ const surface = takeOption(args, "--surface");
100
+ const protocol = takeOption(args, "--protocol");
101
+ const apiKeyId = takeOption(args, "--api-key-id");
102
+ const profileId = takeOption(args, "--profile");
103
+ const fallbackRaw = takeOption(args, "--fallback");
104
+ const cursor = takeOption(args, "--cursor");
105
+ const status = takeIntegerOption(args, "--status", { min: 100 });
106
+ const limit = takeIntegerOption(args, "--limit", { min: 1 }) ?? REQUEST_HISTORY_DEFAULT_PAGE_SIZE;
107
+ const from = timestampOption(args, "--from");
108
+ const to = timestampOption(args, "--to");
109
+ rejectArgs(args, LOG_HISTORY_USAGE);
110
+ if (wantsJson && wantsJsonl) throw new CliUsageError("--json and --jsonl cannot be combined", LOG_HISTORY_USAGE);
111
+ if (status !== undefined && status > 599) throw new CliUsageError("--status must be an integer from 100 to 599", LOG_HISTORY_USAGE);
112
+ if (limit > REQUEST_HISTORY_MAX_PAGE_SIZE) {
113
+ throw new CliUsageError(`--limit must be between 1 and ${REQUEST_HISTORY_MAX_PAGE_SIZE}`, LOG_HISTORY_USAGE);
114
+ }
115
+ if (protocol !== undefined && !PROTOCOLS.includes(protocol as (typeof PROTOCOLS)[number])) {
116
+ throw new CliUsageError(`--protocol must be one of ${PROTOCOLS.join(", ")}`, LOG_HISTORY_USAGE);
117
+ }
118
+ let fallback: boolean | undefined;
119
+ if (fallbackRaw !== undefined) {
120
+ if (fallbackRaw !== "true" && fallbackRaw !== "false") {
121
+ throw new CliUsageError("--fallback must be true or false", LOG_HISTORY_USAGE);
122
+ }
123
+ fallback = fallbackRaw === "true";
124
+ }
125
+ if (from !== undefined && to !== undefined && from > to) {
126
+ throw new CliUsageError("--from must not be after --to", LOG_HISTORY_USAGE);
127
+ }
128
+ const data = await runtimeRequest(`/api/request-history${query({
129
+ provider, model, requestedModel, status, conversationId, surface,
130
+ inboundProtocol: protocol, apiKeyId, profileId, fallback, from, to, cursor, limit,
131
+ })}`, {}, deps);
132
+ const page = historyPage(data);
133
+ if (wantsJson) {
134
+ printData(data, true);
135
+ return;
136
+ }
137
+ if (wantsJsonl) {
138
+ for (const row of page.entries) console.log(JSON.stringify(row));
139
+ if (page.hasMore && page.nextCursor) console.error(`nextCursor=${page.nextCursor}`);
140
+ return;
141
+ }
142
+ const indexed = typeof page.index?.indexedRows === "number" ? page.index.indexedRows : undefined;
143
+ if (page.entries.length === 0) {
144
+ printData(data, false, [
145
+ "No request-history row matched the filters.",
146
+ ...(indexed !== undefined ? [`indexedRows=${indexed}`] : []),
147
+ ]);
148
+ return;
149
+ }
150
+ const footer = [
151
+ ...(indexed !== undefined ? [`indexedRows=${indexed}`] : []),
152
+ `hasMore=${page.hasMore}`,
153
+ ...(page.nextCursor ? [`nextCursor=${page.nextCursor}`] : []),
154
+ ];
155
+ printData(data, false, [...page.entries.map(row => formatHistoryRow(row, formatRow)), ...footer]);
156
+ }
157
+
158
+ /** Read one canonical request-history row through GET /api/request-history/{id}. */
159
+ export async function handleLogGetCommand(
160
+ argv: string[],
161
+ deps: RuntimeApiDeps,
162
+ formatRow: (row: LogRow) => string,
163
+ ): Promise<void> {
164
+ const args = [...argv];
165
+ const requestId = args.shift();
166
+ const wantsJson = takeFlag(args, "--json");
167
+ if (!requestId) throw new CliUsageError("request id is required", GET_USAGE);
168
+ rejectArgs(args, GET_USAGE);
169
+ const result = await runtimeRequest(`/api/request-history/${encodeURIComponent(requestId)}`, {}, deps);
170
+ if (!record(result)) throw new Error("Invalid request-history row. Upgrade the proxy and retry.");
171
+ printData(result, wantsJson, wantsJson ? undefined : [formatHistoryRow(result, formatRow)]);
172
+ }
@@ -0,0 +1,31 @@
1
+ /** A failed connect cannot distinguish an absent loopback address from a slow owner. */
2
+ export type LoopbackFamilyAvailability = "available" | "unavailable" | "unknown";
3
+
4
+ function bindLoopback(hostname: string): { stop(): void } {
5
+ return Bun.listen({ hostname, port: 0, socket: { data(socket) { socket.end(); } } });
6
+ }
7
+
8
+ /**
9
+ * Ask the kernel whether the concrete local address can be bound, independently of
10
+ * routing/connect timeouts. Only address/family absence permits skipping discovery;
11
+ * permissions, resource exhaustion and other failures leave owner evidence unknown.
12
+ * The ephemeral listener is closed synchronously and never accepts client traffic.
13
+ */
14
+ export function loopbackFamilyAvailability(
15
+ hostname: string,
16
+ bindFn: (hostname: string) => { stop(): void } = bindLoopback,
17
+ ): LoopbackFamilyAvailability {
18
+ const address = hostname === "[::1]" ? "::1" : hostname;
19
+ if (address !== "::1" && address !== "127.0.0.1") return "unknown";
20
+ let listener: { stop(): void };
21
+ try {
22
+ listener = bindFn(address);
23
+ } catch (error) {
24
+ const code = (error as { code?: unknown } | null)?.code;
25
+ return code === "EADDRNOTAVAIL" || code === "EAFNOSUPPORT"
26
+ || code === "EPROTONOSUPPORT"
27
+ ? "unavailable" : "unknown";
28
+ }
29
+ listener.stop();
30
+ return "available";
31
+ }
@@ -28,6 +28,7 @@ export const MODELS_RUNTIME_SUBCOMMANDS = [
28
28
  "new-arrivals",
29
29
  "context",
30
30
  "shadow",
31
+ "auto-review",
31
32
  "order",
32
33
  "display-name",
33
34
  ] as const;
@@ -21,7 +21,7 @@ import { redactSecretString } from "../lib/redact";
21
21
  import type { ProviderCostOverlay } from "../types";
22
22
  import { resolveMatchedPrice } from "../usage/cost";
23
23
  import { MAX_COST4_RATE } from "../usage/expected-prices";
24
- import { isValidCost4Rate } from "../usage/user-cost-overlays";
24
+ import { isValidCost4Rate, validCost4 } from "../usage/user-cost-overlays";
25
25
 
26
26
  const USAGE = `Usage:
27
27
  ocx models display-name <provider/raw-model> (--set <text> | --clear) [--json]
@@ -48,6 +48,7 @@ const USAGE = `Usage:
48
48
  ocx models new-arrivals [--json]
49
49
  ${MODELS_CONTEXT_USAGE}
50
50
  ocx models shadow <status|set> [model|-] [--enabled <on|off>] [--json]
51
+ ocx models auto-review <status|set> [model|-] [--enabled <on|off>] [--json]
51
52
 
52
53
  Prices are USD per 1M tokens. Omitted cache rates default to 0.
53
54
  Price selectors use the exact upstream model ID after the first slash.`;
@@ -89,8 +90,8 @@ function priceRecord(value: unknown): value is Record<string, unknown> {
89
90
  const PRICE_RATE_KEYS = ["input", "output", "cacheRead", "cacheWrite"] as const;
90
91
 
91
92
  function validPriceCost(value: unknown): value is ProviderCostOverlay {
92
- return priceRecord(value) && Object.keys(value).length === PRICE_RATE_KEYS.length
93
- && PRICE_RATE_KEYS.every(key => Object.hasOwn(value, key) && isValidCost4Rate(value[key]));
93
+ return priceRecord(value) && validCost4(value)
94
+ && Object.keys(value).every(key => key === "promptLengthPricing" || (PRICE_RATE_KEYS as readonly string[]).includes(key));
94
95
  }
95
96
 
96
97
  async function price(write: boolean, argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -135,7 +136,9 @@ async function priceRequest(write: boolean, argv: string[], deps: RuntimeApiDeps
135
136
  }
136
137
  // The API map owns manual overrides; bundled defaults remain derived rather
137
138
  // than being persisted as overrides that would mask later catalog updates.
138
- const effectiveCost = cost ?? resolveMatchedPrice(provider, modelId, undefined, [])?.cost4 ?? null;
139
+ const effectiveCost = cost
140
+ ? { input: cost.input, output: cost.output, cacheRead: cost.cacheRead, cacheWrite: cost.cacheWrite }
141
+ : resolveMatchedPrice(provider, modelId, undefined, [])?.cost4 ?? null;
139
142
  printData({ provider, modelId, cost, effectiveCost }, wantsJson, [
140
143
  effectiveCost === null ? `${selector}: automatic pricing (unknown)`
141
144
  : `${selector}: ${JSON.stringify(effectiveCost)} USD per 1M tokens${cost === null ? " (automatic estimate)" : ""}`,
@@ -161,17 +164,42 @@ async function priceRequest(write: boolean, argv: string[], deps: RuntimeApiDeps
161
164
  }
162
165
  return value;
163
166
  };
164
- const cost: ProviderCostOverlay | null = auto ? null : {
167
+ // Validate every rate before any request, so a bad flag never reaches the management plane.
168
+ const rates: ProviderCostOverlay | undefined = auto ? undefined : {
165
169
  input: rate(input!, "--input"),
166
170
  output: rate(output!, "--output"),
167
171
  cacheRead: rate(cacheRead ?? "0", "--cache-read"),
168
172
  cacheWrite: rate(cacheWrite ?? "0", "--cache-write"),
169
173
  };
174
+ // A base-rate write replaces the whole row, so read the current row first and carry the
175
+ // stored prompt-length policy for exactly this model forward instead of silently deleting it.
176
+ let carriedPromptLengthPricing: ProviderCostOverlay["promptLengthPricing"];
177
+ if (rates) {
178
+ const current = await runtimeRequest<unknown>(path, {}, deps);
179
+ if (!priceRecord(current) || current.provider !== provider || !priceRecord(current.modelCosts)
180
+ || !Object.values(current.modelCosts).every(validPriceCost)) {
181
+ throw new Error("Invalid model price response");
182
+ }
183
+ if (Object.hasOwn(current.modelCosts, modelId)) {
184
+ carriedPromptLengthPricing = (current.modelCosts[modelId] as ProviderCostOverlay).promptLengthPricing;
185
+ }
186
+ }
187
+ const cost: ProviderCostOverlay | null = rates
188
+ ? { ...rates, ...(carriedPromptLengthPricing ? { promptLengthPricing: carriedPromptLengthPricing } : {}) }
189
+ : null;
170
190
  const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify({ modelId, cost }) }, deps);
191
+ // Absent and { mode: "automatic" } are the same policy (the server stores automatic as
192
+ // absent), and key order carries no meaning in a JSON receipt, so compare canonically.
193
+ const canonicalPolicy = (policy: ProviderCostOverlay["promptLengthPricing"]): string =>
194
+ policy === undefined || policy.mode === "automatic" ? `{"mode":"automatic"}`
195
+ : JSON.stringify(policy, (_key, item: unknown) => (item && typeof item === "object" && !Array.isArray(item)
196
+ ? Object.fromEntries(Object.entries(item as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
197
+ : item));
171
198
  const receivedCost = priceRecord(result) ? result.cost : undefined;
172
199
  if (!priceRecord(result) || result.ok !== true || result.provider !== provider || result.modelId !== modelId
173
200
  || (cost === null ? receivedCost !== null : !validPriceCost(receivedCost)
174
- || !PRICE_RATE_KEYS.every(key => receivedCost[key] === cost[key]))) {
201
+ || !PRICE_RATE_KEYS.every(key => receivedCost[key] === cost[key])
202
+ || canonicalPolicy(receivedCost.promptLengthPricing) !== canonicalPolicy(cost.promptLengthPricing))) {
175
203
  throw new Error("Invalid model price persistence receipt");
176
204
  }
177
205
  // Project the acknowledged fields only; unrelated response fields are not CLI output.
@@ -566,6 +594,39 @@ async function shadow(argv: string[], deps: RuntimeApiDeps): Promise<void> {
566
594
  printData(result, wantsJson, ["Shadow-call settings updated."]);
567
595
  }
568
596
 
597
+ async function autoReview(argv: string[], deps: RuntimeApiDeps): Promise<number | void> {
598
+ const args = [...argv];
599
+ const action = (args.shift() ?? "status").toLowerCase();
600
+ const wantsJson = takeFlag(args, "--json");
601
+ if (action === "status") {
602
+ rejectArgs(args, USAGE);
603
+ const result = await runtimeRequest("/api/auto-review-settings", {}, deps);
604
+ printData(result, wantsJson);
605
+ return;
606
+ }
607
+ if (action !== "set") throw new CliUsageError("unknown auto-review action " + action, USAGE);
608
+ const modelRaw = args[0] && !args[0].startsWith("--") ? args.shift() : undefined;
609
+ const enabled = takeBooleanOption(args, "--enabled");
610
+ rejectArgs(args, USAGE);
611
+ const body: Record<string, unknown> = {};
612
+ if (modelRaw !== undefined) body.model = modelRaw === "-" ? "" : modelRaw;
613
+ if (enabled !== undefined) body.enabled = enabled;
614
+ if (Object.keys(body).length === 0) throw new CliUsageError("model and/or --enabled is required", USAGE);
615
+ const result = await runtimeRequest<{ catalogRefresh?: { status?: string; retryable?: boolean } }>(
616
+ "/api/auto-review-settings", { method: "PUT", body: JSON.stringify(body) }, deps,
617
+ );
618
+ const refresh = result.catalogRefresh;
619
+ printData(result, wantsJson, [
620
+ "Auto-review settings updated.",
621
+ ...(refresh?.status === "failed"
622
+ ? ["Settings saved, but the Codex model catalog refresh failed. Resolve the refresh issue before running ocx sync."]
623
+ : refresh?.status === "skipped" && refresh.retryable === true
624
+ ? ["Settings saved, but the Codex model catalog refresh is pending. Run ocx sync to retry."]
625
+ : []),
626
+ ]);
627
+ if (refresh?.status === "failed") return 1;
628
+ }
629
+
569
630
  export async function handleModelsRuntimeCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
570
631
  if (sub === "order" || sub === "display-name") {
571
632
  const { handleModelsOrderCommand, handleModelsDisplayNameCommand } = await import("./models-order");
@@ -574,7 +635,7 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
574
635
  // The dispatch below and MODELS_RUNTIME_SUBCOMMANDS must name the same set;
575
636
  // tests/cli/cli-models-runtime-dispatch.test.ts fails if they drift (#3094).
576
637
  if (!isModelsRuntimeSubcommand(sub)) return null;
577
- let action: (() => Promise<void>) | undefined;
638
+ let action: (() => Promise<number | void>) | undefined;
578
639
  if (sub === "live") action = () => live(argv, deps);
579
640
  else if (sub === "price") action = () => price(false, argv, deps);
580
641
  else if (sub === "set-price") action = () => price(true, argv, deps);
@@ -589,8 +650,11 @@ export async function handleModelsRuntimeCommand(sub: string, argv: string[], de
589
650
  else if (sub === "new-arrivals") action = () => newArrivals(argv, deps);
590
651
  else if (sub === "context") action = () => context(argv, deps);
591
652
  else if (sub === "shadow") action = () => shadow(argv, deps);
653
+ else if (sub === "auto-review") action = () => autoReview(argv, deps);
592
654
  if (!action) return null;
593
- return runCliAction(action);
655
+ let outcome: number | void = 0;
656
+ const exit = await runCliAction(async () => { outcome = await action(); });
657
+ return exit || outcome || 0;
594
658
  }
595
659
 
596
660
  export const MODELS_RUNTIME_USAGE = USAGE;
@@ -14,6 +14,8 @@ export interface ObserveStreamDeps extends RuntimeApiDeps {
14
14
  /** Reduced timings for isolated tests only; never command-line options. */
15
15
  pollIntervalMs?: number;
16
16
  requestTimeoutMs?: number;
17
+ /** Frozen clock for usage-compare tests; never a command-line option. */
18
+ now?: () => number;
17
19
  }
18
20
 
19
21
  export class ObservationError extends Error {}
@@ -14,9 +14,11 @@ import {
14
14
  } from "./runtime-api";
15
15
  import { followLogs } from "./log-follow";
16
16
  import { handleLogFilterCommand } from "./log-view-filter";
17
+ import { handleLogGetCommand, handleLogHistoryCommand } from "./log-history";
17
18
  import { followInjection } from "./injection-follow";
18
19
  import type { ObserveStreamDeps } from "./observe-stream";
19
20
  import { formatUsageReport } from "./usage-report";
21
+ import { handleUsageCompareCommand } from "./usage-compare";
20
22
  import { selectUsageModelView, takeUsageSearchOption } from "./usage-model-search";
21
23
  import { USAGE_RANGES, USAGE_SURFACES, type UsageSummary, type UsageFilterEcho } from "../usage/summary";
22
24
  import { parseUsageTimeWindow, type UsageTimeWindow } from "../usage/time-range";
@@ -30,12 +32,16 @@ const USAGE = `Usage:
30
32
  ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
31
33
  [--conversation <id>] [--account <label>] [--limit <n>] [--follow] [--json|--jsonl] [--events]
32
34
  ocx logs explain <request-id> [--json]
35
+ ocx logs history [selectors] [--cursor <token>] [--limit <1-100>] [--json|--jsonl]
36
+ ocx logs get <request-id> [--json]
33
37
  ocx logs filter [selectors] [--scan-limit <1..2000>] [--limit <1..2000>] [--json|--jsonl]
34
38
  ocx logs rebuild-index
35
39
  ocx logs index-status
36
40
  ocx observe usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>]
37
41
  [--since <epoch-ms|ISO-datetime>] [--until <epoch-ms|ISO-datetime>]
38
- [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--json]
42
+ [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--top <1-1000>] [--json]
43
+ ocx usage compare [--range <today|1d|7d|30d>] [--surface <all|codex|claude|grok>]
44
+ [--provider <name>] [--model <id>] [--api-key-id <id>] [--json]
39
45
  ocx observe storage [codex-logs [status|protect|unprotect|repair|compact] [--mode <compat|quiet>]] [--json]
40
46
  ocx observe memory [--json]
41
47
  ocx observe debug [--json]
@@ -174,6 +180,7 @@ async function indexStatus(argv: string[], deps: RuntimeApiDeps): Promise<void>
174
180
  async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
175
181
  const args = [...argv];
176
182
  const search = takeUsageSearchOption(args);
183
+ const top = takeIntegerOption(args, "--top", { min: 1, max: 1000 });
177
184
  const wantsJson = takeFlag(args, "--json");
178
185
  const range = takeOption(args, "--range") ?? "30d";
179
186
  const surface = takeOption(args, "--surface") ?? "all";
@@ -246,10 +253,10 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
246
253
  // the call, so passing formatUsageReport(...) inline would run the human
247
254
  // renderer during --json and let its assumptions affect a path that is meant
248
255
  // to bypass it entirely.
249
- const view = search === undefined ? undefined : selectUsageModelView(result, search);
256
+ const view = search === undefined ? undefined : selectUsageModelView(result, search, wantsJson ? undefined : top);
250
257
  const displayed = view ?? result;
251
258
  if (wantsJson) printData(displayed, true);
252
- else printData(displayed, false, formatUsageReport(displayed as Parameters<typeof formatUsageReport>[0], view ? { modelView: view.modelView } : undefined));
259
+ else printData(displayed, false, formatUsageReport(displayed as Parameters<typeof formatUsageReport>[0], { modelView: view?.modelView, modelRowLimit: top }));
253
260
  }
254
261
 
255
262
  async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
@@ -320,12 +327,17 @@ export async function handleObserveCommand(argv: string[], deps: ObserveStreamDe
320
327
  if (sub === "logs") {
321
328
  const action = rest[0];
322
329
  if (action === "filter") streamExit = await handleLogFilterCommand(rest.slice(1), deps, formatLog);
330
+ else if (action === "history") await handleLogHistoryCommand(rest.slice(1), deps, formatLog);
331
+ else if (action === "get") await handleLogGetCommand(rest.slice(1), deps, formatLog);
323
332
  else if (action === "explain") await explain(rest.slice(1), deps);
324
333
  else if (action === "rebuild-index") await rebuildIndex(rest.slice(1), deps);
325
334
  else if (action === "index-status") await indexStatus(rest.slice(1), deps);
326
335
  else streamExit = await logs(rest, deps);
327
336
  }
328
- else if (sub === "usage") await usage(rest, deps);
337
+ else if (sub === "usage") {
338
+ if (rest[0] === "compare") await handleUsageCompareCommand(rest.slice(1), deps);
339
+ else await usage(rest, deps);
340
+ }
329
341
  else if (sub === "storage") await storage(rest, deps);
330
342
  else if (sub === "memory") await simple("/api/system/memory", rest, deps);
331
343
  else if (sub === "debug") await simple("/api/debug", rest, deps);