@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
@@ -22,7 +22,8 @@ import {
22
22
  export const REMOTE_WORKSPACE_USAGE = `Usage:
23
23
  ocx remote-workspace pair <hub-url> --pairing-code-stdin --root <absolute-path> [--root <absolute-path> ...] [--toolchain-root <absolute-directory> ...] [--executor-helper <absolute-file>] [--name <device-name>] [--json]
24
24
  ocx remote-workspace agent
25
- ocx remote-workspace status [--json]`;
25
+ ocx remote-workspace status [--json]
26
+ ocx remote-workspace hub [status|runtimes|sessions] [--json]`;
26
27
 
27
28
  export interface RemoteWorkspaceCliDeps extends RuntimeApiDeps {
28
29
  store?: RemoteWorkspaceDeviceStateStore;
@@ -76,6 +77,10 @@ function publicStatus(state: RemoteWorkspaceDeviceState | null): Record<string,
76
77
  export async function runRemoteWorkspaceCommand(rawArgs: string[], deps: RemoteWorkspaceCliDeps = {}): Promise<number> {
77
78
  const args = [...rawArgs];
78
79
  const command = args.shift();
80
+ if (command === "hub") {
81
+ const { handleRemoteWorkspaceHubCommand } = await import("./remote-workspace-hub");
82
+ return handleRemoteWorkspaceHubCommand(args, deps);
83
+ }
79
84
  const store = deps.store ?? new RemoteWorkspaceDeviceFileStore();
80
85
  if (command === "status") {
81
86
  const wantsJson = takeJsonFlag(args);
@@ -0,0 +1,27 @@
1
+ import type { ProxyRestartResult } from "./tray-proxy";
2
+
3
+ export const INCOMPLETE_INSTALL_RECOVERY = "If the installer already exited or failed, run `ocx status`, stop any running proxy through its owner (`ocx stop`, or its service or desktop app), then reinstall with the same package manager and start it again (see https://opencodex.me/troubleshooting/update-failed/).";
4
+
5
+ export function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
6
+ if (result.phase === "identity") {
7
+ console.error("Refusing to restart because the running proxy identity could not be attested. Run `ocx status` to inspect the intended proxy.");
8
+ } else if (result.phase === "request") {
9
+ const code = result.error instanceof Error ? result.error.message : "";
10
+ if (code === "restart_capability_unsupported") {
11
+ console.error("The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
12
+ console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
13
+ } else if (code === "restart_version_skew") {
14
+ console.error("This CLI is older than the running proxy (or versions cannot be compared). Use the newer installation's ocx to restart; check `which -a ocx` and run `ocx status`. No changes were made.");
15
+ } else if (code === "update_restart_home_unverified") {
16
+ console.error("The proxy home or lifecycle ownership could not be verified. Run `ocx status`; if a service owns the proxy, use its installation's `ocx service restart`, otherwise use the owning app or supervisor. No changes were made.");
17
+ } else if (code === "restart_package_tree_unsettled") {
18
+ console.error(`The proxy's package files are unsettled. Wait for the install to finish, then run \`ocx restart\`. ${INCOMPLETE_INSTALL_RECOVERY}`);
19
+ } else {
20
+ console.error("Proxy restart request could not be confirmed; no fallback stop/start was attempted. Run `ocx status` to inspect ownership and runtime state.");
21
+ }
22
+ } else if (result.phase === "replacement") {
23
+ console.error("Proxy restart was accepted, but no identity-verified replacement became healthy in time. Run `ocx status` to inspect the replacement.");
24
+ } else {
25
+ console.error("Proxy was not running and the fallback start did not become healthy. Run `ocx status` to inspect startup state.");
26
+ }
27
+ }
package/src/cli/root.ts CHANGED
@@ -8,11 +8,14 @@
8
8
  * the exit paths and the shim preflight, then returns the dispatchable head
9
9
  * for the command switch in src/cli/index.ts.
10
10
  */
11
- import { hasHelpFlag, printSubcommandUsage, printUsage, printVersion } from "./help";
11
+ import { printFullUsage, printSubcommandUsage, printUsage, printVersion } from "./help";
12
12
  import { parseReadyArgs, type ReadyArgs } from "./ready";
13
13
  import { parseResolveArgs, type ResolveArgs } from "./resolve";
14
14
  import { parseStopApproval } from "./stop-approval";
15
15
  import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
16
+ import { findCommand } from "./registry";
17
+ import { printUnknownCommand } from "./help-recovery";
18
+ import { noteCredentialArgv, redactSecretArgs } from "./secret-args";
16
19
 
17
20
  export interface CliHead {
18
21
  kind: "version" | "help" | "ready" | "resolve" | "command";
@@ -20,6 +23,10 @@ export interface CliHead {
20
23
  args: string[];
21
24
  /** For kind "help": the subcommand whose usage should print, if any. */
22
25
  helpTarget?: string;
26
+ /** Explicit multi-token help topic; never passed to command execution. */
27
+ helpPath?: string[];
28
+ /** Complete static reference requested from root help. */
29
+ helpAll?: boolean;
23
30
  /** Present only for `ready`; undefined when the ready args failed to parse. */
24
31
  readyArgs?: ReadyArgs;
25
32
  /** Present only for `resolve`; undefined when the resolve args failed to parse. */
@@ -32,19 +39,26 @@ export function parseCliHead(argv: string[]): CliHead {
32
39
  if (command === "--version" || command === "-v" || command === "version") {
33
40
  return { kind: "version", command, args };
34
41
  }
35
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
36
- // `ocx help <sub>` carries the subcommand; bare/flag help prints the full usage.
42
+ const boundary = argv.indexOf("--");
43
+ const prefix = boundary < 0 ? argv : argv.slice(0, boundary);
44
+ const rootHelp = command === undefined || command === "help" || command === "--help" || command === "-h";
45
+ const commandHelp = prefix[1] === "help";
46
+ if (rootHelp || commandHelp || prefix.slice(1).some(value => value === "--help" || value === "-h")) {
47
+ const topic = rootHelp ? prefix.slice(1)
48
+ : commandHelp ? [command!, ...prefix.slice(2)] : prefix;
49
+ // Options and their operands are not part of a help topic. Keep argv intact.
50
+ const option = topic.findIndex((value, index) => value.startsWith("-")
51
+ && (index > 0 || (rootHelp && ["--all", "--help", "-h"].includes(value))));
52
+ const path = option < 0 ? topic : topic.slice(0, option);
37
53
  return {
38
54
  kind: "help",
39
55
  command,
40
56
  args,
41
- ...(command === "help" && args[1] ? { helpTarget: args[1] } : {}),
57
+ ...(path.length ? { helpTarget: path[0] } : {}),
58
+ ...(path.length > 1 ? { helpPath: path } : {}),
59
+ ...(rootHelp && path.length === 0 && prefix.includes("--all") ? { helpAll: true } : {}),
42
60
  };
43
61
  }
44
- if (command !== "help" && hasHelpFlag(args.slice(1))) {
45
- // `ocx <cmd> --help|-h|help` prints that command's usage, not the full list.
46
- return { kind: "help", command, args, helpTarget: command };
47
- }
48
62
  // P1: pre-parse `ocx ready` and reject invalid arguments with exit 64 BEFORE
49
63
  // maybeAutoRestoreCodexShim (or any discovery/probe/filesystem-capable global
50
64
  // preflight) runs. `ready --help` / `help ready` already exited above, so this
@@ -65,14 +79,24 @@ export function parseCliHead(argv: string[]): CliHead {
65
79
  return { kind: "command", command, args };
66
80
  }
67
81
 
82
+ export function uninstallArgsError(command: string | undefined, args: string[]): string | undefined {
83
+ if ((command !== "uninstall" && command !== "remove") || args.length <= 1) return undefined;
84
+ return `ocx ${command} does not accept arguments (got: ${redactSecretArgs(args.slice(1)).join(" ")}). No changes were made. See: ocx help ${command}`;
85
+ }
86
+
68
87
  export async function runCli(argv: string[]): Promise<CliHead> {
88
+ noteCredentialArgv(argv);
69
89
  const head = parseCliHead(argv);
70
90
  switch (head.kind) {
71
91
  case "version":
72
92
  printVersion();
73
93
  process.exit(0);
74
94
  case "help": {
75
- if (head.helpTarget) printSubcommandUsage(head.helpTarget);
95
+ if (head.helpAll) printFullUsage();
96
+ else if (head.helpTarget) printSubcommandUsage(head.helpTarget, head.helpPath, {
97
+ fallbackToParent: head.command !== "help" && head.command !== "--help"
98
+ && head.command !== "-h" && head.args[1] !== "help",
99
+ });
76
100
  else printUsage();
77
101
  process.exit(0);
78
102
  }
@@ -104,10 +128,19 @@ export async function runCli(argv: string[]): Promise<CliHead> {
104
128
  return head;
105
129
  }
106
130
  case "command":
131
+ if (head.command !== "internal" && !findCommand(head.command ?? "")) {
132
+ printUnknownCommand(head.command);
133
+ process.exit(1);
134
+ }
107
135
  if (head.command === "stop" && !parseStopApproval(head.args.slice(1)).ok) {
108
136
  console.error("Usage: ocx stop [--json [--expect-pid <pid> --expect-port <port> --expect-hostname <host> --expect-config-home <home> --expect-cli-version <version> --expect-compatibility-token <hex>]]");
109
137
  process.exit(64);
110
138
  }
139
+ const uninstallError = uninstallArgsError(head.command, head.args);
140
+ if (uninstallError) {
141
+ console.error(uninstallError);
142
+ process.exit(2);
143
+ }
111
144
  maybeAutoRestoreCodexShim(head.command, head.args);
112
145
  return head;
113
146
  }
@@ -0,0 +1,137 @@
1
+ /** Explicit create/replace/delete for routing profiles; contextual validation stays server-owned. */
2
+ import { isValidPolicyId } from "../routing/profile";
3
+ import { printCatalogResult, runCatalogAction } from "./catalog-command-result";
4
+ import { readJsonInput, serializeManagementJson } from "./json-input";
5
+ import { CliUsageError, runtimeBaseUrl, runtimeRequest, takeFlag, takeOptionWithSyntax, type RuntimeApiDeps } from "./runtime-api";
6
+
7
+ const USAGE = `Usage:
8
+ ocx route policy create <id> --file <profile.json|-> [--json]
9
+ ocx route policy update <id> --file <profile.json|-> --expected-revision <revision> [--json]
10
+ ocx route policy remove <id> --yes [--json]`;
11
+ const ERRORS: Readonly<Record<string, string>> = {
12
+ profile_exists: "Routing profile already exists. Use show and review an explicit update.",
13
+ profile_revision_conflict: "Routing profile changed. Read show again and review the changes before updating with its revision.",
14
+ unknown_profile: "Routing profile was not found on the selected target.",
15
+ invalid_profile: "The target refused the profile. Check its candidates, alias and policy settings against the target configuration.",
16
+ missing_profile_id: "A routing profile id is required.",
17
+ invalid_profile_mode: "The target refused the routing profile operation.",
18
+ alias_reference_conflict: "The alias change conflicts with existing model references. Resolve the conflicting mappings first.",
19
+ invalid_shadow_call_target: "The alias change would create an invalid shadow-call target. Inspect its routing references first.",
20
+ };
21
+ const PROFILE_KEYS = ["alias", "candidates", "require", "optimize", "limits", "unknownEvidence", "compatibility"];
22
+ const REQUIRE_KEYS = ["minContextWindow", "minQuotaHeadroom", "tools", "imageInput", "structuredOutput", "reasoningEffort", "serviceTier", "localOnly", "remoteAllowed", "encryptedCodexTasks"];
23
+ const WEIGHT_KEYS = ["latency", "health", "cost", "quota"];
24
+ const UNKNOWN_KEYS = ["capability", "health", "quota", "cost"];
25
+ const COMPATIBILITY_KEYS = ["requiredSuites", "minStatus", "maxEvidenceAgeMs", "unknownEvidence", "degradedEvidence"];
26
+
27
+ function record(value: unknown): value is Record<string, unknown> {
28
+ return value !== null && typeof value === "object" && !Array.isArray(value)
29
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value));
30
+ }
31
+ function only(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
32
+ return record(value) && Object.keys(value).every(key => keys.includes(key));
33
+ }
34
+ function text(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0; }
35
+ function nonnegative(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
36
+ function evidence(value: unknown): boolean { return value === "allow" || value === "penalize" || value === "exclude"; }
37
+ function optionalFields(value: Record<string, unknown>, validators: Record<string, (value: unknown) => boolean>): boolean {
38
+ return Object.entries(validators).every(([key, accepts]) => !Object.hasOwn(value, key) || accepts(value[key]));
39
+ }
40
+
41
+ /** Shape/ownership only: do not invent a provider configuration to run contextual validators. */
42
+ function editableProfile(value: unknown): value is Record<string, unknown> {
43
+ if (!only(value, PROFILE_KEYS) || !Array.isArray(value.candidates) || !value.candidates.length
44
+ || !value.candidates.every(candidate => only(candidate, ["provider", "model"]) && text(candidate.provider) && text(candidate.model))) return false;
45
+ if (Object.hasOwn(value, "alias") && typeof value.alias !== "string") return false;
46
+ if (Object.hasOwn(value, "require")) {
47
+ const required = value.require;
48
+ if (!only(required, REQUIRE_KEYS) || !optionalFields(required, {
49
+ minContextWindow: n => Number.isSafeInteger(n) && (n as number) > 0,
50
+ minQuotaHeadroom: n => nonnegative(n) && n <= 1,
51
+ reasoningEffort: n => typeof n === "string", serviceTier: n => typeof n === "string",
52
+ })) return false;
53
+ for (const key of ["tools", "imageInput", "structuredOutput", "localOnly", "remoteAllowed", "encryptedCodexTasks"]) {
54
+ if (Object.hasOwn(required, key) && typeof required[key] !== "boolean") return false;
55
+ }
56
+ }
57
+ if (Object.hasOwn(value, "optimize") && (!only(value.optimize, WEIGHT_KEYS) || !Object.values(value.optimize).every(nonnegative))) return false;
58
+ if (Object.hasOwn(value, "limits") && (!only(value.limits, ["maxEstimatedCostUsd", "onUnknownCost"])
59
+ || !optionalFields(value.limits, { maxEstimatedCostUsd: nonnegative, onUnknownCost: n => n === "allow" || n === "exclude" }))) return false;
60
+ if (Object.hasOwn(value, "unknownEvidence") && (!only(value.unknownEvidence, UNKNOWN_KEYS) || !Object.values(value.unknownEvidence).every(evidence))) return false;
61
+ if (Object.hasOwn(value, "compatibility")) {
62
+ const compatibility = value.compatibility;
63
+ if (!only(compatibility, COMPATIBILITY_KEYS) || !optionalFields(compatibility, {
64
+ requiredSuites: n => Array.isArray(n) && n.every(suite => only(suite, ["suiteId", "evidenceLayer"])
65
+ && text(suite.suiteId) && ["protocol_conformance", "live_route_compatibility"].includes(suite.evidenceLayer as string)),
66
+ minStatus: n => n === "PROBED" || n === "VERIFIED",
67
+ maxEvidenceAgeMs: n => Number.isSafeInteger(n) && (n as number) >= 0,
68
+ unknownEvidence: evidence, degradedEvidence: evidence,
69
+ })) return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ function invalidResult(): never { throw new Error("Invalid routing profile result"); }
75
+
76
+ function projectProfile(value: unknown, id: string, model: unknown): Record<string, unknown> {
77
+ if (!only(value, [...PROFILE_KEYS, "id", "model", "revision"]) || value.id !== id
78
+ || !text(value.revision) || !text(model) || value.model !== model
79
+ || (value.alias !== null && !text(value.alias))) invalidResult();
80
+ const { id: _id, model: _model, revision: _revision, alias, ...editable } = value;
81
+ const input = { ...editable, ...(alias === null ? {} : { alias }) };
82
+ if (!editableProfile(input) || model !== (alias ?? `policy/${id}`)
83
+ || !record(value.require) || !record(value.limits)
84
+ || !record(value.optimize) || !WEIGHT_KEYS.every(key => Object.hasOwn(value.optimize as object, key))
85
+ || !record(value.unknownEvidence) || !UNKNOWN_KEYS.every(key => Object.hasOwn(value.unknownEvidence as object, key))) invalidResult();
86
+ if (value.compatibility !== undefined && (!record(value.compatibility)
87
+ || !Array.isArray(value.compatibility.requiredSuites) || !evidence(value.compatibility.unknownEvidence)
88
+ || !evidence(value.compatibility.degradedEvidence))) invalidResult();
89
+ // Every nested field was admitted above; rebuild the envelope rather than echoing server metadata.
90
+ return { id, model, revision: value.revision, alias, ...editable };
91
+ }
92
+
93
+ function printResult(value: unknown, id: string, remove: boolean, wantsJson: boolean): number {
94
+ if (!only(value, remove ? ["success", "id", "catalogRefresh"] : ["success", "id", "model", "profile", "catalogRefresh"])
95
+ || value.success !== true || value.id !== id) invalidResult();
96
+ const data: Record<string, unknown> = { success: true, id };
97
+ const lines = [remove ? `Removed routing profile: ${id}` : `Saved routing profile: ${id}`];
98
+ if (!remove) {
99
+ data.profile = projectProfile(value.profile, id, value.model);
100
+ data.model = value.model;
101
+ lines.push(`Model: ${value.model}`, `Revision: ${(data.profile as Record<string, unknown>).revision}`);
102
+ }
103
+ return printCatalogResult(data, value.catalogRefresh, wantsJson, lines);
104
+ }
105
+
106
+ export async function handleRoutePolicyWriteCommand(
107
+ sub: "create" | "update" | "remove", argv: string[], deps: RuntimeApiDeps = {},
108
+ ): Promise<number> {
109
+ return runCatalogAction(async () => {
110
+ const args = [...argv];
111
+ const id = args.shift();
112
+ if (!id || !isValidPolicyId(id)) throw new CliUsageError("A valid exact routing profile id is required (letters, numbers, dot, underscore or hyphen; max 64).", USAGE);
113
+ const wantsJson = takeFlag(args, "--json");
114
+ const yes = takeFlag(args, "--yes");
115
+ const file = takeOptionWithSyntax(args, "--file")?.value;
116
+ const revision = takeOptionWithSyntax(args, "--expected-revision")?.value;
117
+ if (args.length) throw new CliUsageError("Unknown, repeated or unexpected routing profile arguments.", USAGE);
118
+ if (sub === "remove") {
119
+ if (!yes || file !== undefined || revision !== undefined) throw new CliUsageError("Removal requires --yes and does not accept a file or revision.", USAGE);
120
+ } else {
121
+ if (yes || !file?.trim()) throw new CliUsageError("Creation and update require --file and do not accept --yes.", USAGE);
122
+ if (sub === "update" ? !revision?.trim() : revision !== undefined) throw new CliUsageError("Update requires an explicit nonblank --expected-revision; create does not accept it.", USAGE);
123
+ }
124
+ let body: string | undefined;
125
+ if (sub !== "remove") {
126
+ const profile = await readJsonInput(file!, deps);
127
+ if (!editableProfile(profile)) throw new CliUsageError("Profile file must contain only editable routing fields with valid nested types; omit id, model and revision.", USAGE);
128
+ body = serializeManagementJson({ id, mode: sub, profile, ...(sub === "update" ? { expectedRevision: revision } : {}) });
129
+ }
130
+ const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
131
+ const result = await runtimeRequest(sub === "remove" ? `/api/routing-profiles?id=${encodeURIComponent(id)}` : "/api/routing-profiles", {
132
+ method: sub === "remove" ? "DELETE" : "PUT", redirect: "error",
133
+ ...(body !== undefined ? { headers: { "content-type": "application/json" }, body } : {}),
134
+ }, pinned);
135
+ return printResult(result, id, sub === "remove", wantsJson);
136
+ }, ERRORS);
137
+ }
@@ -11,6 +11,9 @@ import {
11
11
  } from "./runtime-api";
12
12
 
13
13
  const USAGE = `Usage:
14
+ ocx route policy create <id> --file <FILE|-> [--json]
15
+ ocx route policy update <id> --file <FILE|-> --expected-revision <revision> [--json]
16
+ ocx route policy remove <id> --yes [--json]
14
17
  ocx route policy list [--json]
15
18
  ocx route policy show <id> [--json]
16
19
  ocx route policy dry-run <id> [--model-context <tokens>] [--tools]
@@ -83,6 +86,11 @@ async function dryRun(argv: string[], deps: RuntimeApiDeps): Promise<void> {
83
86
  }
84
87
 
85
88
  export async function handleRoutePolicyCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
89
+ const [write, ...rest] = argv;
90
+ if (write === "create" || write === "update" || write === "remove") {
91
+ const { handleRoutePolicyWriteCommand } = await import("./route-policy-write");
92
+ return handleRoutePolicyWriteCommand(write, rest, deps);
93
+ }
86
94
  return runCliAction(async () => {
87
95
  const [sub, ...rest] = argv;
88
96
  if (!sub) throw new CliUsageError("route policy requires a subcommand (list, show, dry-run, evaluate)", USAGE);
@@ -11,6 +11,8 @@
11
11
  */
12
12
  import { findLiveProxy, probeHostname, type LivenessIo, type LiveProxy } from "../server/proxy-liveness";
13
13
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
14
+ import { redactSecretArgs } from "./secret-args";
15
+ export { redactSecretArgs };
14
16
 
15
17
  export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
16
18
 
@@ -22,6 +24,8 @@ export interface RuntimeApiDeps {
22
24
  /** Test injection for commands that read a secret from stdin instead of argv. */
23
25
  stdinImpl?: CliStdin;
24
26
  stdinTimeoutMs?: number;
27
+ /** Cancellation for bounded secret-byte reads; line readers retain their contract. */
28
+ stdinSignal?: AbortSignal;
25
29
  /** Optional proxy liveness probe injection for commands that check or fall back around live runtime state. */
26
30
  findLiveProxy?: (io?: LivenessIo) => Promise<LiveProxy | null>;
27
31
  }
@@ -33,11 +37,15 @@ export class CliUsageError extends Error {
33
37
  }
34
38
  }
35
39
 
40
+ export const PROXY_NOT_RUNNING_MESSAGE = "Proxy is not running. Start the intended proxy with: ocx start. No request was sent.";
41
+
36
42
  export class RuntimeApiError extends Error {
37
43
  constructor(
38
44
  message: string,
39
45
  readonly status: number,
40
46
  readonly body: unknown,
47
+ /** Local discovery refusals occur before the management request is sent. */
48
+ readonly code?: "proxy_not_running" | "client_role_management_unavailable",
41
49
  ) {
42
50
  super(message);
43
51
  this.name = "RuntimeApiError";
@@ -70,12 +78,12 @@ function clientRoleManagementRefusal(port: number): string {
70
78
  export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
71
79
  if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
72
80
  const live = await (deps.findLiveProxy ?? findLiveProxy)();
73
- if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
81
+ if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
74
82
  // The role comes from the same identity-checked /healthz body liveness already parsed, so
75
83
  // this costs no extra request. Only the client role is refused: an absent role is a
76
84
  // standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
77
85
  if (live.role === "client") {
78
- throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null);
86
+ throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null, "client_role_management_unavailable");
79
87
  }
80
88
  return `http://${probeHostname(live.hostname)}:${live.port}`;
81
89
  }
@@ -85,6 +93,33 @@ function stringField(record: Record<string, unknown>, key: string): string | und
85
93
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
86
94
  }
87
95
 
96
+ function integrationRecoveryMessage(reason: string | undefined): string | undefined {
97
+ switch (reason) {
98
+ case "not_installed": return "The client is not installed. Run: ocx integration client status.";
99
+ case "non_loopback": return "The integration requires a loopback destination. Run: ocx integration client status.";
100
+ case "drift_requires_confirm": return "The client configuration changed. Run `ocx integration client status` before explicitly confirming restore.";
101
+ case "snapshot_expired": return "The recovery snapshot expired. Run `ocx integration client status` before creating a new change.";
102
+ case "superseded_store": return "The client uses a different configuration store. Run `ocx integration client status`, then disable and enable the selected client.";
103
+ case "unsafe": return "The client config path is unsafe to edit (for example a link or a competing file). Run `ocx integration client status` to see the affected path.";
104
+ case "conflict": return "The client config no longer matches the recorded ownership. Run `ocx integration client status`; add --overwrite-conflict to enable only if replacing it is intended.";
105
+ case "write_failed": return "The client config write failed. Run `ocx integration client status` before retrying.";
106
+ default: return undefined;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Integration writer detail is the only server prose this client appends beside the primary
112
+ * error. Scope it to the normalized request pathname (dot segments resolved, no query) and to
113
+ * an unredirected response so no other route's body can ride along.
114
+ */
115
+ function isIntegrationRoute(path: string, response: Response): boolean {
116
+ if (response.redirected) return false;
117
+ let pathname: string;
118
+ try { pathname = new URL(path.startsWith("/") ? path : "/" + path, "http://cli.invalid").pathname; }
119
+ catch { return false; }
120
+ return /^\/api\/client-integrations(?:\/|$)/.test(pathname);
121
+ }
122
+
88
123
  /**
89
124
  * Compose the operator-facing message from a management error body.
90
125
  *
@@ -102,7 +137,7 @@ function stringField(record: Record<string, unknown>, key: string): string | und
102
137
  * (#4662). Name the route instead, so any listener that does not serve a path stays legible
103
138
  * even if another one starts answering this way.
104
139
  */
105
- function responseMessage(body: unknown, status: number): string {
140
+ function responseMessage(body: unknown, status: number, integrationRoute = false): string {
106
141
  if (typeof body === "string" && body.trim()) return body.trim().slice(0, 400);
107
142
  if (!body || typeof body !== "object") return `Management request failed (${status})`;
108
143
  const record = body as Record<string, unknown>;
@@ -119,6 +154,13 @@ function responseMessage(body: unknown, status: number): string {
119
154
  if (primary) break;
120
155
  }
121
156
  const parts = [primary ?? `Management request failed (${status})`];
157
+ // Integration mutation routes carry a writer explanation beside a generic error. Its prose is
158
+ // server text (paths, upstream errors), so render fixed guidance keyed on the closed reason
159
+ // code instead of echoing it; other management bodies keep their presentation contract.
160
+ if (integrationRoute && stringField(record, "message") && stringField(record, "message") !== primary) {
161
+ const fixed = integrationRecoveryMessage(stringField(record, "reason"));
162
+ if (fixed) parts.push(`Details: ${fixed}`);
163
+ }
122
164
  const reason = stringField(record, "reason");
123
165
  // A body of {ok:false, reason:"…"} with no `error` key used to degrade to the
124
166
  // generic line, discarding the only actionable field.
@@ -158,7 +200,9 @@ export async function runtimeRequest<T = unknown>(
158
200
  try { body = JSON.parse(text); }
159
201
  catch { body = text; }
160
202
  }
161
- if (!response.ok) throw new RuntimeApiError(responseMessage(body, response.status), response.status, body);
203
+ if (!response.ok) {
204
+ throw new RuntimeApiError(responseMessage(body, response.status, isIntegrationRoute(path, response)), response.status, body);
205
+ }
162
206
  return body as T;
163
207
  }
164
208
 
@@ -226,79 +270,6 @@ export function csv(value: string | undefined): string[] | undefined {
226
270
  return [...new Set(value.split(",").map(item => item.trim()).filter(Boolean))];
227
271
  }
228
272
 
229
- /**
230
- * Options whose VALUE is a credential (or can carry one), listed here so a parse
231
- * error never prints one. `--headers` belongs on the list defensively: custom
232
- * headers are documented as non-secret metadata and the validator rejects the
233
- * standard credential names, but it cannot recognize an arbitrary one such as
234
- * `X-My-Token`, so a parse error must not echo the value back either way.
235
- *
236
- * `takeOption` only understands `--flag value`. `--flag=value` therefore falls
237
- * through to `rejectArgs`, which reports the offending argument verbatim — for
238
- * `--code=https://…?code=SECRET` that writes the authorization code to stderr,
239
- * which is the exact exposure the stdin path exists to avoid.
240
- */
241
- const SECRET_OPTIONS = [
242
- "--code",
243
- "--headers",
244
- "--token",
245
- "--admin-token",
246
- "--pairing-code",
247
- "--credential-env",
248
- "--admin-token-env",
249
- "--pairing-code-env",
250
- ];
251
-
252
- /**
253
- * Replace credential values before they are reported back.
254
- *
255
- * Both spellings have to be covered, and the space-separated one spans two
256
- * tokens: mistyping `ocx account cancel <p> --code <secret>` on a command that
257
- * does not parse `--code` leaves the flag AND its value in the leftovers, and
258
- * reporting them verbatim writes the credential to stderr. Repeating the
259
- * option does the same with the second value, since the parser takes only the
260
- * first occurrence.
261
- *
262
- * The token after the option is redacted whatever it looks like. Skipping
263
- * `--`-prefixed tokens read as "that is a flag, not a value", but the shell
264
- * hands over whatever was typed: `--code --SUPERSECRET` and
265
- * `--code -- SUPERSECRET` both put the credential straight in the message. A
266
- * mistaken `--code --json` now reads `--code <redacted>`, which is worse
267
- * diagnostics for a case that already prints the usage text, and better than
268
- * printing a credential.
269
- *
270
- * `redactValues` extends that to bare leftovers, for commands whose positional
271
- * argument is itself a credential.
272
- */
273
- function redactSecretArgs(args: string[], redactValues = false): string[] {
274
- const out: string[] = [];
275
- for (let index = 0; index < args.length; index++) {
276
- const arg = args[index] as string;
277
- const inline = SECRET_OPTIONS.find(option => arg.startsWith(`${option}=`));
278
- if (inline) {
279
- out.push(`${inline}=<redacted>`);
280
- continue;
281
- }
282
- if (SECRET_OPTIONS.includes(arg)) {
283
- out.push(arg);
284
- // Swallow the value that belongs to it. `--` is an end-of-options
285
- // separator, so the value is the token after it.
286
- let valueIndex = index + 1;
287
- if (args[valueIndex] === "--") {
288
- out.push("--");
289
- valueIndex++;
290
- }
291
- if (args[valueIndex] !== undefined) {
292
- out.push("<redacted>");
293
- index = valueIndex;
294
- }
295
- continue;
296
- }
297
- out.push(redactValues && !arg.startsWith("-") ? "<redacted>" : arg);
298
- }
299
- return out;
300
- }
301
-
302
273
  export interface RejectArgsOptions {
303
274
  /**
304
275
  * Report bare leftovers as `<redacted>`. Set by commands where a stray
@@ -399,16 +370,19 @@ export async function readSecretBytes(
399
370
  deps: RuntimeApiDeps,
400
371
  label: string,
401
372
  maxBytes = MAX_LINK_CREDENTIAL_BYTES,
402
- ): Promise<Uint8Array> {
373
+ ): Promise<Uint8Array<ArrayBuffer>> {
403
374
  const input: CliStdin = deps.stdinImpl ?? process.stdin;
404
375
  const timeoutMs = deps.stdinTimeoutMs ?? 120_000;
376
+ const signal = deps.stdinSignal;
377
+ signal?.throwIfAborted();
405
378
  if (input.readableEnded === true) throw new CliUsageError(`${label} input was empty`);
406
- return await new Promise<Uint8Array>((resolve, reject) => {
379
+ return await new Promise<Uint8Array<ArrayBuffer>>((resolve, reject) => {
407
380
  const chunks: Uint8Array[] = [];
408
381
  let total = 0;
409
382
  let settled = false;
410
383
  const cleanup = () => {
411
384
  clearTimeout(timer);
385
+ signal?.removeEventListener("abort", onAbort);
412
386
  input.removeListener("data", onData);
413
387
  input.removeListener("end", onEnd);
414
388
  input.removeListener("error", onError);
@@ -459,6 +433,7 @@ export async function readSecretBytes(
459
433
  resolve(result);
460
434
  });
461
435
  const onError = (error: Error) => finish(() => reject(error));
436
+ const onAbort = () => finish(() => reject(signal?.reason));
462
437
  const timer = setTimeout(
463
438
  () => finish(() => reject(new CliUsageError(`timed out waiting for ${label} on stdin`))),
464
439
  timeoutMs,
@@ -466,12 +441,14 @@ export async function readSecretBytes(
466
441
  input.on("data", onData);
467
442
  input.on("end", onEnd);
468
443
  input.on("error", onError);
444
+ signal?.addEventListener("abort", onAbort, { once: true });
445
+ if (signal?.aborted) onAbort();
469
446
  });
470
447
  }
471
448
 
472
449
  export function printData(value: unknown, wantsJson: boolean, lines?: string[]): void {
473
450
  if (wantsJson || !lines) console.log(JSON.stringify(value, null, 2));
474
- else for (const line of lines) console.log(line);
451
+ else for (const line of lines) console.log(terminalSafeText(line));
475
452
  }
476
453
 
477
454
  /**