@bitkyc08/opencodex 2.86.0 → 2.87.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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
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
+ }
@@ -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);
@@ -8,13 +8,15 @@
8
8
  * show <name> Show provider config details (secrets masked)
9
9
  * set-default <name> Change the default provider
10
10
  */
11
- import { hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
11
+ import { hasOwnProvider, isValidProviderName, loadConfig, readConfigAdmissionSnapshot, sanitizeModelCostsForDisplay, saveConfig, validateConfigCandidate, withConfigMutationLockSync } from "../config";
12
12
  import { apiKeyTransportConfigError, modelCapabilitiesConfigError, mergeModelCapabilities } from "../config/provider-validation";
13
13
  import { hasHelpFlag, printSubcommandUsage } from "./help";
14
14
  import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
15
15
  import { providerConfigSeed } from "../providers/derive";
16
16
  import { assertAnthropicInstanceLoginConfig } from "../oauth/store-anthropic-instance";
17
17
  import { dropProviderCustomModels } from "../providers/provider-id-rewrite";
18
+ import { cleanupRemovedGcpCredentials, commitProviderGcpCredentialRemoval, providerGcpCredentialMarkers } from "../providers/gcp-credential-cleanup";
19
+ import { providerCredentialInputError } from "../providers/api-keys";
18
20
  import type { OcxProviderConfig } from "../types";
19
21
  import { findLiveProxy } from "../server/proxy-liveness";
20
22
  import { syncModelsToCodex } from "../codex/sync";
@@ -223,6 +225,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
223
225
  if (registryEntry) {
224
226
  provConfig = providerConfigSeed(registryEntry);
225
227
  if (apiKey) {
228
+ const candidateError = providerCredentialInputError({ apiKey });
229
+ if (candidateError) {
230
+ console.error(`Error: ${candidateError}`);
231
+ process.exitCode = 2;
232
+ return;
233
+ }
226
234
  if ((authMode ?? registryEntry.authKind) === "forward") {
227
235
  console.warn(`Warning: provider "${name}" uses ChatGPT login (forward auth); --api-key is ignored.`);
228
236
  } else if ((authMode ?? registryEntry.authKind) === "oauth") {
@@ -249,6 +257,12 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
249
257
  }
250
258
 
251
259
  if (responsesPath !== undefined) provConfig.responsesPath = responsesPath;
260
+ const credentialError = providerCredentialInputError(provConfig);
261
+ if (credentialError) {
262
+ console.error(`Error: ${credentialError}`);
263
+ process.exitCode = 2;
264
+ return;
265
+ }
252
266
  if (authMode !== undefined) provConfig.authMode = authMode as OcxProviderConfig["authMode"];
253
267
  if (name === "anthropic2") {
254
268
  if (provConfig.adapter !== "anthropic" || provConfig.authMode !== "oauth") delete provConfig.anthropicOAuthInstance;
@@ -328,12 +342,17 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
328
342
  }
329
343
  const { initializeProviderModelSelection } = await import("../providers/initial-model-selection");
330
344
  let markerCollision = false;
345
+ let removedCredentials: string[] = [];
331
346
  withConfigMutationLockSync(() => {
332
347
  if (provConfig.anthropicOAuthInstance) {
333
348
  try { assertAnthropicInstanceLoginConfig(config, name); }
334
349
  catch { markerCollision = true; return; }
335
350
  }
336
351
  initializeProviderModelSelection(name, provConfig, existingProvider, config);
352
+ const durable = readConfigAdmissionSnapshot();
353
+ const durableProvider = durable.kind === "read" && durable.diagnostics.source === "file"
354
+ && durable.diagnostics.error === null ? durable.diagnostics.config.providers[name] : undefined;
355
+ removedCredentials = [config.providers[name], durableProvider].flatMap(provider => provider ? providerGcpCredentialMarkers(provider) : []);
337
356
  config.providers[name] = provConfig;
338
357
  if (setDefault) config.defaultProvider = name;
339
358
  validateAndSave(config);
@@ -344,6 +363,7 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
344
363
  return;
345
364
  }
346
365
 
366
+ cleanupRemovedGcpCredentials(removedCredentials);
347
367
  let sync: LocalSyncResult | undefined;
348
368
  if (wantsSync) {
349
369
  try {
@@ -440,10 +460,12 @@ function handleRemove(args: string[]): void {
440
460
  process.exit(1);
441
461
  }
442
462
 
443
- delete config.providers[name];
444
- const droppedCustomModels = dropProviderCustomModels(config, name);
445
- validateAndSave(config);
446
-
463
+ const droppedCustomModels = commitProviderGcpCredentialRemoval(config, name, () => {
464
+ delete config.providers[name];
465
+ const dropped = dropProviderCustomModels(config, name);
466
+ validateAndSave(config);
467
+ return dropped;
468
+ });
447
469
 
448
470
  if (wantsJson) {
449
471
  console.log(JSON.stringify({
@@ -8,6 +8,12 @@ export interface CliCommandEntry {
8
8
  }
9
9
 
10
10
  export const CLI_COMMANDS: CliCommandEntry[] = [
11
+ {
12
+ name: "codex-provider",
13
+ usage: "ocx codex-provider <status|allow <id> [--yes]|revoke <id>>",
14
+ summary: "Explicitly allow or revoke local routing management for a custom Codex provider.",
15
+ details: ["Consent is bound to the selected CODEX_HOME and provider ID. Only base_url is managed; transport settings and history are preserved."],
16
+ },
11
17
  {
12
18
  name: "message",
13
19
  usage: "ocx message <sessions|send> [options]",
@@ -178,7 +184,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
178
184
  summary: "Fetch provider models and inject them into Codex config.",
179
185
  details: [
180
186
  "After writing the catalog, warns if long-lived Codex app-server processes are still running.",
181
- "--restart-codex restarts the app-servers AND fully quits and relaunches the Codex desktop app on macOS, Linux and Windows, so its model picker re-reads the catalog. It ends live conversations.",
187
+ "--restart-codex restarts the app-servers AND fully quits and relaunches the Codex desktop app on macOS, Linux and Windows, after catalog/cache writes or a committed configuration change. It ends live conversations.",
182
188
  "--restart-app-server-only keeps the narrow behaviour: SIGTERM to matching app-server / code-mode-host processes, desktop app left running. It wins over --restart-codex when both are given.",
183
189
  "--restart-desktop-app is a deprecated alias of --restart-codex and prints a notice.",
184
190
  ],
@@ -226,9 +232,10 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
226
232
  },
227
233
  {
228
234
  name: "login",
229
- usage: "ocx login <provider>",
235
+ usage: "ocx login <provider> [--headless]",
230
236
  summary: "OAuth or API-key login for a provider.",
231
237
  details: [
238
+ "--headless (anthropic, anthropic2): no browser and no local port; approve in any browser and paste the displayed code.",
232
239
  "Codex/ChatGPT: ocx login codex runs the Codex account-pool login (same flow as ocx account login codex).",
233
240
  "That one needs a running proxy; the OAuth and API-key providers log in locally.",
234
241
  "'chatgpt' and 'openai' are the same route; an OpenAI platform API key is 'ocx login openai-apikey'.",
@@ -336,6 +343,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
336
343
  "add-key <provider> [--label <label>] Add a key read only from piped stdin.",
337
344
  "pool <provider> Read/edit runtime-supported pool policy.",
338
345
  "credits openai ... Explicit paid-credit-after-limit policy; separate from displaying balances.",
346
+ "use-remaining openai <id|main> <on|off|status> Use included quota up to its limit.",
339
347
  "quota-activation openai Enable/disable one account reset-window refresh policy.",
340
348
  "anthropic-reset-grants Read grant status only; consumption remains in the dashboard.",
341
349
  "login supports explicit browser/add-account options only for applicable flows; human verification remains required.",
@@ -444,11 +452,21 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
444
452
  "`inspect star` reads the repository star status only. Starring uses your GitHub identity and is available from the dashboard alone.",
445
453
  ],
446
454
  },
447
- { name: "logs", usage: "ocx logs [filters] [--follow] [--json|--jsonl|--events]", summary: "Read or follow request logs." },
455
+ {
456
+ name: "logs",
457
+ usage: "ocx logs [filters] [--follow] [--json|--jsonl|--events]; ocx logs history [selectors] [--cursor <token>] [--json|--jsonl]; ocx logs get <request-id> [--json]",
458
+ summary: "Read or follow request logs.",
459
+ details: [
460
+ "Default reads the recent ring. `history` pages the derived index with a resumable cursor; `get` reads one canonical row; `explain` is the route-decision view.",
461
+ ],
462
+ },
448
463
  {
449
464
  name: "usage",
450
- usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--json]",
465
+ usage: "ocx usage [--range <today|1d|7d|30d|all>] [--surface <all|codex|claude|grok>] [--provider <name>] [--model <id>] [--api-key-id <id>] [--search <text>] [--top <1-1000>] [--json]; ocx usage compare [--range <today|1d|7d|30d>] [--json]",
451
466
  summary: "Report token usage and estimated cost (alias of ocx observe usage).",
467
+ details: [
468
+ "`compare` reads the current named range and the previous equal window; `--range all` is refused.",
469
+ ],
452
470
  },
453
471
  {
454
472
  name: "storage",
@@ -486,13 +504,14 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
486
504
  },
487
505
  {
488
506
  name: "export",
489
- usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid> [--json] [--out <path>] [--force]",
490
- summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder) wired to the running proxy.",
507
+ usage: "ocx export --client <qoder|opencode|pi|omp|hermes|openclaw|kimi|gajae|dsh|mcode|zcode|commandcode|prime|aside|raycast|omo|cline|kilo|droid|gentle-shell> [--json] [--out <path>] [--force]",
508
+ summary: "Print a client config (OpenCode, Pi, OMP, Hermes, OpenClaw, Kimi Code, gjc, DeepSeek Harness, MiniMax Code, ZCode, Command Code, Prime Agent, Aside, Raycast, omo, Cline, Kilo, Factory Droid, Qoder, Gentle Shell) wired to the running proxy.",
491
509
  details: [
492
510
  "--json prints the generated document as JSON on stdout; use --out for the client's native format.",
493
511
  "--out <path> writes the native config there and refuses to replace an existing file without --force.",
494
512
  "Cline exports a two-document bundle: settings for providers.json and catalog for sibling models.json. Use integration client enable --client cline for a journaled write.",
495
513
  "Droid exports documented customModels for settings.json; use integration client enable --client droid for a journaled write.",
514
+ "Gentle Shell uses its persistent selected home; use integration client enable --client gentle-shell for a journaled write, or Pi for link/shared homes.",
496
515
  "The config never contains a real key; it carries a documented env reference or a non-secret loopback placeholder.",
497
516
  "The destination path is printed for merging by hand — ocx never writes your real client config.",
498
517
  ],
@@ -516,14 +535,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
516
535
  {
517
536
  name: "system",
518
537
  usage: "ocx system <status|health|settings|startup|diagnostics|sync|codex-app-server|codex-restart|update|codex-cli-update> ...",
519
- summary: "Manage headless runtime settings, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection.",
538
+ summary: "Manage headless runtime settings, health, startup, sync, diagnostics, OpenCodex updates, and read-only Codex CLI inspection and planning.",
520
539
  details: [
521
540
  "system update manages OpenCodex itself.",
522
541
  "system health reads authenticated service and spend-ledger diagnostics; root health remains liveness.",
523
542
  "ocx system codex-cli-update check [--json]",
524
543
  "ocx system codex-cli-update attest [--json]",
525
544
  "ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]",
526
- "The Codex CLI inspection command makes no package-registry request, does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.",
545
+ "ocx system codex-cli-update plan [--channel latest] [--json]",
546
+ "check and attest make no package-registry request, do not execute Codex or npm, and write nothing. plan adds a registry query and a process-table read and mutates no application state (temporary isolated npm files only, removed best-effort).",
527
547
  ],
528
548
  },
529
549
  {