@iislee/opencodex 2.31.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -190,8 +190,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
190
190
  if (args.length > 0 || confirmed || rollback) return reject(args);
191
191
  const path = sub === "doctor" ? "/api/native-main-profiles/doctor" : "/api/native-main-profiles";
192
192
  const result = await apiJson(deps, baseUrl, "GET", path);
193
- if (result.status === 0) return proxyUnreachable();
194
- if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`);
193
+ if (result.status === 0) return proxyUnreachable(result.transportError);
194
+ if (result.status !== 200) return apiError(result.json, `failed to ${sub} native profiles`, result.status);
195
195
  if (wantsJson || sub === "doctor") console.log(JSON.stringify(result.json, null, 2));
196
196
  else printProfiles(Array.isArray(result.json.profiles) ? result.json.profiles as PublicProfile[] : []);
197
197
  return 0;
@@ -201,8 +201,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
201
201
  const label = args.shift();
202
202
  if (!label || args.length > 0 || confirmed || rollback) return reject(args);
203
203
  const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/register", { label });
204
- if (result.status === 0) return proxyUnreachable();
205
- if (result.status !== 200) return apiError(result.json, "failed to register the current native login");
204
+ if (result.status === 0) return proxyUnreachable(result.transportError);
205
+ if (result.status !== 200) return apiError(result.json, "failed to register the current native login", result.status);
206
206
  if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
207
207
  else console.log(`Registered '${label}' for ${effectiveCodexHome(result.json)}.`);
208
208
  return 0;
@@ -212,8 +212,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
212
212
  const label = args.shift();
213
213
  if (!label || args.length > 0 || wantsJson || confirmed || rollback) return reject(args);
214
214
  const stage = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage", {});
215
- if (stage.status === 0) return proxyUnreachable();
216
- if (stage.status !== 200) return apiError(stage.json, "failed to prepare native login staging");
215
+ if (stage.status === 0) return proxyUnreachable(stage.transportError);
216
+ if (stage.status !== 200) return apiError(stage.json, "failed to prepare native login staging", stage.status);
217
217
  const stageId = typeof stage.json.stageId === "string" ? stage.json.stageId : "";
218
218
  const writerToken = typeof stage.json.writerToken === "string" ? stage.json.writerToken : "";
219
219
  const stagingHome = typeof stage.json.stagingCodexHome === "string" ? stage.json.stagingCodexHome : "";
@@ -258,8 +258,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
258
258
  if (leaseLost) throw new Error("The native-login staging lease was lost before login completed.");
259
259
  if (exitCode !== 0) throw new Error("Official Codex login did not complete successfully.");
260
260
  const finish = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/stage/finish", { stageId, writerToken, label });
261
- if (finish.status === 0) return proxyUnreachable();
262
- if (finish.status !== 200) return apiError(finish.json, "failed to encrypt the staged native login");
261
+ if (finish.status === 0) return proxyUnreachable(finish.transportError);
262
+ if (finish.status !== 200) return apiError(finish.json, "failed to encrypt the staged native login", finish.status);
263
263
  finished = true;
264
264
  console.log(`Added encrypted native profile '${label}' for ${effectiveCodexHome(finish.json)}.`);
265
265
  return 0;
@@ -283,8 +283,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
283
283
  return reject(args);
284
284
  }
285
285
  const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/switch", { target, confirmedStopped: true });
286
- if (result.status === 0) return proxyUnreachable();
287
- if (result.status !== 200) return apiError(result.json, "failed to switch the native login");
286
+ if (result.status === 0) return proxyUnreachable(result.transportError);
287
+ if (result.status !== 200) return apiError(result.json, "failed to switch the native login", result.status);
288
288
  if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
289
289
  else {
290
290
  const profile = result.json.activeProfile as PublicProfile | undefined;
@@ -301,8 +301,8 @@ export async function cmdNativeMainAccount(args: string[], deps: AccountDeps): P
301
301
  const result = await apiJson(deps, baseUrl, "POST", "/api/native-main-profiles/recover", rollback
302
302
  ? { rollback: true, confirmedStopped: true }
303
303
  : { rollback: false });
304
- if (result.status === 0) return proxyUnreachable();
305
- if (result.status !== 200) return apiError(result.json, "failed to recover the native-profile transaction");
304
+ if (result.status === 0) return proxyUnreachable(result.transportError);
305
+ if (result.status !== 200) return apiError(result.json, "failed to recover the native-profile transaction", result.status);
306
306
  if (wantsJson) console.log(JSON.stringify(result.json, null, 2));
307
307
  else {
308
308
  const home = effectiveCodexHome(result.json);
@@ -2,7 +2,20 @@
2
2
  import { loadConfig } from "../config";
3
3
  import { providerCodexAccountMode } from "../providers/registry";
4
4
  import type { OcxConfig } from "../types";
5
- import { cmdAddKey, cmdAlias, cmdAutoSwitch, cmdClearCooldown, cmdImport, cmdPriority, cmdRefresh, cmdRemove } from "./account-extended";
5
+ import {
6
+ cmdAddKey,
7
+ cmdAlias,
8
+ cmdAutoSwitch,
9
+ cmdClearCooldown,
10
+ cmdImport,
11
+ cmdPause,
12
+ cmdPauseExhausted,
13
+ cmdPriority,
14
+ cmdRefresh,
15
+ cmdRemove,
16
+ cmdSticky,
17
+ cmdStrategy,
18
+ } from "./account-extended";
6
19
  import { apiError, apiJson, classifyAccount, fetchRows, proxyUnreachable, resolveBaseUrl, type AccountDeps, type AccountRow, type AccountType, type ApiResult }
7
20
  from "./account-api";
8
21
 
@@ -16,13 +29,18 @@ const MAIN_CODEX_ID = "__main__";
16
29
  const REPLACEMENT_STYLE_OAUTH = new Set(["kiro"]);
17
30
 
18
31
  const ACCOUNT_USAGE = `Usage:
19
- ocx account list [provider] [--json] [--all]
32
+ ocx account list [provider] [--json] [--all] [--quota [--refresh]]
20
33
  ocx account current <provider> [--json]
21
34
  ocx account use <provider> <account-or-key-id|main> [--json]
22
35
  ocx account refresh <provider> [--json]
23
36
  ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
24
37
  ocx account alias <provider> <account-or-key-id> <display-name|-> [--json]
25
38
  ocx account priority <provider> <account-id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
39
+ ocx account pause <provider> <account-id|main> [--json]
40
+ ocx account resume <provider> <account-id|main> [--json]
41
+ ocx account pause-exhausted <provider> [--json]
42
+ ocx account strategy <provider> [<quota|round-robin|fill-first>] [--json]
43
+ ocx account sticky <provider> [<1-100>] [--json]
26
44
  ocx account remove <provider> <account-or-key-id|main> --yes [--json]
27
45
  ocx account clear-cooldown <provider> <account-id|main> [--json]
28
46
  ocx account add-key <provider> [--label <label>] [--json]
@@ -64,6 +82,10 @@ function displayId(id: string): string {
64
82
 
65
83
  function statusText(row: AccountRow): string {
66
84
  const parts: string[] = [];
85
+ // `paused` leads, and does NOT replace `selected`. A paused-but-selected account is the
86
+ // state an operator most needs named -- requests route to it while the pool believes it is
87
+ // held out -- so printing only one of the two would hide exactly the confusing case (#2703).
88
+ if (row.paused) parts.push("paused");
67
89
  if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
68
90
  if (row.needsReauth) parts.push("needs-reauth");
69
91
  return parts.join(" ");
@@ -75,11 +97,29 @@ function priorityText(row: AccountRow): string {
75
97
  return row.priority > 0 ? `+${row.priority}` : String(row.priority);
76
98
  }
77
99
 
78
- export function formatAccountTable(rows: AccountRow[]): string {
100
+ /**
101
+ * Compact per-account quota for the opt-in QUOTA column: the two windows an operator actually
102
+ * decides on before a long session. The full breakdown stays in `--json`.
103
+ */
104
+ function quotaText(row: AccountRow): string {
105
+ if ((row as { quotaUnavailable?: boolean }).quotaUnavailable) return "unavailable";
106
+ const quota = row.quota;
107
+ if (!quota) return "-";
108
+ const parts: string[] = [];
109
+ // Two spellings reach this DTO: the per-account provider probe reports `fiveHourPercent`,
110
+ // while the Codex pool reports the same idea as `shortPercent`.
111
+ const short = quota.fiveHourPercent ?? quota.shortPercent;
112
+ if (typeof short === "number") parts.push(`5h ${short}%`);
113
+ if (typeof quota.weeklyPercent === "number") parts.push(`wk ${quota.weeklyPercent}%`);
114
+ return parts.length > 0 ? parts.join(" ") : "-";
115
+ }
116
+
117
+ export function formatAccountTable(rows: AccountRow[], withQuota = false): string {
79
118
  const header = ["PROVIDER", "TYPE", "ID", "PLAN/LABEL", "PRIORITY", "STATUS"];
119
+ if (withQuota) header.push("QUOTA");
80
120
  const data = rows.map(r => {
81
121
  const keyLabel = r.masked && r.label !== r.masked ? `${r.masked} (${r.label})` : r.masked;
82
- return [
122
+ const cols = [
83
123
  r.provider,
84
124
  r.type,
85
125
  displayId(r.id),
@@ -87,6 +127,8 @@ export function formatAccountTable(rows: AccountRow[]): string {
87
127
  priorityText(r),
88
128
  statusText(r),
89
129
  ];
130
+ if (withQuota) cols.push(quotaText(r));
131
+ return cols;
90
132
  });
91
133
  const widths = header.map((h, i) => Math.max(h.length, ...data.map(d => d[i]!.length)));
92
134
  const line = (cols: string[]) => cols.map((c, i) => c.padEnd(widths[i]!)).join(" ").trimEnd();
@@ -96,6 +138,10 @@ export function formatAccountTable(rows: AccountRow[]): string {
96
138
  async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
97
139
  const wantsJson = consumeFlag(rest, "--json");
98
140
  const showAll = consumeFlag(rest, "--all");
141
+ // Opt-in: the server probes the upstream once per stored credential, so the default listing
142
+ // stays a cheap local read (#2566). --refresh bypasses the server-side TTL.
143
+ const wantsQuota = consumeFlag(rest, "--quota");
144
+ const refreshQuota = consumeFlag(rest, "--refresh");
99
145
  const name = rest.shift();
100
146
  const leftover = leftoverArgsError(rest);
101
147
  if (leftover) {
@@ -126,8 +172,8 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
126
172
  };
127
173
  push("openai", "codex");
128
174
  const providersRes = await apiJson(deps, baseUrl, "GET", "/api/oauth/providers");
129
- if (providersRes.status === 0) return proxyUnreachable();
130
- if (providersRes.status !== 200) return apiError(providersRes.json, "failed to list OAuth providers");
175
+ if (providersRes.status === 0) return proxyUnreachable(providersRes.transportError);
176
+ if (providersRes.status !== 200) return apiError(providersRes.json, "failed to list OAuth providers", providersRes.status);
131
177
  if (Array.isArray(providersRes.json.providers)) {
132
178
  for (const p of providersRes.json.providers) {
133
179
  if (typeof p === "string") push(p, "live-oauth-list");
@@ -139,10 +185,11 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
139
185
  const rows: AccountRow[] = [];
140
186
  const notes: string[] = [];
141
187
  for (const t of targets) {
142
- const r = await fetchRows(deps, baseUrl, t.name, t.type);
143
- if (r.networkDown) return proxyUnreachable();
188
+ const r = await fetchRows(deps, baseUrl, t.name, t.type, wantsQuota ? { refresh: refreshQuota } : undefined);
189
+ if (r.networkDown) return proxyUnreachable(r.transportError);
144
190
  if (r.errorJson) {
145
- if (name) return apiError(r.errorJson, `failed to list ${t.name}`);
191
+ if (name) return apiError(r.errorJson, `failed to list ${t.name}`, r.status);
192
+
146
193
  const errorText = typeof r.errorJson.error === "string" ? r.errorJson.error : "";
147
194
  const skipUnknownKey = t.type === "api-key"
148
195
  && r.status === 404
@@ -152,7 +199,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
152
199
  && r.status === 400
153
200
  && errorText.includes("unknown oauth provider");
154
201
  if (skipUnknownKey || skipConfigOAuth) continue;
155
- return apiError(r.errorJson, `failed to list ${t.name}`);
202
+ return apiError(r.errorJson, `failed to list ${t.name}`, r.status);
156
203
  }
157
204
  if (r.rows.length === 0) {
158
205
  if (showAll) notes.push(`${t.name}: no stored accounts or keys`);
@@ -174,7 +221,7 @@ async function cmdList(rest: string[], deps: AccountDeps): Promise<number> {
174
221
  console.log(JSON.stringify({ accounts: rows, notes }, null, 2));
175
222
  return 0;
176
223
  }
177
- if (rows.length > 0) console.log(formatAccountTable(rows));
224
+ if (rows.length > 0) console.log(formatAccountTable(rows, wantsQuota));
178
225
  for (const n of notes) console.log(n);
179
226
  if (rows.length === 0 && notes.length === 0) console.log("No stored accounts or keys.");
180
227
  return 0;
@@ -198,8 +245,8 @@ async function cmdCurrent(rest: string[], deps: AccountDeps): Promise<number> {
198
245
  const baseUrl = await resolveBaseUrl(deps);
199
246
  if (!baseUrl) return proxyUnreachable();
200
247
  const r = await fetchRows(deps, baseUrl, name, c.type);
201
- if (r.networkDown) return proxyUnreachable();
202
- if (r.errorJson) return apiError(r.errorJson, `failed to read ${name}`);
248
+ if (r.networkDown) return proxyUnreachable(r.transportError);
249
+ if (r.errorJson) return apiError(r.errorJson, `failed to read ${name}`, r.status);
203
250
 
204
251
  const activeRow = r.rows.find(row => row.active) ?? null;
205
252
  if (wantsJson) {
@@ -253,8 +300,8 @@ async function cmdUse(rest: string[], deps: AccountDeps): Promise<number> {
253
300
  activeId = id;
254
301
  res = await apiJson(deps, baseUrl, "PUT", "/api/providers/keys/active", { name, id });
255
302
  }
256
- if (res.status === 0) return proxyUnreachable();
257
- if (res.status !== 200) return apiError(res.json, `failed to switch ${name}`);
303
+ if (res.status === 0) return proxyUnreachable(res.transportError);
304
+ if (res.status !== 200) return apiError(res.json, `failed to switch ${name}`, res.status);
258
305
 
259
306
  if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, type: c.type, activeId }, null, 2));
260
307
  else console.log(`${name}: active ${c.type === "api-key" ? "key" : "account"} is now ${displayId(activeId)}`);
@@ -278,6 +325,13 @@ export async function cmdAccount(args: string[], deps: AccountDeps = {}): Promis
278
325
  if (sub === "auto-switch") return await cmdAutoSwitch(rest, deps);
279
326
  if (sub === "alias" || sub === "rename") return await cmdAlias(rest, deps);
280
327
  if (sub === "priority") return await cmdPriority(rest, deps);
328
+ // #2702: the server routes existed and only the CLI caller was missing, so these were
329
+ // dashboard-only capabilities.
330
+ if (sub === "pause") return await cmdPause(rest, deps, true);
331
+ if (sub === "resume") return await cmdPause(rest, deps, false);
332
+ if (sub === "pause-exhausted") return await cmdPauseExhausted(rest, deps);
333
+ if (sub === "strategy") return await cmdStrategy(rest, deps);
334
+ if (sub === "sticky") return await cmdSticky(rest, deps);
281
335
  if (sub === "remove") return await cmdRemove(rest, deps);
282
336
  if (sub === "clear-cooldown") return await cmdClearCooldown(rest, deps);
283
337
  if (sub === "add-key") return await cmdAddKey(rest, deps);
package/src/cli/agent.ts CHANGED
@@ -29,7 +29,8 @@ const USAGE = `Usage:
29
29
  ocx agent fallback <status|set|clear> [model,model...] [--poll-ms <5000-600000>] [--json]
30
30
  ocx agent sidecar <status|web|vision> [--list] [--model <id|->]
31
31
  [--backend web:<openai|anthropic|xai|gemini|exa|-> vision:<openai|anthropic|routed|->]
32
- [--reasoning <level>] [--max-descriptions <n>] [--json]`;
32
+ [--reasoning <level>] [--max-descriptions <n>] [--json]
33
+ ocx agent request-user-input [on|off] [--json]`;
33
34
 
34
35
  function clearable(value: string | undefined): string | null | undefined {
35
36
  return value === "-" ? null : value;
@@ -221,6 +222,12 @@ export async function handleAgentCommand(argv: string[], deps: RuntimeApiDeps =
221
222
  else if (sub === "subagents" || sub === "roster") await subagents(rest, deps);
222
223
  else if (sub === "fallback") await fallback(rest, deps);
223
224
  else if (sub === "sidecar") await sidecar(rest, deps);
225
+ // Lives here rather than as a top-level verb because it is an agent-behavior feature flag:
226
+ // it controls whether default mode may ask the operator a question mid-task.
227
+ else if (sub === "request-user-input") {
228
+ const { requestUserInputAction } = await import("./inspect");
229
+ await requestUserInputAction(rest, deps);
230
+ }
224
231
  else throw new CliUsageError(`unknown agent command ${sub}`, USAGE);
225
232
  });
226
233
  }
@@ -0,0 +1,66 @@
1
+ import { CliUsageError, printData, rejectArgs, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
2
+
3
+ const USAGE = `Usage:
4
+ ocx alias list [--json]
5
+ ocx alias set <provider> <alias>
6
+ ocx alias set <provider>/<native-model-id> <alias>
7
+ ocx alias rm <provider>[/<native-model-id>]
8
+ ocx alias defaults <on|off> [--provider <name>]`;
9
+
10
+ function selector(value: string): { provider: string; model?: string } {
11
+ const slash = value.indexOf("/");
12
+ return slash < 0 ? { provider: value } : { provider: value.slice(0, slash), model: value.slice(slash + 1) };
13
+ }
14
+
15
+ export async function handleAliasCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
16
+ const args = [...argv];
17
+ const action = (args.shift() ?? "list").toLowerCase();
18
+ const wantsJson = takeFlag(args, "--json");
19
+ if (action === "list") {
20
+ rejectArgs(args, USAGE);
21
+ const result = await runtimeRequest<Record<string, unknown>>("/api/aliases", {}, deps);
22
+ const lines: string[] = [];
23
+ for (const [target, alias] of Object.entries((result.providers ?? {}) as Record<string, string>)) lines.push(`provider ${target} ${alias} user`);
24
+ for (const [provider, rows] of Object.entries((result.models ?? {}) as Record<string, Record<string, { alias: string; source: string }>>)) {
25
+ for (const [model, value] of Object.entries(rows)) lines.push(`model ${provider}/${model} ${value.alias} ${value.source}`);
26
+ }
27
+ printData(result, wantsJson, lines.length ? lines : ["No aliases configured."]);
28
+ return 0;
29
+ }
30
+ if (action === "defaults") {
31
+ const state = args.shift()?.toLowerCase();
32
+ const provider = takeOption(args, "--provider");
33
+ rejectArgs(args, USAGE);
34
+ if (state !== "on" && state !== "off") throw new CliUsageError("defaults requires on or off", USAGE);
35
+ const result = await runtimeRequest("/api/default-aliases", { method: "PUT", body: JSON.stringify({ enabled: state === "on", ...(provider ? { provider } : {}) }) }, deps);
36
+ printData(result, wantsJson, [`Default aliases ${state}${provider ? ` for ${provider}` : " globally"}.`]);
37
+ return 0;
38
+ }
39
+ const target = args.shift()?.trim();
40
+ if (!target) throw new CliUsageError("alias target is required", USAGE);
41
+ const parsed = selector(target);
42
+ if (!parsed.provider || parsed.model === "") throw new CliUsageError("target must be provider or provider/native-model-id", USAGE);
43
+ if (action === "set") {
44
+ const alias = args.shift()?.trim();
45
+ rejectArgs(args, USAGE);
46
+ if (!alias) throw new CliUsageError("alias value is required", USAGE);
47
+ const path = parsed.model === undefined
48
+ ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
49
+ : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
50
+ const body = parsed.model === undefined ? { alias } : { set: { [parsed.model]: alias } };
51
+ const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
52
+ printData(result, wantsJson, [`${target} → ${alias}`]);
53
+ return 0;
54
+ }
55
+ if (action === "rm") {
56
+ rejectArgs(args, USAGE);
57
+ const path = parsed.model === undefined
58
+ ? `/api/providers/${encodeURIComponent(parsed.provider)}/alias`
59
+ : `/api/providers/${encodeURIComponent(parsed.provider)}/model-aliases`;
60
+ const body = parsed.model === undefined ? { alias: null } : { remove: [parsed.model] };
61
+ const result = await runtimeRequest(path, { method: "PUT", body: JSON.stringify(body) }, deps);
62
+ printData(result, wantsJson, [`Removed alias for ${target}.`]);
63
+ return 0;
64
+ }
65
+ throw new CliUsageError(`unknown alias action '${action}'`, USAGE);
66
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * `ocx capabilities` -- the surface index an agent reads first.
3
+ *
4
+ * The point of this verb is that driving `ocx` programmatically should not require
5
+ * parsing help text. `--json` emits the capability table with the management routes each
6
+ * capability drives; `--route` answers the inverse question.
7
+ */
8
+ import {
9
+ CAPABILITIES,
10
+ HEAD_CAPABILITIES,
11
+ capabilitiesForRoute,
12
+ capabilityInvocation,
13
+ type Capability,
14
+ } from "./capabilities";
15
+ import { takeFlag } from "./runtime-api";
16
+
17
+ function takeValueFlag(args: string[], flag: string): string | undefined {
18
+ // Order-independent by construction: scan for the flag anywhere in argv rather than
19
+ // reading a fixed position. Positional flag handling is exactly why
20
+ // `ocx restore back --json` ignored its flag.
21
+ const idx = args.indexOf(flag);
22
+ if (idx === -1) return undefined;
23
+ const value = args[idx + 1];
24
+ args.splice(idx, value === undefined || value.startsWith("-") ? 1 : 2);
25
+ if (value === undefined || value.startsWith("-")) return "";
26
+ return value;
27
+ }
28
+
29
+ function renderHuman(caps: readonly Capability[], includeHead: boolean): void {
30
+ for (const cap of caps) {
31
+ const marker = cap.mutates ? "!" : " ";
32
+ console.log(`${marker} ${capabilityInvocation(cap)}`);
33
+ console.log(` ${cap.summary}`);
34
+ if (cap.routes.length > 0) {
35
+ console.log(` routes: ${cap.routes.map(r => `${r.method} ${r.path}`).join(", ")}`);
36
+ }
37
+ if (cap.flags.length > 0) {
38
+ console.log(` flags: ${cap.flags.map(f => f.name).join(" ")}`);
39
+ }
40
+ }
41
+ if (!includeHead) return;
42
+ for (const head of HEAD_CAPABILITIES) {
43
+ console.log(` ocx ${head.invocations[0]}`);
44
+ console.log(` ${head.summary}`);
45
+ }
46
+ }
47
+
48
+ export async function runCapabilities(argv: string[]): Promise<number> {
49
+ const args = [...argv];
50
+ const json = takeFlag(args, "--json");
51
+ const mutatingOnly = takeFlag(args, "--mutating-only");
52
+ const route = takeValueFlag(args, "--route");
53
+
54
+ if (route !== undefined && route.length === 0) {
55
+ console.error("Usage: ocx capabilities --route <path>");
56
+ return 64;
57
+ }
58
+
59
+ let selected: readonly Capability[] = route === undefined ? CAPABILITIES : capabilitiesForRoute(route);
60
+ if (mutatingOnly) selected = selected.filter(cap => cap.mutates);
61
+
62
+ if (route !== undefined && selected.length === 0) {
63
+ // An unmatched route is a real answer, not an error: the route may be exempt or may
64
+ // not exist. Say which, rather than exiting 0 with silence.
65
+ if (json) {
66
+ console.log(JSON.stringify({ schemaVersion: 1, route, capabilities: [] }, null, 2));
67
+ } else {
68
+ console.error(`No CLI capability drives ${route}.`);
69
+ }
70
+ return 4;
71
+ }
72
+
73
+ if (json) {
74
+ console.log(JSON.stringify({
75
+ schemaVersion: 1,
76
+ ...(route === undefined ? {} : { route }),
77
+ capabilities: selected.map(cap => ({
78
+ command: cap.command,
79
+ invocation: capabilityInvocation(cap),
80
+ summary: cap.summary,
81
+ routes: cap.routes,
82
+ flags: cap.flags,
83
+ mutates: cap.mutates,
84
+ json: cap.json,
85
+ ...(cap.details ? { details: cap.details } : {}),
86
+ })),
87
+ ...(route === undefined && !mutatingOnly ? { headCapabilities: HEAD_CAPABILITIES } : {}),
88
+ }, null, 2));
89
+ return 0;
90
+ }
91
+
92
+ renderHuman(selected, route === undefined && !mutatingOnly);
93
+ return 0;
94
+ }