@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,118 @@
1
+ /** Public API-key quota projection; private probe identity stays on the server. */
2
+ import { parseQuotaFailureCode, type AccountQuotaFields, type ProviderQuota, type ProviderQuotaWindow, type ProviderQuotaCreditsUsd } from "../providers/quota-types";
3
+ import { terminalSafeText } from "./runtime-api";
4
+ import type { AccountRow, FamilyRows } from "./account-api";
5
+
6
+ function record(value: unknown): Record<string, unknown> {
7
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Malformed API-key quota response.");
8
+ return value as Record<string, unknown>;
9
+ }
10
+ function finite(value: unknown): number {
11
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("Malformed API-key quota response.");
12
+ return value;
13
+ }
14
+ function optionalString(row: Record<string, unknown>, key: string): string | undefined {
15
+ if (row[key] === undefined) return undefined;
16
+ if (typeof row[key] !== "string") throw new Error("Malformed API-key quota response.");
17
+ return row[key];
18
+ }
19
+ function optionalBoolean(row: Record<string, unknown>, key: string): boolean | undefined {
20
+ if (row[key] === undefined) return undefined;
21
+ if (typeof row[key] !== "boolean") throw new Error("Malformed API-key quota response.");
22
+ return row[key];
23
+ }
24
+ function windowProjection(value: unknown): ProviderQuotaWindow {
25
+ const row = record(value);
26
+ const label = optionalString(row, "label");
27
+ if (label === undefined) throw new Error("Malformed API-key quota response.");
28
+ const result: ProviderQuotaWindow = { label, percent: finite(row.percent) };
29
+ if (row.resetAt !== undefined) result.resetAt = finite(row.resetAt);
30
+ if (row.passiveObservedAt !== undefined) result.passiveObservedAt = finite(row.passiveObservedAt);
31
+ if (row.scope !== undefined) {
32
+ if (row.scope !== "model") throw new Error("Malformed API-key quota response.");
33
+ result.scope = row.scope;
34
+ }
35
+ if (row.rejected !== undefined) {
36
+ if (row.rejected !== true) throw new Error("Malformed API-key quota response.");
37
+ result.rejected = true;
38
+ }
39
+ return result;
40
+ }
41
+ function creditsProjection(value: unknown): ProviderQuotaCreditsUsd {
42
+ const row = record(value);
43
+ const result: ProviderQuotaCreditsUsd = {
44
+ used: finite(row.used), limit: finite(row.limit), remaining: finite(row.remaining), percent: finite(row.percent),
45
+ };
46
+ if (row.expiresAt !== undefined) result.expiresAt = finite(row.expiresAt);
47
+ const unlimited = optionalBoolean(row, "unlimited");
48
+ if (unlimited !== undefined) result.unlimited = unlimited;
49
+ return result;
50
+ }
51
+ function quotaProjection(value: unknown): ProviderQuota | null {
52
+ if (value === null) return null;
53
+ const row = record(value);
54
+ const result: ProviderQuota = { updatedAt: finite(row.updatedAt) };
55
+ for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "weeklyResetAt", "monthlyPercent", "monthlyResetAt", "kiroCreditsUsed", "kiroCreditsLimit"] as const) {
56
+ if (row[key] !== undefined) result[key] = finite(row[key]);
57
+ }
58
+ if (row.customWindows !== undefined) {
59
+ if (!Array.isArray(row.customWindows)) throw new Error("Malformed API-key quota response.");
60
+ result.customWindows = row.customWindows.map(windowProjection);
61
+ }
62
+ if (row.creditsUsd !== undefined) result.creditsUsd = creditsProjection(row.creditsUsd);
63
+ return result;
64
+ }
65
+
66
+ /** Only opt-in reads use the strict DTO boundary; legacy cheap lists retain their contract. */
67
+ export function projectApiKeyQuotaRows(value: unknown, provider: string): FamilyRows {
68
+ const body = record(value);
69
+ if (!Array.isArray(body.keys) || (body.activeId !== null && typeof body.activeId !== "string")) {
70
+ throw new Error("Malformed API-key quota response.");
71
+ }
72
+ const activeId = body.activeId;
73
+ const rows: AccountRow[] = body.keys.map(value => {
74
+ const row = record(value);
75
+ const id = optionalString(row, "id");
76
+ if (!id) throw new Error("Malformed API-key quota response.");
77
+ const quotaMode = row.quotaMode;
78
+ if (quotaMode === undefined) throw new Error("API-key quota is unverified: server did not report quotaMode. Update the proxy and retry.");
79
+ if (quotaMode !== "probe" && quotaMode !== "passive" && quotaMode !== "unsupported") throw new Error("Malformed API-key quota response.");
80
+ const fields: AccountQuotaFields = { quotaMode };
81
+ if (row.quota !== undefined) fields.quota = quotaProjection(row.quota);
82
+ const unavailable = optionalBoolean(row, "quotaUnavailable");
83
+ if (unavailable !== undefined) fields.quotaUnavailable = unavailable;
84
+ if (row.quotaFailure !== undefined) {
85
+ const failure = parseQuotaFailureCode(row.quotaFailure);
86
+ if (!failure) throw new Error("Malformed API-key quota response.");
87
+ if (unavailable === true) fields.quotaFailure = failure;
88
+ }
89
+ const masked = optionalString(row, "masked");
90
+ return { provider, type: "api-key", id, label: optionalString(row, "label") ?? masked,
91
+ masked, active: optionalBoolean(row, "active") ?? id === activeId, ...fields };
92
+ });
93
+ return { rows, activeId, status: 200 };
94
+ }
95
+
96
+ export function apiKeyQuotaText(row: AccountRow): string {
97
+ const mode = row.quotaMode;
98
+ if (!mode) return "unverified";
99
+ if (mode === "unsupported") return "unsupported";
100
+ if (row.quotaUnavailable) return `${mode}: unavailable${row.quotaFailure ? ` (${row.quotaFailure})` : ""}`;
101
+ const quota = row.quota;
102
+ if (!quota) return `${mode}: not measured`;
103
+ const parts: string[] = [];
104
+ for (const [label, percent] of [["5h", quota.fiveHourPercent], ["wk", quota.weeklyPercent], ["mo", quota.monthlyPercent]] as const) {
105
+ if (percent !== undefined) parts.push(`${label} ${percent}%`);
106
+ }
107
+ for (const window of quota.customWindows ?? []) {
108
+ // Labels are upstream text: keep terminal controls out of the human table.
109
+ const label = terminalSafeText(window.label);
110
+ parts.push(`${label} ${window.percent}%${window.rejected ? " (rejected)" : ""}`);
111
+ }
112
+ if (quota.creditsUsd) {
113
+ const c = quota.creditsUsd;
114
+ parts.push(`USD ${c.used}/${c.unlimited ? "unlimited" : c.limit} used; ${c.remaining} remaining (${c.percent}%)`);
115
+ }
116
+ if (quota.kiroCreditsUsed !== undefined || quota.kiroCreditsLimit !== undefined) parts.push(`credits ${quota.kiroCreditsUsed ?? "?"}/${quota.kiroCreditsLimit ?? "?"}`);
117
+ return `${mode}: ${parts.join("; ") || "not measured"}`;
118
+ }
@@ -1,4 +1,5 @@
1
1
  import { isAbsolute } from "node:path";
2
+ import { redactSecretArgs } from "./secret-args";
2
3
  import { codexExecInvocation } from "../codex/exec-invocation";
3
4
  import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
4
5
  import type { ResolveDeps, SpawnInvocation } from "../lib/win-exec";
@@ -53,7 +54,7 @@ function flag(args: string[], name: string): boolean {
53
54
  }
54
55
 
55
56
  function reject(args: string[]): number {
56
- if (args.length > 0) console.error(`Unexpected argument(s): ${args.join(", ")}`);
57
+ if (args.length > 0) console.error(`Unexpected argument(s): ${redactSecretArgs(args).join(", ")}`);
57
58
  console.error(USAGE);
58
59
  return 1;
59
60
  }
@@ -64,8 +65,10 @@ function effectiveCodexHome(result: Record<string, unknown>): string {
64
65
  : "the effective CODEX_HOME";
65
66
  }
66
67
 
68
+ const EMPTY_PROFILES_NOTES = ["No native main login profiles registered.", "Next: ocx account main add <label>"];
69
+
67
70
  function printProfiles(profiles: PublicProfile[]): void {
68
- if (profiles.length === 0) { console.log("No native main login profiles registered."); return; }
71
+ if (profiles.length === 0) { for (const note of EMPTY_PROFILES_NOTES) console.log(note); return; }
69
72
  const rows = profiles.map(profile => [profile.state === "active" ? "*" : " ", profile.label, profile.id, profile.identityHint]);
70
73
  const header = ["", "LABEL", "PROFILE ID", "IDENTITY"];
71
74
  const widths = header.map((value, index) => Math.max(value.length, ...rows.map(row => row[index]!.length)));
@@ -272,8 +275,12 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
272
275
  const result = await apiJson(deps, baseUrl, "GET", path);
273
276
  if (result.status === 0) return proxyUnreachable(result.transportError);
274
277
  if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`, result.status);
275
- if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2));
276
- else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []);
278
+ const profiles = Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : [];
279
+ if (wantsJson || sub === "doctor") console.log(JSON.stringify({
280
+ ...result.json,
281
+ ...(sub === "list" && profiles.length === 0 ? { notes: [...(Array.isArray(result.json.notes) ? result.json.notes : []), ...EMPTY_PROFILES_NOTES] } : {}),
282
+ }, null, 2));
283
+ else printProfiles(profiles);
277
284
  return 0;
278
285
  }
279
286
 
@@ -0,0 +1,63 @@
1
+ import type { AccountType } from "./account-api";
2
+ import type { OAuthHealthLabel } from "../oauth/health";
3
+
4
+ const HEALTH_LABELS: readonly OAuthHealthLabel[] = [
5
+ "Healthy", "Rate limited", "Quota limited", "Reauthentication required",
6
+ "Verification required", "Refresh failed", "Metadata mismatch", "Credential conflict", "Validation pending",
7
+ ];
8
+
9
+ // Commands are hints, never executed. Admit only bounded, shell-safe selectors;
10
+ // arbitrary server summaries/actions may contain credentials or terminal controls.
11
+ function commandSelector(value: string): string | undefined {
12
+ return /^[A-Za-z0-9_][A-Za-z0-9_.:@/-]{0,199}$/.test(value) ? value : undefined;
13
+ }
14
+
15
+ /** Account label for recovery lines; ids that fail the selector allowlist are never echoed. */
16
+ export function recoveryAccountLabel(id: string, display: string): string {
17
+ return commandSelector(id) ? display : "<unprintable id>";
18
+ }
19
+
20
+ export function emptyAccountNextAction(provider?: string, type?: AccountType): string {
21
+ const name = provider && commandSelector(provider);
22
+ if (!name) return "Next: ocx account login <provider> (see ocx help account login)";
23
+ return type === "api-key"
24
+ ? `Next: ocx account add-key ${name} (pipe the key from a human-controlled stdin source; see ocx help account add-key)`
25
+ : `Next: ocx account login ${name}`;
26
+ }
27
+
28
+ export function projectAccountHealth(
29
+ account: { healthLabel?: unknown },
30
+ provider: string,
31
+ id: string,
32
+ ): { health?: OAuthHealthLabel; healthAction?: string } {
33
+ const health = HEALTH_LABELS.find(label => label === account.healthLabel);
34
+ if (!health) return {};
35
+ const name = commandSelector(provider);
36
+ const selector = commandSelector(id);
37
+ const reauth = provider === "openai" && id === "__main__"
38
+ ? "ocx account main reauth --device"
39
+ : name && selector ? `ocx account reauth ${name} --id ${selector}` : "ocx help account reauth";
40
+ let healthAction: string | undefined;
41
+ switch (health) {
42
+ case "Reauthentication required":
43
+ case "Refresh failed":
44
+ healthAction = reauth;
45
+ break;
46
+ case "Verification required":
47
+ healthAction = `Verify the account with the provider in a browser, then run: ${reauth}`;
48
+ break;
49
+ case "Credential conflict":
50
+ healthAction = "Ensure only one proxy process writes the credential store, then run: ocx doctor";
51
+ break;
52
+ case "Validation pending":
53
+ healthAction = "Wait for quota recovery, then click Refresh quotas in the dashboard Codex account pool";
54
+ break;
55
+ case "Rate limited":
56
+ healthAction = "Wait for the rate-limit cooldown or use another eligible account";
57
+ break;
58
+ case "Quota limited":
59
+ healthAction = "Wait for quota recovery or use another eligible account";
60
+ break;
61
+ }
62
+ return { health, ...(healthAction ? { healthAction } : {}) };
63
+ }
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
3
+ import { parseAnthropicModelRoutes } from "../oauth/anthropic-model-routes";
4
+
5
+ const text = z.string().max(4096);
6
+ export const accountId = z.string().refine(id => id === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(id));
7
+ export const threshold = z.number().int().min(0).max(100);
8
+ const nullableThreshold = threshold.nullable();
9
+ const row = z.object({
10
+ id: accountId, alias: text.optional(), autoSwitchThresholdOverride: nullableThreshold.optional(),
11
+ quota: z.object({ shortWindowSeconds: z.number().finite().optional(), shortResetAt: z.number().finite().optional(),
12
+ weeklyResetAt: z.number().finite().optional() }).nullable().optional(),
13
+ quotaAutoRefresh: z.object({ fiveHourAvailable: z.boolean(), weeklyAvailable: z.boolean(),
14
+ fiveHourEnabled: z.boolean(), weeklyEnabled: z.boolean() }).optional(),
15
+ });
16
+ export const rosterSchema = z.object({ accounts: z.array(row).refine(rows => new Set(rows.map(r => r.id)).size === rows.length) });
17
+ const fields = z.enum(["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes", "nativeMessages"]);
18
+ export const poolSchema = z.object({
19
+ provider: z.string(), kind: z.enum(["codex", "anthropic", "generic"]), supported: z.array(fields),
20
+ enabled: z.boolean().nullable(), enabledEffective: z.boolean(),
21
+ strategy: z.enum(["quota", "round-robin", "fill-first", "reset-first", "least-loaded"]).nullable(),
22
+ stickyLimit: z.number().int().min(1).max(100).nullable(), autoSwitchThreshold: nullableThreshold,
23
+ quotaWindow: z.enum(["five-hour", "weekly", "max-utilization"]).nullable(),
24
+ maxConcurrentPerAccount: z.number().int().min(1).max(100).nullable(),
25
+ // Anthropic native Messages preference; null for kinds that do not honour it. Optional for older servers.
26
+ nativeMessages: z.boolean().nullable().optional(),
27
+ routes: z.unknown().transform((value, ctx) => {
28
+ if (value === null) return null;
29
+ const parsed = parseAnthropicModelRoutes(value);
30
+ if (parsed.ok) return parsed.routes;
31
+ ctx.addIssue({ code: "custom", message: "Invalid routes" });
32
+ return z.NEVER;
33
+ }),
34
+ // The server returns a static validation diagnostic here; never forward its text.
35
+ routesError: z.string().transform(() => "Stored model routes are invalid.").optional(), inert: z.boolean().optional(),
36
+ // Saved, but post-save bookkeeping failed. Only this fixed code is projected.
37
+ warning: z.literal("config_bookkeeping_failed").optional(),
38
+ });
39
+ export const thresholdReceipt = z.object({ ok: z.literal(true), id: accountId,
40
+ autoSwitchThresholdOverride: nullableThreshold, autoSwitchThreshold: threshold });
41
+ export const creditReceipt = z.object({ ok: z.literal(true), id: accountId, creditsAfterLimit: z.boolean() });
42
+ export const creditAllReceipt = z.object({ ok: z.literal(true), all: z.boolean(),
43
+ ids: z.array(accountId).refine(ids => new Set(ids).size === ids.length) });
44
+ const window = z.enum(["five_hour", "seven_day", "seven_day_overage_included"]);
45
+ // The server preserves any Date.parse-compatible timestamp; do not narrow it to Z-only ISO.
46
+ const date = text.refine(value => !Number.isNaN(Date.parse(value))).nullable();
47
+ const grantId = z.string().regex(/^[a-z0-9_-]{1,40}$/);
48
+ const grant = z.object({ id: grantId, label: text, resetsTotal: z.number().int().nonnegative(),
49
+ resetsLeft: z.number().int().nonnegative(), startsAt: date, endsAt: date, clears: z.array(window),
50
+ paused: z.boolean(), usableNow: z.boolean(), useRequiresLimit: z.boolean(),
51
+ percentUsed: z.object({ five_hour: threshold.optional(), seven_day: threshold.optional(),
52
+ seven_day_overage_included: threshold.optional() }),
53
+ }).refine(value => value.resetsLeft <= value.resetsTotal);
54
+ export const grantsSchema = z.object({ accountId: text.min(1), eligible: z.boolean(),
55
+ ineligibleReason: z.enum(["config_off", "tier", "seat", "mobile", "surface", "cli_version", "no_grant", "tenure", "other_experiment", "unavailable", "unknown"]).nullable(),
56
+ atLimit: z.boolean(), grants: z.array(grant).refine(values => new Set(values.map(value => value.id)).size === values.length), nextGrantId: grantId.nullable(), weeklyResetsAt: date, cooldownUntil: date,
57
+ pendingOperation: z.object({ operationId: z.string().uuid(), grantId, createdAt: z.number().finite(),
58
+ retryableUntil: z.number().finite() }).nullable(), journalAvailable: z.boolean(),
59
+ });
60
+ export function parseDto<T>(schema: z.ZodType<T>, value: unknown): T {
61
+ const parsed = schema.safeParse(value);
62
+ if (!parsed.success) throw new Error("Invalid account policy response");
63
+ return parsed.data;
64
+ }
65
+ export function record(value: unknown): Record<string, unknown> {
66
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid account policy response");
67
+ return value as Record<string, unknown>;
68
+ }
@@ -0,0 +1,167 @@
1
+ import { MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
2
+ import { isValidProviderName } from "../config/provider-name";
3
+ import { resolveCodexAccountTargetFromRows } from "./account-target";
4
+ import { runCatalogAction } from "./catalog-command-result";
5
+ import { serializeManagementJson } from "./json-input";
6
+ import { creditAllReceipt, creditReceipt, grantsSchema, parseDto, poolSchema, record,
7
+ rosterSchema, threshold, thresholdReceipt } from "./account-policy-dto";
8
+ import { CliUsageError, RuntimeApiError, printData, runtimeBaseUrl, runtimeRequest, summaryLines,
9
+ takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
10
+
11
+ type Subcommand = "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants";
12
+ const USAGE = "Usage: ocx account pool <provider> [--enabled on|off] [--threshold 0-100] [--strategy NAME] [--sticky 1-100] [--quota-window NAME] [--json]\n"
13
+ + " ocx account auto-switch openai <status|on|off|inherit|threshold N> --account ID [--json]\n"
14
+ + " ocx account credits openai <ID on|off|--all on|off> [--json]\n"
15
+ + " ocx account quota-activation openai ID --window <fiveHour|weekly> <on|off> [--json]\n"
16
+ + " ocx account anthropic-reset-grants [ID] [--json]";
17
+ const READ_ERRORS = { no_account: "No matching Anthropic OAuth account.", auth_failed: "Sign in to this Anthropic account again.",
18
+ upstream_unavailable: "Anthropic did not return the reset-grant status.", ledger_unavailable: "The reset journal is unavailable.",
19
+ ledger_busy: "The reset journal is busy. No grant was consumed." };
20
+ function usage(): never { throw new CliUsageError("Invalid account policy arguments.", USAGE); }
21
+ function onOff(value: string | undefined): boolean { if (value !== "on" && value !== "off") usage(); return value === "on"; }
22
+ function integer(value: string, min: number): number {
23
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) < min || Number(value) > 100) usage();
24
+ return Number(value);
25
+ }
26
+ function selector(value: string | undefined): string {
27
+ if (!value || value.trim() !== value || value.length > 4096 || /[\x00-\x1f\x7f]/.test(value) || value.startsWith("-")) usage();
28
+ return value;
29
+ }
30
+ function option(args: string[], name: string): string | undefined { return takeOptionWithSyntax(args, name)?.value; }
31
+ function done(args: string[]): void { if (args.length) usage(); }
32
+ async function request(path: string, deps: RuntimeApiDeps, body?: Record<string, unknown>): Promise<unknown> {
33
+ return runtimeRequest(path, { redirect: "error", ...(body === undefined ? {} : {
34
+ method: "PUT", headers: { "Content-Type": "application/json" }, body: serializeManagementJson(body),
35
+ }) }, deps);
36
+ }
37
+ async function roster(deps: RuntimeApiDeps) {
38
+ return parseDto(rosterSchema, await request("/api/codex-auth/accounts", deps)).accounts;
39
+ }
40
+ async function target(deps: RuntimeApiDeps, requested: string) {
41
+ const rows = await roster(deps);
42
+ const selected = resolveCodexAccountTargetFromRows(rows, requested);
43
+ if (!("id" in selected)) {
44
+ if ("error" in selected && selected.kind === "not_found") throw new RuntimeApiError("Account not found", 404, null);
45
+ throw new CliUsageError("Account selector is reserved, ambiguous or unavailable. Use a unique account ID.");
46
+ }
47
+ return { id: selected.id, rows };
48
+ }
49
+ function output(data: Record<string, unknown>, json: boolean, note?: string): number {
50
+ printData(data, json, [...(note ? [note] : []), ...summaryLines(data)]);
51
+ return 0;
52
+ }
53
+ function requireEqual(actual: unknown, expected: unknown): void { if (actual !== expected) throw new Error("Unverified account policy result"); }
54
+
55
+ async function pool(args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
56
+ const provider = selector(args.shift()).toLowerCase();
57
+ if (!isValidProviderName(provider)) usage();
58
+ const patch: Record<string, unknown> = {};
59
+ const enabled = option(args, "--enabled"), limit = option(args, "--threshold"), sticky = option(args, "--sticky");
60
+ const strategy = option(args, "--strategy"), window = option(args, "--quota-window");
61
+ if (enabled !== undefined) patch.enabled = onOff(enabled);
62
+ if (limit !== undefined) patch.autoSwitchThreshold = integer(limit, 0);
63
+ if (sticky !== undefined) patch.stickyLimit = integer(sticky, 1);
64
+ if (strategy !== undefined) {
65
+ if (!["quota", "round-robin", "fill-first", ...(provider === "openai" ? ["reset-first"] : []),
66
+ ...(provider === "kiro" ? ["least-loaded"] : [])].includes(strategy)) usage();
67
+ patch.strategy = strategy;
68
+ }
69
+ if (window !== undefined) {
70
+ if (provider !== "anthropic" || !["five-hour", "weekly", "max-utilization"].includes(window)) usage();
71
+ patch.quotaWindow = window;
72
+ }
73
+ done(args);
74
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
75
+ const before = parseDto(poolSchema, await request(`/api/pool/settings?provider=${encodeURIComponent(provider)}`, pinned));
76
+ requireEqual(before.provider, provider);
77
+ requireEqual(before.kind, provider === "openai" ? "codex" : provider === "anthropic" ? "anthropic" : "generic");
78
+ if (Object.keys(patch).some(key => !(before.supported as string[]).includes(key))
79
+ || (before.kind === "codex" && enabled !== undefined)) throw new CliUsageError("The selected pool does not support a requested field.");
80
+ if (!Object.keys(patch).length) return output(before, json);
81
+ const after = parseDto(poolSchema, await request("/api/pool/settings", pinned, { provider, ...patch }));
82
+ requireEqual(after.provider, provider); requireEqual(after.kind, before.kind);
83
+ for (const [key, value] of Object.entries(patch)) requireEqual(record(after)[key], value);
84
+ return output(after, json, after.warning === "config_bookkeeping_failed"
85
+ ? "Pool policy saved, but configuration bookkeeping failed. Re-read the pool policy to confirm the current state."
86
+ : "Pool policy saved.");
87
+ }
88
+
89
+ async function codex(sub: "auto-switch" | "credits" | "quota-activation", args: string[], json: boolean, deps: RuntimeApiDeps): Promise<number> {
90
+ if (args.shift() !== "openai") usage();
91
+ let requested: string | undefined, action: string | undefined, all = false, window: string | undefined;
92
+ let value: number | null | undefined, enabled: boolean | undefined;
93
+ if (sub === "auto-switch") {
94
+ requested = selector(option(args, "--account")); action = args.shift();
95
+ if (action === "threshold") value = integer(args.shift() ?? "", 0);
96
+ else if (action === "on") value = 80;
97
+ else if (action === "off") value = 0;
98
+ else if (action === "inherit") value = null;
99
+ else if (action !== "status") usage();
100
+ } else if (sub === "credits") {
101
+ all = takeFlag(args, "--all");
102
+ if (!all) requested = selector(args.shift());
103
+ enabled = onOff(args.shift());
104
+ } else {
105
+ window = option(args, "--window");
106
+ if (window !== "fiveHour" && window !== "weekly") usage();
107
+ requested = selector(args.shift()); enabled = onOff(args.shift());
108
+ }
109
+ done(args);
110
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
111
+ if (all) {
112
+ const result = parseDto(creditAllReceipt, await request("/api/codex-auth/accounts/credits", pinned, { all: enabled }));
113
+ requireEqual(result.all, enabled);
114
+ if ((!enabled && result.ids.length !== 0) || (enabled && !result.ids.includes(MAIN_CODEX_ACCOUNT_ID))) throw new Error("Invalid credit scope");
115
+ return output(result, json, enabled ? "Paid-credit use enabled for the returned current accounts." : "Paid-credit use disabled for all accounts.");
116
+ }
117
+ const { id, rows } = await target(pinned, requested!);
118
+ if (sub === "auto-switch") {
119
+ if (action === "status") {
120
+ const selected = rows.find(row => row.id === id);
121
+ const override = selected?.autoSwitchThresholdOverride;
122
+ if (override === undefined) throw new Error("Missing account threshold evidence");
123
+ const active = record(await request("/api/codex-auth/active", pinned));
124
+ const effective = override ?? parseDto(threshold, active.autoSwitchThreshold);
125
+ return output({ ok: true, id, autoSwitchThresholdOverride: override, autoSwitchThreshold: effective }, json);
126
+ }
127
+ const result = parseDto(thresholdReceipt, await request("/api/codex-auth/auto-switch", pinned, { id, threshold: value }));
128
+ requireEqual(result.id, id); requireEqual(result.autoSwitchThresholdOverride, value);
129
+ if (value !== null) requireEqual(result.autoSwitchThreshold, value);
130
+ return output(result, json, "Account threshold saved.");
131
+ }
132
+ if (sub === "credits") {
133
+ const result = parseDto(creditReceipt, await request("/api/codex-auth/accounts/credits", pinned, { id, creditsAfterLimit: enabled }));
134
+ requireEqual(result.id, id); requireEqual(result.creditsAfterLimit, enabled);
135
+ return output(result, json, enabled ? "Paid-credit use enabled for this account." : "Paid-credit use disabled for this account.");
136
+ }
137
+ const reply = record(await request("/api/settings", pinned, { codexQuotaAutoRefresh: { id, window, enabled } }));
138
+ requireEqual(reply.ok, true);
139
+ const settings = record(reply.codexQuotaAutoRefresh);
140
+ // Disabling the last window removes the settings entry, by the existing server contract.
141
+ const setting = Object.hasOwn(settings, id) ? record(settings[id]) : { fiveHour: false, weekly: false };
142
+ if (typeof setting.fiveHour !== "boolean" || typeof setting.weekly !== "boolean") throw new Error("Invalid window settings");
143
+ requireEqual(setting[window!], enabled);
144
+ const selected = (await roster(pinned)).find(row => row.id === id);
145
+ if (!selected || selected.quota === undefined) throw new Error("Missing quota evidence after save");
146
+ const available = selected.quotaAutoRefresh
147
+ ? selected.quotaAutoRefresh[window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"]
148
+ : window === "fiveHour" ? selected.quota?.shortWindowSeconds === 18000 && typeof selected.quota.shortResetAt === "number"
149
+ : typeof selected.quota?.weeklyResetAt === "number";
150
+ return output({ ok: true, id, window, enabled: setting[window!], available }, json, "Quota activation setting saved; scheduled refresh is separate.");
151
+ }
152
+
153
+ /** New policy verbs use strict runtime identity; legacy pool-scope auto-switch stays in its existing owner. */
154
+ export async function handleAccountPolicyCommand(sub: Subcommand, args: string[], deps: RuntimeApiDeps = {}): Promise<number> {
155
+ return runCatalogAction(async () => {
156
+ const rest = [...args], json = takeFlag(rest, "--json");
157
+ if (rest.includes("--json")) usage();
158
+ if (sub === "pool") return pool(rest, json, deps);
159
+ if (sub !== "anthropic-reset-grants") return codex(sub, rest, json, deps);
160
+ const id = rest.length ? selector(rest.shift()) : undefined;
161
+ done(rest);
162
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
163
+ const result = parseDto(grantsSchema, await request(`/api/anthropic/reset-grants${id ? `?accountId=${encodeURIComponent(id)}` : ""}`, pinned));
164
+ if (id) requireEqual(result.accountId, id);
165
+ return output(result, json, "Reset-grant status read; no grant was consumed.");
166
+ }, READ_ERRORS);
167
+ }
@@ -46,6 +46,14 @@ export async function resolveCodexAccountTarget(
46
46
  const accounts = (Array.isArray(res.json.accounts) ? res.json.accounts : [])
47
47
  .filter((entry): entry is { id: string; alias?: unknown } =>
48
48
  typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string");
49
+ return resolveCodexAccountTargetFromRows(accounts, requested);
50
+ }
51
+
52
+ /** Shared precedence only; new policy callers validate their target roster before calling. */
53
+ export function resolveCodexAccountTargetFromRows(
54
+ accounts: readonly { id: string; alias?: unknown }[], requested: string,
55
+ ): Exclude<CodexAccountTarget, { networkDown: true }> {
56
+ if (requested === MAIN_ALIAS || requested === MAIN_CODEX_ACCOUNT_ID) return { id: MAIN_CODEX_ACCOUNT_ID };
49
57
  if (accounts.some(account => account.id === requested)) return { id: requested };
50
58
  if (isReservedCodexAccountWord(requested)) {
51
59
  return { error: `"${requested}" is reserved; it clears the selection with \`ocx account use\` and names no account`, kind: "reserved" };
@@ -1,4 +1,7 @@
1
1
  /** `ocx account` — list and switch provider credentials (issue #180). */
2
+ import { apiKeyQuotaText } from "./account-key-quota";
3
+ import { redactSecretArgs } from "./secret-args";
4
+ import { emptyAccountNextAction, recoveryAccountLabel } from "./account-next-actions";
2
5
  import { loadConfig } from "../config";
3
6
  import { explainCodexUseOutcome, reportCodexAccountTargetError, resolveCodexUseTarget } from "./account-target";
4
7
  import { providerCodexAccountMode } from "../providers/registry";
@@ -63,7 +66,11 @@ const ACCOUNT_USAGE = `Usage:
63
66
  ocx account add-key <provider> [--label <label>] [--json]
64
67
  ocx account import <provider> --format <format> (--file <path>|--stdin) [--json]
65
68
  ocx account import-orca --source <orca-data-directory> --registry <orca-data.json> [--apply] [--json]
66
- ocx account login <provider> [--id <account-id>] [--reauth] [--code -] [--no-wait] [--json]
69
+ ocx account login <provider> [--id <account-id>] [--reauth] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json]
70
+ ocx account pool <provider> [--enabled on|off] [--threshold N] [--strategy NAME] [--sticky N] [--quota-window W] [--json]
71
+ ocx account credits openai <ID on|off|--all on|off> [--json]
72
+ ocx account quota-activation openai ID --window fiveHour|weekly <on|off> [--json]
73
+ ocx account anthropic-reset-grants [ID] [--json]
67
74
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
68
75
  ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
69
76
  ocx account reset-credits <account-id|main> [--consume --yes] [--json]
@@ -87,10 +94,12 @@ function consumeFlag(args: string[], flag: string): boolean {
87
94
  /** Returns an error message for leftover args, or null when clean. */
88
95
  function leftoverArgsError(args: string[]): string | null {
89
96
  if (args.length === 0) return null;
90
- const unknown = args.filter(a => a.startsWith("-"));
97
+ const shown = redactSecretArgs(args);
98
+ // Flags plus redaction markers only: a stray positional may be a credential operand.
99
+ const unknown = shown.filter(a => a.startsWith("-") || a === "<redacted>");
91
100
  return unknown.length > 0
92
101
  ? `Unknown flag(s): ${unknown.join(", ")}`
93
- : `Unexpected argument(s): ${args.join(", ")}`;
102
+ : `Unexpected argument(s): ${shown.join(", ")}`;
94
103
  }
95
104
 
96
105
  function candidateNames(config: OcxConfig): string {
@@ -110,11 +119,15 @@ function statusText(row: AccountRow): string {
110
119
  // held out -- so printing only one of the two would hide exactly the confusing case (#2703).
111
120
  if (row.paused) parts.push("paused");
112
121
  if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
113
- if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) parts.push("needs-reauth");
122
+ if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) {
123
+ parts.push(row.needsReauthReason === "verify_account" ? "needs-reauth(verify)" : "needs-reauth");
124
+ }
114
125
  // A paused Kiro row already says "paused"; repeating it as a skip reason adds nothing.
115
126
  if (row.provider === "kiro" && row.autoSelectable === false && !(row.paused && row.skipReason === "paused"))
116
127
  parts.push(row.skipReason ? `not-auto-selected(${row.skipReason})` : "not-auto-selected");
117
128
  if (row.validationPending) parts.push("validation-pending");
129
+ if (row.health && row.health !== "Healthy" && !row.needsReauth && !row.validationPending) parts.push(row.health.toLowerCase());
130
+ if (row.creditsAfterLimit === true) parts.push("paid-credits: on");
118
131
  if (row.selectionExcludedReason === "plan_excluded") {
119
132
  parts.push(`not-auto-selected(plan=${row.selectionExcludedPlan ?? row.plan ?? "unknown"})`);
120
133
  }
@@ -132,6 +145,7 @@ function priorityText(row: AccountRow): string {
132
145
  * decides on before a long session. The full breakdown stays in `--json`.
133
146
  */
134
147
  function quotaText(row: AccountRow): string {
148
+ if (row.type === "api-key") return apiKeyQuotaText(row);
135
149
  if (row.quotaUnavailable) return row.quotaFailure ? `unavailable (${row.quotaFailure})` : "unavailable";
136
150
  const quota = row.quota;
137
151
  if (!quota) return "-";
@@ -165,7 +179,8 @@ export function formatAccountTable(rows: AccountRow[], withQuota = false): strin
165
179
  });
166
180
  const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
167
181
  const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
168
- return [line(header), ...data.map(line)].join("\n");
182
+ const actions = rows.flatMap(row => row.healthAction ? [`${row.provider} ${recoveryAccountLabel(row.id, displayId(row.id))}: ${row.health?.toLowerCase() ?? "needs attention"}. Next: ${row.healthAction}`] : []);
183
+ return [line(header), ...data.map(line), ...actions].join("\n");
169
184
  }
170
185
 
171
186
  async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
@@ -175,7 +190,9 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
175
190
  // stays a cheap local read (#2566). --refresh bypasses the server-side TTL.
176
191
  const wantsQuota = consumeFlag(rest, "--quota");
177
192
  const refreshQuota = consumeFlag(rest, "--refresh");
178
- const name = rest.shift();
193
+ // An option-shaped token is never the provider: leave it for the leftover check so a
194
+ // credential option keeps its operand redacted.
195
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
179
196
  const leftover = leftoverArgsError(rest);
180
197
  if (leftover) {
181
198
  console.error(leftover);
@@ -235,7 +252,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
235
252
  return apiError(r.errorJson, `failed to list ${t.name}`, r.status);
236
253
  }
237
254
  if (r.rows.length === 0) {
238
- if (showAll) notes.push(`${t.name}: no stored accounts or keys`);
255
+ if (showAll || name) notes.push(`${t.name}: no stored accounts or keys`, emptyAccountNextAction(t.name, t.type));
239
256
  continue;
240
257
  }
241
258
  rows.push(...r.rows);
@@ -250,6 +267,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
250
267
  }
251
268
  }
252
269
 
270
+ if (rows.length === 0 && !name) notes.push("No stored accounts or keys.", emptyAccountNextAction());
253
271
  if (wantsJson) {
254
272
  console.log(JSON.stringify({ accounts: rows, notes }, null, 2));
255
273
  return 0;
@@ -262,7 +280,9 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
262
280
 
263
281
  async function cmdCurrent(rest: string[], deps: AccountDeps): Promise<number> {
264
282
  const wantsJson = consumeFlag(rest, "--json");
265
- const name = rest.shift();
283
+ // An option-shaped token is never the provider: leave it for the leftover check so a
284
+ // credential option keeps its operand redacted.
285
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
266
286
  const leftover = leftoverArgsError(rest);
267
287
  if (!name || leftover) {
268
288
  if (leftover) console.error(leftover);
@@ -363,7 +383,9 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
363
383
  * named `auto` cannot shadow the verb that returns the pool to automatic selection. */
364
384
  async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
365
385
  const wantsJson = consumeFlag(rest, "--json");
366
- const name = rest.shift();
386
+ // An option-shaped token is never the provider: leave it for the leftover check so a
387
+ // credential option keeps its operand redacted.
388
+ const name = rest[0]?.startsWith("-") ? undefined : rest.shift();
367
389
  const leftover = leftoverArgsError(rest);
368
390
  if (!name || leftover) {
369
391
  if (leftover) console.error(leftover);
@@ -401,6 +423,12 @@ async function cmdClear(rest: string[], deps: AccountDeps): Promise<number> {
401
423
  export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promise<number> {
402
424
  const [sub, ...rest] = args;
403
425
  try {
426
+ if (["pool", "credits", "quota-activation", "anthropic-reset-grants"].includes(sub ?? "")
427
+ || (sub === "auto-switch" && rest[0]?.trim().toLowerCase() === "openai"
428
+ && rest.some(arg => arg === "--account" || arg.startsWith("--account=")))) {
429
+ const { handleAccountPolicyCommand } = await import("./account-policy");
430
+ return handleAccountPolicyCommand(sub as "pool" | "auto-switch" | "credits" | "quota-activation" | "anthropic-reset-grants", rest, deps);
431
+ }
404
432
  if (sub === "list") return await cmdList(rest, deps);
405
433
  if (sub === "history") {
406
434
  const { cmdAccountHistory } = await import("./account-history");