@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,76 @@
1
+ /** Whitelist native text completions; never serialize arbitrary provider payloads. */
2
+ import { redactSelectedKey } from "./access-data-client";
3
+ export type DataProtocol = "chat" | "responses" | "messages";
4
+ export interface SafeModelResponse {
5
+ protocol: DataProtocol;
6
+ text: string[];
7
+ completion: "complete" | "limited";
8
+ usage?: { inputTokens?: number; outputTokens?: number; totalTokens?: number };
9
+ }
10
+ function record(value: unknown): Record<string, unknown> | undefined {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
12
+ }
13
+ function usageOf(raw: unknown, protocol: DataProtocol): SafeModelResponse["usage"] {
14
+ if (raw === undefined || raw === null) return undefined;
15
+ const usage = record(raw);
16
+ if (!usage) throw new Error("Invalid usage.");
17
+ const result: NonNullable<SafeModelResponse["usage"]> = {};
18
+ const fields = protocol === "chat"
19
+ ? { inputTokens: "prompt_tokens", outputTokens: "completion_tokens", totalTokens: "total_tokens" } as const
20
+ : { inputTokens: "input_tokens", outputTokens: "output_tokens", totalTokens: "total_tokens" } as const;
21
+ for (const name of ["inputTokens", "outputTokens", "totalTokens"] as const) {
22
+ const value = usage[fields[name]];
23
+ if (value === undefined) continue;
24
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid usage.");
25
+ result[name] = value;
26
+ }
27
+ return Object.keys(result).length ? result : undefined;
28
+ }
29
+ export function projectModelResponse(raw: unknown, protocol: DataProtocol, key: string): SafeModelResponse {
30
+ const row = record(raw);
31
+ if (!row || (row.error !== undefined && row.error !== null)) throw new Error("Unsupported response.");
32
+ const text: string[] = [];
33
+ let limited = false;
34
+ if (protocol === "responses") {
35
+ if (row.object !== "response" || (row.status !== "completed" && row.status !== "incomplete") || !Array.isArray(row.output)) throw new Error("Unsupported response.");
36
+ if (record(row.incomplete_details)?.reason === "content_filter") throw new Error("Unsupported response.");
37
+ limited = row.status === "incomplete";
38
+ for (const value of row.output) {
39
+ const item = record(value);
40
+ if (!item || item.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content)) {
41
+ // Reasoning/tool entries are not text evidence and are never exposed.
42
+ continue;
43
+ }
44
+ for (const blockValue of item.content) {
45
+ const block = record(blockValue);
46
+ if (block?.type !== "output_text" || typeof block.text !== "string") throw new Error("Unsupported response.");
47
+ text.push(block.text);
48
+ }
49
+ }
50
+ } else if (protocol === "chat") {
51
+ if (row.object !== "chat.completion" || !Array.isArray(row.choices) || row.choices.length === 0) throw new Error("Unsupported response.");
52
+ for (const value of row.choices) {
53
+ const choice = record(value);
54
+ const message = record(choice?.message);
55
+ if (!choice || (choice.finish_reason !== "stop" && choice.finish_reason !== "length")
56
+ || !message || message.role !== "assistant" || typeof message.content !== "string"
57
+ || (message.refusal !== undefined && message.refusal !== null)) throw new Error("Unsupported response.");
58
+ limited ||= choice.finish_reason === "length";
59
+ text.push(message.content);
60
+ }
61
+ } else {
62
+ if (row.type !== "message" || row.role !== "assistant" || !Array.isArray(row.content)
63
+ || (row.stop_reason !== "end_turn" && row.stop_reason !== "stop_sequence" && row.stop_reason !== "max_tokens")) {
64
+ throw new Error("Unsupported response.");
65
+ }
66
+ limited = row.stop_reason === "max_tokens";
67
+ for (const value of row.content) {
68
+ const block = record(value);
69
+ if (block?.type === "text" && typeof block.text === "string") text.push(block.text);
70
+ else if (block?.type !== "thinking" && block?.type !== "redacted_thinking") throw new Error("Unsupported response.");
71
+ }
72
+ }
73
+ if (!text.length) throw new Error("Unsupported response.");
74
+ const usage = usageOf(row.usage, protocol);
75
+ return { protocol, text: text.map(value => redactSelectedKey(value, key)), completion: limited ? "limited" : "complete", ...(usage ? { usage } : {}) };
76
+ }
package/src/cli/access.ts CHANGED
@@ -4,15 +4,20 @@ import {
4
4
  rejectArgs,
5
5
  runCliAction,
6
6
  runtimeRequest,
7
+ runtimeBaseUrl,
7
8
  takeFlag,
8
9
  takeOption,
9
10
  type RuntimeApiDeps,
10
11
  } from "./runtime-api";
11
12
 
13
+ import { handleSelectedKeyTest, type SelectedKeyTestDeps } from "./access-data-plane";
14
+ import { handleAccessAudioCommand } from "./access-audio";
15
+
12
16
  const USAGE = `Usage:
13
17
  ocx access key [list] [--json]
14
18
  ocx access key create [name] [--json]
15
19
  ocx access key get <id-or-name> [--json]
20
+ ocx access key rename <id-or-name> <name> [--json]
16
21
  ocx access key set <id-or-name> [--allow-provider <name>]... [--allow-model <id>]... [--clear] [--json]
17
22
  ocx access key rotate <id> [--json]
18
23
  ocx access key rotate commit <id> <rotation-id> [--json]
@@ -20,7 +25,9 @@ const USAGE = `Usage:
20
25
  ocx access key remove <id> --yes [--json]
21
26
  ocx access endpoints [--json]
22
27
  ocx access models [--json]
23
- ocx access test <model> [--protocol <chat|responses|messages>] [--json]`;
28
+ ocx access test <model> [--protocol <chat|responses|messages>] [--api-key-stdin] [--json]
29
+ ocx access audio transcribe <file> --model <id> --api-key-stdin [--json]
30
+ ocx access audio live-check --model <id> --api-key-stdin [--json]`;
24
31
 
25
32
  const UTC_ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
26
33
 
@@ -234,6 +241,56 @@ async function key(argv: string[], deps: RuntimeApiDeps): Promise<void> {
234
241
  throw new CliUsageError(`unknown key command ${action}`, USAGE);
235
242
  }
236
243
 
244
+ /** Rename never sends or prints a secret, and cannot silently choose duplicate ids. */
245
+ async function renameKey(argv: string[], deps: RuntimeApiDeps): Promise<number> {
246
+ const args = [...argv];
247
+ const wantsJson = takeFlag(args, "--json");
248
+ const [selector, rawName] = args;
249
+ if (args.length !== 2 || !selector?.trim() || selector.startsWith("-") || !rawName
250
+ || /[\u0000-\u001f\u007f]/.test(rawName) || !rawName.trim() || rawName.trim().length > 64) {
251
+ console.error("Error: Supply one key id or unambiguous name and a new name of 1–64 characters without controls.");
252
+ return 2;
253
+ }
254
+ const name = rawName.trim();
255
+ try {
256
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
257
+ const before = await runtimeRequest<unknown>("/api/keys", { redirect: "error", credentials: "omit" }, pinned);
258
+ if (!before || typeof before !== "object" || !("keys" in before) || !Array.isArray(before.keys)
259
+ || before.keys.some(row => !row || typeof row !== "object" || Array.isArray(row)
260
+ || typeof row.id !== "string" || !row.id || typeof row.name !== "string")) throw new Error();
261
+ const rows = before.keys as Array<Record<string, unknown>>;
262
+ const wanted = selector.trim().toLowerCase();
263
+ if (rows.filter(row => (row.id as string).toLowerCase() === wanted).length > 1) {
264
+ throw new CliUsageError("Ambiguous key identity.");
265
+ }
266
+ const target = findKeyRow(rows, selector);
267
+ if (rows.filter(row => (row.id as string).toLowerCase() === (target.id as string).toLowerCase()).length !== 1) {
268
+ throw new CliUsageError("Ambiguous key identity.");
269
+ }
270
+ const result = await runtimeRequest<Record<string, unknown>>("/api/keys", {
271
+ method: "PATCH", body: JSON.stringify({ id: target.id, name }), redirect: "error", credentials: "omit",
272
+ }, pinned);
273
+ if (!result || typeof result !== "object" || Array.isArray(result) || result.id !== target.id || result.name !== name
274
+ || typeof result.createdAt !== "string" || !isUtcIsoInstant(result.createdAt)) throw new Error();
275
+ const projected: Record<string, unknown> = { id: result.id, name: result.name, createdAt: result.createdAt };
276
+ for (const scope of ["allowedProviders", "allowedModels"] as const) {
277
+ if (result[scope] === undefined) continue;
278
+ const value = result[scope];
279
+ if (!Array.isArray(value) || value.some(item => typeof item !== "string" || !item.trim() || item.length > 256)) throw new Error();
280
+ projected[scope] = [...value];
281
+ }
282
+ printData(projected, wantsJson, ["API key renamed.", ...scopeLines(projected)]);
283
+ return 0;
284
+ } catch (error) {
285
+ if (error instanceof CliUsageError) {
286
+ console.error("Error: API key selector is missing or ambiguous. List keys and use a unique id.");
287
+ return 2;
288
+ }
289
+ console.error("Error: API key rename was not confirmed. List keys to check the current name before retrying.");
290
+ return 1;
291
+ }
292
+ }
293
+
237
294
  async function endpoints(argv: string[], deps: RuntimeApiDeps): Promise<void> {
238
295
  const args = [...argv];
239
296
  const wantsJson = takeFlag(args, "--json");
@@ -271,7 +328,13 @@ async function testModel(argv: string[], deps: RuntimeApiDeps): Promise<void> {
271
328
  printData(result, wantsJson, [`${model}: ${protocol} request succeeded.`]);
272
329
  }
273
330
 
274
- export async function handleAccessCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
331
+ export async function handleAccessCommand(argv: string[], deps: SelectedKeyTestDeps = {}): Promise<number> {
332
+ const [command, ...args] = argv;
333
+ if (command === "audio") return handleAccessAudioCommand(args, deps);
334
+ if (command === "test" && args.some(arg => arg === "--api-key-stdin" || arg.startsWith("--api-key-stdin="))) {
335
+ return handleSelectedKeyTest(args, deps);
336
+ }
337
+ if ((command === "key" || command === "keys") && args[0] === "rename") return renameKey(args.slice(1), deps);
275
338
  return runCliAction(async () => {
276
339
  const [sub = "key", ...rest] = argv;
277
340
  if (sub === "key" || sub === "keys") await key(rest, deps);
@@ -1,4 +1,4 @@
1
- import { parseQuotaFailureCode, type QuotaFailureCode } from "../providers/quota-types";
1
+ import { parseQuotaFailureCode, type QuotaFailureCode, type ProviderQuota, type AccountQuotaMode } from "../providers/quota-types";
2
2
  /**
3
3
  * Data-access layer for `ocx account` (issue #180) — live-proxy HTTP client and
4
4
  * per-family account readers. Kept separate from account.ts (command handlers)
@@ -11,6 +11,10 @@ import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers
11
11
  import type { OcxConfig } from "../types";
12
12
  import { projectCodexQuotaRefreshOutcome, type CodexQuotaRefreshOutcome } from "../codex/quota-refresh-outcome";
13
13
 
14
+ import { projectApiKeyQuotaRows } from "./account-key-quota";
15
+ import { projectAccountHealth } from "./account-next-actions";
16
+ import type { OAuthHealthLabel } from "../oauth/health";
17
+
14
18
  export type AccountType = "codex" | "oauth" | "api-key";
15
19
 
16
20
  export interface AccountRow {
@@ -27,6 +31,12 @@ export interface AccountRow {
27
31
  masked?: string;
28
32
  active: boolean;
29
33
  needsReauth?: boolean;
34
+ /** Validated server health label and locally generated recovery guidance. */
35
+ health?: OAuthHealthLabel;
36
+ healthAction?: string;
37
+ /** Explicit paid-credit consent, reported only when supplied by the Codex API. */
38
+ creditsAfterLimit?: boolean;
39
+ needsReauthReason?: "verify_account";
30
40
  autoSelectable?: boolean;
31
41
  skipReason?: "paused" | "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
32
42
  selectionExcludedReason?: "plan_excluded";
@@ -37,7 +47,8 @@ export interface AccountRow {
37
47
  priority?: number;
38
48
  /** Null means the account inherits the global usage-switch threshold. */
39
49
  autoSwitchThresholdOverride?: number | null;
40
- quota?: CodexQuotaDto | null;
50
+ quota?: (CodexQuotaDto & Partial<ProviderQuota>) | null;
51
+ quotaMode?: AccountQuotaMode;
41
52
  quotaRefresh?: CodexQuotaRefreshOutcome;
42
53
  quotaUnavailable?: boolean;
43
54
  quotaFailure?: QuotaFailureCode;
@@ -122,7 +133,7 @@ export async function apiJson(
122
133
  method: "GET" | "PUT" | "POST" | "DELETE",
123
134
  path: string,
124
135
  body?: unknown,
125
- options: { signal?: AbortSignal } = {},
136
+ options: { signal?: AbortSignal; redirect?: RequestRedirect } = {},
126
137
  ): Promise<ApiResult> {
127
138
  const fetchImpl = deps.fetchImpl ?? fetch;
128
139
  try {
@@ -131,6 +142,7 @@ export async function apiJson(
131
142
  headers: runningProxyUpdateHeaders(),
132
143
  body: body === undefined ? undefined : JSON.stringify(body),
133
144
  signal: options.signal,
145
+ ...(options.redirect ? { redirect: options.redirect } : {}),
134
146
  });
135
147
  const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
136
148
  return { status: res.status, json };
@@ -255,6 +267,8 @@ interface CodexAccountDto {
255
267
  selectionExcludedReason?: "plan_excluded";
256
268
  selectionExcludedPlan?: string;
257
269
  health?: { reason?: string };
270
+ healthLabel?: unknown;
271
+ creditsAfterLimit?: unknown;
258
272
  priority?: number;
259
273
  autoSwitchThresholdOverride?: number | null;
260
274
  quota?: CodexQuotaDto | null;
@@ -321,6 +335,8 @@ export async function fetchCodexRows(
321
335
  plan: a.plan,
322
336
  active: a.id === activeId,
323
337
  needsReauth: a.needsReauth,
338
+ ...projectAccountHealth(a, "openai", a.id),
339
+ ...(typeof a.creditsAfterLimit === "boolean" ? { creditsAfterLimit: a.creditsAfterLimit } : {}),
324
340
  ...(a.selectionExcludedReason === "plan_excluded" ? {
325
341
  selectionExcludedReason: "plan_excluded" as const,
326
342
  ...(typeof a.selectionExcludedPlan === "string" ? { selectionExcludedPlan: a.selectionExcludedPlan } : {}),
@@ -345,9 +361,11 @@ interface OAuthAccountDto {
345
361
  email?: string;
346
362
  active?: boolean;
347
363
  needsReauth?: boolean;
364
+ healthLabel?: unknown;
348
365
  /** Present only for providers that support operator pause (generic OAuth pools). */
349
366
  paused?: boolean;
350
367
  autoSwitchThresholdOverride?: number | null;
368
+ needsReauthReason?: "verify_account";
351
369
  autoSelectable?: boolean;
352
370
  skipReason?: unknown;
353
371
  /** Always sent by the management route; explicitly `null` when the tier is unknown. */
@@ -388,8 +406,10 @@ async function fetchOAuthRows(
388
406
  email: a.email,
389
407
  active: a.active ?? a.id === activeId,
390
408
  needsReauth: a.needsReauth,
409
+ ...projectAccountHealth(a, name, a.id),
391
410
  ...(a.paused === true ? { paused: true } : {}),
392
411
  ...(name === "anthropic" && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
412
+ ...(a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
393
413
  ...(name === "kiro" && typeof a.autoSelectable === "boolean"
394
414
  ? { autoSelectable: a.autoSelectable } : {}),
395
415
  ...(name === "kiro" && a.autoSelectable === false && isKiroSkipReason(a.skipReason)
@@ -413,12 +433,19 @@ interface ApiKeyDto {
413
433
  active?: boolean;
414
434
  }
415
435
 
416
- async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): Promise<FamilyRows> {
417
- const res = await apiJson(deps, baseUrl, "GET", `/api/providers/keys?name=${encodeURIComponent(name)}`);
436
+ async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string, quota?: { refresh?: boolean }): Promise<FamilyRows> {
437
+ const query = `?name=${encodeURIComponent(name)}${quota ? `&quota=1${quota.refresh ? "&refresh=1" : ""}` : ""}`;
438
+ const res = await apiJson(deps, baseUrl, "GET", `/api/providers/keys${query}`, undefined, quota ? { redirect: "error" } : {});
418
439
  if (res.status === 0) {
419
440
  return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
420
441
  }
421
442
  if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
443
+ if (quota) {
444
+ try { return projectApiKeyQuotaRows(res.json, name); }
445
+ catch (error) {
446
+ return { rows: [], activeId: null, status: 200, errorJson: { error: error instanceof Error ? error.message : "Malformed API-key quota response." } };
447
+ }
448
+ }
422
449
  const activeId = typeof res.json.activeId === "string" ? res.json.activeId : null;
423
450
  const keys = Array.isArray(res.json.keys) ? res.json.keys as ApiKeyDto[] : [];
424
451
  const rows = keys.map(k => ({
@@ -441,7 +468,7 @@ export function fetchRows(
441
468
  ): Promise<FamilyRows> {
442
469
  if (type === "codex") return fetchCodexRows(deps, baseUrl, Boolean(quota?.refresh), quota !== undefined);
443
470
  if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
444
- return fetchKeyRows(deps, baseUrl, name);
471
+ return fetchKeyRows(deps, baseUrl, name, quota);
445
472
  }
446
473
 
447
474
  export async function fetchProviderQuotaReport(
@@ -1,4 +1,6 @@
1
1
  import { writeSync } from "node:fs";
2
+ import { isValidProviderName } from "../config/provider-name";
3
+ import { runCatalogAction } from "./catalog-command-result";
2
4
  import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance";
3
5
  import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
4
6
  import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
@@ -10,6 +12,8 @@ import {
10
12
  rejectArgs,
11
13
  runCliAction,
12
14
  runtimeRequest,
15
+ runtimeBaseUrl,
16
+ terminalSafeText,
13
17
  takeFlag,
14
18
  takeOption,
15
19
  takeOptionWithSyntax,
@@ -34,7 +38,7 @@ function writeStdoutFully(text: string): void {
34
38
  }
35
39
 
36
40
  const USAGE = `Usage:
37
- ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--method builder-id|google|github] [--code -] [--no-wait] [--json]
41
+ ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--method builder-id|google|github] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json]
38
42
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
39
43
  ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
40
44
  ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
@@ -81,6 +85,62 @@ interface LoginStart {
81
85
  warning?: string;
82
86
  }
83
87
 
88
+ /** Public handoff only. Never spread runtime bodies into terminal or JSON output. */
89
+ function loginObject(value: unknown): Record<string, unknown> {
90
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid login reply");
91
+ return value as Record<string, unknown>;
92
+ }
93
+ function publicLoginStart(value: unknown, kiro = false): LoginStart {
94
+ const data = loginObject(value);
95
+ const out: LoginStart = {};
96
+ const strings = kiro
97
+ ? ["flowId", "userCode", "verificationUri", "verificationUriComplete"] as const
98
+ : ["url", "flowId", "instructions", "deviceCode"] as const;
99
+ for (const key of strings) {
100
+ if (data[key] !== undefined && typeof data[key] !== "string") throw new Error("Invalid login handoff field");
101
+ if (typeof data[key] === "string") out[key] = data[key];
102
+ }
103
+ if (typeof data.browserLaunch === "string" && ["started", "failed", "skipped"].includes(data.browserLaunch)) out.browserLaunch = data.browserLaunch as LoginStart["browserLaunch"];
104
+ if (typeof data.method === "string" && ["builder-id", "google", "github"].includes(data.method)) out.method = data.method as LoginStart["method"];
105
+ if (typeof data.state === "string" && ["pending", "done", "failed", "expired", "cancelled"].includes(data.state)) out.state = data.state as string;
106
+ if (typeof data.warning === "string" && ["duplicate_profile_arn", "manual_review_required"].includes(data.warning)) out.warning = data.warning as string;
107
+ if (typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt)) out.expiresAt = data.expiresAt;
108
+ return out;
109
+ }
110
+ function publicLoginStatus(value: unknown): Record<string, unknown> {
111
+ const data = loginObject(value);
112
+ const out: Record<string, unknown> = {};
113
+ for (const key of ["accountId", "activeAccountId", "email"]) if (typeof data[key] === "string") out[key] = data[key];
114
+ for (const key of ["loggedIn", "done", "needsReauth", "validationPending", "catalogRefreshPending"]) {
115
+ if (data[key] !== undefined && typeof data[key] !== "boolean") throw new Error("Invalid login completion field");
116
+ if (typeof data[key] === "boolean") out[key] = data[key];
117
+ }
118
+ if (typeof data.status === "string" && ["starting", "pending", "done", "error", "expired", "cancelled", "idle"].includes(data.status)) out.status = data.status;
119
+ if (typeof data.source === "string" && ["oauth", "local-cli", "credential-file", "environment", "manual"].includes(data.source)) out.source = data.source;
120
+ if (data.hint && typeof data.hint === "object" && !Array.isArray(data.hint)) out.hint = publicLoginStart(data.hint);
121
+ if (Array.isArray(data.accounts)) out.accounts = data.accounts.map(value => {
122
+ const row = loginObject(value);
123
+ const account: Record<string, unknown> = {};
124
+ for (const key of ["id", "alias", "email"]) if (typeof row[key] === "string") account[key] = row[key];
125
+ for (const key of ["active", "needsReauth"]) if (typeof row[key] === "boolean") account[key] = row[key];
126
+ if (row.needsReauthReason === "verify_account") account.needsReauthReason = "verify_account";
127
+ if (typeof row.expiresAt === "number" && Number.isFinite(row.expiresAt)) account.expiresAt = row.expiresAt;
128
+ if (row.plan === null) account.plan = null;
129
+ return account;
130
+ });
131
+ return out;
132
+ }
133
+ function loginBoolean(args: string[], flag: string): boolean | undefined {
134
+ const option = takeOptionWithSyntax(args, flag);
135
+ if (!option) return undefined;
136
+ if (option.value !== "on" && option.value !== "off") throw new CliUsageError(`${flag} must be on or off`, USAGE);
137
+ return option.value === "on";
138
+ }
139
+ function writeLoginInstructions(lines: readonly string[]): void {
140
+ const block = lines.filter(line => line !== "").flatMap(line => line.split("\n")).map(terminalSafeText).join("\n");
141
+ if (block) writeStdoutFully(`${block}\n`);
142
+ }
143
+
84
144
  /**
85
145
  * Said only when the host could not open a browser (#5261).
86
146
  *
@@ -136,7 +196,7 @@ async function resolveCode(
136
196
  return await readSecretLine(deps, "authorization code");
137
197
  }
138
198
 
139
- async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
199
+ async function login(argv: string[], deps: RuntimeApiDeps): Promise<number | void> {
140
200
  const args = [...argv];
141
201
  const provider = args.shift()?.trim().toLowerCase();
142
202
  const wantsJson = takeFlag(args, "--json");
@@ -146,34 +206,39 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
146
206
  const method = takeOption(args, "--method");
147
207
  const id = takeOption(args, "--id");
148
208
  const suppliedCode = takeOptionWithSyntax(args, "--code");
209
+ const openBrowser = loginBoolean(args, "--open-browser");
210
+ const addAccount = loginBoolean(args, "--add-account");
211
+ if (provider && !isValidProviderName(provider)) throw new CliUsageError("invalid provider name", USAGE);
149
212
  if (!provider) throw new CliUsageError("provider is required", USAGE);
150
213
  if (method !== undefined) {
151
214
  if (provider !== "kiro" || !["builder-id", "google", "github"].includes(method)) {
152
215
  throw new CliUsageError("--method requires kiro and builder-id, google, or github", USAGE);
153
216
  }
154
217
  if (reauth || id) throw new CliUsageError("native Kiro device login only adds accounts; remove and re-add to reauthenticate", USAGE);
218
+ if (openBrowser !== undefined || addAccount !== undefined) throw new CliUsageError("native Kiro --method does not accept --open-browser or --add-account", USAGE);
155
219
  if (device || suppliedCode) throw new CliUsageError("--method cannot be combined with --device or --code", USAGE);
156
220
  }
157
- // A bare leftover here is plausibly the authorization code itself: this flow takes one
158
- // through --code, and a user who pastes it as a positional would otherwise see it echoed
159
- // back in the usage error. `ocx login codex` reaches this parser too, so the paste lands
160
- // one word away from a command people run constantly. Flag-shaped leftovers stay visible,
161
- // because a mistyped flag is exactly what the message has to name.
162
- rejectArgs(args, USAGE, { redactValues: true });
221
+ // A malformed leftover may contain pasted authorization input, even inside an
222
+ // unknown flag. New login parsing reports only a fixed usage diagnostic.
223
+ if (args.length) throw new CliUsageError("Unexpected arguments or repeated options", USAGE);
163
224
  // kimi, nous, and github-copilot are already device flows, so --device is a
164
225
  // true statement about them and is accepted as a no-op rather than an error.
165
226
  // Anything else has no device grant at all and must fail loudly.
166
227
  if (device && !CODEX_NAMES.has(provider) && !DEVICE_NATIVE_PROVIDERS.has(provider)) {
167
- throw new CliUsageError(`--device is not supported for provider '${provider}'`, USAGE);
228
+ throw new CliUsageError("--device is not supported for this provider", USAGE);
168
229
  }
230
+ if (addAccount !== undefined && (reauth || CODEX_NAMES.has(provider))) throw new CliUsageError("--add-account is only valid for fresh provider OAuth login", USAGE);
231
+ if (openBrowser !== undefined && (device || DEVICE_NATIVE_PROVIDERS.has(provider))) throw new CliUsageError("--open-browser is not supported for device login", USAGE);
232
+ if (!CODEX_NAMES.has(provider) && id && !reauth) throw new CliUsageError("--id is only valid with --reauth for provider OAuth accounts", USAGE);
169
233
  // Only resolve when --code was actually given: a plain `ocx account login`
170
234
  // opens the browser flow and polls, and must not block on stdin.
171
235
  const code = await resolveCode(suppliedCode, deps, false);
236
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
172
237
 
173
238
  if (method) {
174
- const start = await runtimeRequest<LoginStart>("/api/oauth/login", {
175
- method: "POST", body: JSON.stringify({ provider: "kiro", method }),
176
- }, deps);
239
+ const start = publicLoginStart(await runtimeRequest<unknown>("/api/oauth/login", {
240
+ method: "POST", redirect: "error", body: JSON.stringify({ provider: "kiro", method }),
241
+ }, pinned), true);
177
242
  if (!wantsJson) {
178
243
  const block = formatKiroDeviceInstructions(start);
179
244
  if (block) writeStdoutFully(`${block}\n`);
@@ -182,7 +247,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
182
247
  if (!start.flowId) throw new CliUsageError("Kiro device login did not return a flow id");
183
248
  for (let attempt = 0; attempt < 450; attempt++) {
184
249
  await Bun.sleep(2_000);
185
- const state = await runtimeRequest<LoginStart>(`/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(start.flowId)}`, {}, deps);
250
+ const state = publicLoginStart(await runtimeRequest<unknown>(`/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(start.flowId)}`, { redirect: "error" }, pinned), true);
186
251
  if (state.state === "done") {
187
252
  printData(state, wantsJson, ["Logged in to kiro.", ...(state.warning ? [`Warning: ${state.warning}`] : [])]);
188
253
  return;
@@ -195,31 +260,33 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
195
260
  }
196
261
 
197
262
  if (CODEX_NAMES.has(provider)) {
198
- const start = await runtimeRequest<LoginStart>("/api/codex-auth/login", {
199
- method: "POST",
263
+ const start = publicLoginStart(await runtimeRequest<unknown>("/api/codex-auth/login", {
264
+ method: "POST", redirect: "error",
200
265
  body: JSON.stringify({
201
266
  ...(id ? { id } : {}),
202
267
  ...(reauth ? { reauth: true } : {}),
203
268
  ...(device ? { device: true } : {}),
269
+ ...(openBrowser === undefined ? {} : { openBrowser }),
204
270
  }),
205
- }, deps);
271
+ }, pinned));
206
272
  if (!wantsJson) {
207
273
  // One atomic pre-poll block, flushed synchronously so a piped parent
208
274
  // reads the URL before the polling window starts (#1007).
209
- const block = [
275
+ writeLoginInstructions([
210
276
  start.url ? `Open this URL to sign in:\n${start.url}` : "",
211
277
  start.deviceCode ? `Device code: ${start.deviceCode}` : "",
212
278
  start.instructions ?? "",
213
279
  start.browserLaunch === "failed" ? BROWSER_LAUNCH_FAILED_HINT : "",
214
280
  start.flowId ? `Flow: ${start.flowId}` : "",
215
- ].filter(line => line !== "").join("\n");
216
- if (block) writeStdoutFully(`${block}\n`);
281
+ ]);
217
282
  }
283
+ if (code && !start.flowId) throw new Error("Code submission requires a returned flow id");
218
284
  if (code && start.flowId) {
219
- await runtimeRequest("/api/codex-auth/login/code", {
220
- method: "POST",
285
+ const submitted = await runtimeRequest("/api/codex-auth/login/code", {
286
+ method: "POST", redirect: "error",
221
287
  body: JSON.stringify({ flowId: start.flowId, input: code }),
222
- }, deps);
288
+ }, pinned);
289
+ if (loginObject(submitted).ok !== true) throw new Error("Code submission was not accepted");
223
290
  }
224
291
  if (noWait) {
225
292
  printData({ ...start, modelSelection: modelSelectionNextSteps("openai", true) }, wantsJson, modelSelectionGuidance("openai", true));
@@ -233,10 +300,11 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
233
300
  const maxAttempts = device ? 480 : 150;
234
301
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
235
302
  await Bun.sleep(2_000);
236
- const state = await runtimeRequest<Record<string, unknown>>(
303
+ const state = publicLoginStatus(await runtimeRequest<unknown>(
237
304
  `/api/codex-auth/login-status?flowId=${encodeURIComponent(start.flowId)}${id ? `&accountId=${encodeURIComponent(id)}` : ""}${reauth ? "&reauth=1" : ""}`,
238
- {}, deps,
239
- );
305
+ { redirect: "error" }, pinned,
306
+ ));
307
+ if (start.flowId) state.flowId = start.flowId;
240
308
  if (state.status === "done") {
241
309
  if (state.validationPending === true) {
242
310
  printData({ ...state, recoveryCommand: "ocx gui", recoveryAction: "After quota recovers, click Refresh quotas in the dashboard Codex account pool." }, wantsJson, [
@@ -247,33 +315,32 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
247
315
  printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
248
316
  }
249
317
  if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
250
- return;
318
+ return state.validationPending === true || state.catalogRefreshPending === true ? 1 : 0;
251
319
  }
252
- if (state.status === "error" || state.status === "expired") {
253
- throw new CliUsageError(String(state.error ?? `login ${state.status}`));
320
+ if (state.status === "error" || state.status === "expired" || state.status === "cancelled") {
321
+ throw new Error("Login failed or expired");
254
322
  }
255
323
  }
256
324
  throw new CliUsageError("login timed out");
257
325
  }
258
326
 
259
- if (id && !reauth) throw new CliUsageError("--id is only valid with --reauth for provider OAuth accounts", USAGE);
260
- const start = await runtimeRequest<LoginStart>("/api/oauth/login", {
261
- method: "POST",
262
- body: JSON.stringify({ provider, addAccount: !reauth, ...(reauth && id ? { accountId: id, reauth: true } : {}) }),
263
- }, deps);
327
+ const start = publicLoginStart(await runtimeRequest<unknown>("/api/oauth/login", {
328
+ method: "POST", redirect: "error",
329
+ body: JSON.stringify({ provider, addAccount: addAccount ?? !reauth, ...(openBrowser === undefined ? {} : { openBrowser }), ...(reauth && id ? { accountId: id, reauth: true } : {}) }),
330
+ }, pinned));
264
331
  if (!wantsJson) {
265
- const block = [
332
+ writeLoginInstructions([
266
333
  start.url ? `Open this URL to sign in:\n${start.url}` : "",
267
334
  start.instructions ?? "",
268
335
  start.deviceCode ? `Device code: ${start.deviceCode}` : "",
269
- ].filter(line => line !== "").join("\n");
270
- if (block) writeStdoutFully(`${block}\n`);
336
+ ]);
271
337
  }
272
338
  if (code) {
273
- await runtimeRequest("/api/oauth/login/code", {
274
- method: "POST",
339
+ const submitted = await runtimeRequest("/api/oauth/login/code", {
340
+ method: "POST", redirect: "error",
275
341
  body: JSON.stringify({ provider, input: code }),
276
- }, deps);
342
+ }, pinned);
343
+ if (loginObject(submitted).ok !== true) throw new Error("Code submission was not accepted");
277
344
  }
278
345
  if (noWait) {
279
346
  printData({ ...start, modelSelection: modelSelectionNextSteps(provider, true) }, wantsJson, modelSelectionGuidance(provider, true));
@@ -281,8 +348,10 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
281
348
  }
282
349
  for (let attempt = 0; attempt < 100; attempt++) {
283
350
  await Bun.sleep(2_000);
284
- const state = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, {}, deps);
285
- if (state.error) throw new CliUsageError(String(state.error));
351
+ const raw = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, { redirect: "error" }, pinned);
352
+ if (loginObject(raw).error) throw new Error("Provider login failed");
353
+ const state = publicLoginStatus(raw);
354
+ if (state.done === true && state.loggedIn !== true) throw new Error("Provider login ended without authentication");
286
355
  if (state.loggedIn === true) {
287
356
  printData({ ...state, modelSelection: modelSelectionNextSteps(provider) }, wantsJson, [`Logged in to ${provider}.`, ...modelSelectionGuidance(provider)]);
288
357
  return;
@@ -413,7 +482,7 @@ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<v
413
482
 
414
483
  export async function handleAccountAuthCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
415
484
  let action: (() => Promise<void>) | undefined;
416
- if (sub === "login" || sub === "reauth") action = () => login(sub === "reauth" ? [...argv, "--reauth"] : argv, deps);
485
+ if (sub === "login" || sub === "reauth") return runCatalogAction(() => login(sub === "reauth" ? [...argv, "--reauth"] : argv, deps));
417
486
  else if (sub === "code") action = () => code(argv, deps);
418
487
  else if (sub === "cancel") action = () => cancel(argv, deps);
419
488
  else if (sub === "reset-credits") action = () => resetCredits(argv, deps);