@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
package/src/cli/doctor.ts CHANGED
@@ -10,11 +10,14 @@
10
10
  import { accessSync, constants, existsSync, readFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { dirname, join } from "node:path";
13
- import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
13
+ import { getConfigDir, getConfigPath, readConfigDiagnostics, resolveEnvValue } from "../config";
14
+ import { readPid } from "../config/process-state";
14
15
  import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
15
16
  import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
16
17
  import type { BunRuntimeSource } from "../lib/bun-runtime";
17
18
  import { maskAccountId } from "../lib/privacy";
19
+ import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
20
+ import { readInstalledServiceToken } from "../lib/service-secrets";
18
21
  import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
19
22
  import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
20
23
  import { readCodexTokens } from "../codex/auth-collision";
@@ -41,7 +44,7 @@ import {
41
44
  resolveEffectiveUserIdentity,
42
45
  } from "../codex/user-identity";
43
46
  import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
44
- import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
47
+ import { collectStartupHealth, formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
45
48
  import {
46
49
  displayCodexRuntimePath,
47
50
  loadLastEffortClamp,
@@ -57,7 +60,35 @@ import {
57
60
  } from "../server/local-management-read-client";
58
61
  export { resolveCodexHomeDir } from "../codex/home";
59
62
 
60
- export type OAuthDoctorCheck = { level: "OK" | "WARN"; message: string };
63
+ /**
64
+ * `FAIL` exists for a condition that makes the surface unusable rather than degraded.
65
+ * A review of the #2696 work pointed out that reporting a fully fenced management plane
66
+ * — every `/api/*` returning 503 — at the same level as a directory-permission note
67
+ * misleads the reader about severity.
68
+ *
69
+ * Doctor's own exit code still belongs to the uniform contract in wp3b (devlog 025);
70
+ * this type only fixes what the operator is told.
71
+ */
72
+ export type OAuthDoctorCheck = { level: "OK" | "WARN" | "FAIL"; message: string };
73
+
74
+ /**
75
+ * Whether any FAIL-level condition was seen during this `runDoctor` pass.
76
+ *
77
+ * Module-scoped and reset at the top of `runDoctor` rather than threaded through, because
78
+ * `runDoctor` reports by direct `console.log` across a dozen sections and has no checks
79
+ * collection to inspect. Reset matters for the test suite, which calls `runDoctor` several
80
+ * times in one process; a sticky flag would make the second call fail because the first did.
81
+ */
82
+ let doctorSawFailure = false;
83
+
84
+ function recordDoctorFailure(): void {
85
+ doctorSawFailure = true;
86
+ }
87
+
88
+ /** True when the last `runDoctor` pass saw a FAIL-level condition. */
89
+ export function doctorFailed(): boolean {
90
+ return doctorSawFailure;
91
+ }
61
92
 
62
93
  function pathIsWritable(path: string): boolean {
63
94
  try {
@@ -136,6 +167,50 @@ function describeDoctorHealth(entry: OAuthHealthEntry): string {
136
167
  }
137
168
  }
138
169
 
170
+ /**
171
+ * Detect the management/data-plane credential collision behind #2696.
172
+ *
173
+ * The service exports the service token file as `OPENCODEX_API_AUTH_TOKEN` before
174
+ * starting the proxy. When that value is the admin token, the server treats the
175
+ * management credential as a data-plane admission secret and fences the ENTIRE
176
+ * management plane closed at boot: every `/api/*` returns 503, including on a loopback
177
+ * install that never needed a data-plane secret.
178
+ *
179
+ * `assertNotAdminToken` in src/service.ts now refuses to create this state, but an
180
+ * install made before that guard existed is already broken on disk, and the symptom
181
+ * (every management command failing) points nowhere. This is the check that names it.
182
+ *
183
+ * Observe-only, like the rest of doctor: it compares shapes and never prints, logs, or
184
+ * returns a credential value.
185
+ */
186
+ export function dataPlaneCredentialCollisionCheck(
187
+ env: NodeJS.ProcessEnv = process.env,
188
+ installedServiceToken: string | null = readInstalledServiceToken(),
189
+ ): OAuthDoctorCheck {
190
+ const dataPlane = env.OPENCODEX_API_AUTH_TOKEN?.trim() || installedServiceToken?.trim() || "";
191
+ if (!dataPlane) {
192
+ return { level: "OK", message: "No data-plane token is set, so it cannot collide with the management token." };
193
+ }
194
+ // Same comparison as assertNotAdminToken: minted prefix or configuredAdminToken
195
+ // (env or admin-api-token file). The file token is the one the service wrapper
196
+ // actually exports; inspecting only the doctor process env reported OK on every
197
+ // already-broken install (#2696).
198
+ if (!tokenCollidesWithAdmin(dataPlane, env)) {
199
+ return { level: "OK", message: "Data-plane and management credentials are distinct." };
200
+ }
201
+ return {
202
+ // Not a degradation: while this holds, every /api/* returns 503 and no ocx
203
+ // management command can work at all.
204
+ level: "FAIL",
205
+ message:
206
+ "The data-plane secret (OPENCODEX_API_AUTH_TOKEN or the service token file) holds the "
207
+ + "management (admin) token, so the proxy fences the whole management API closed and "
208
+ + "every ocx management command fails with 503. "
209
+ + "Action: unset OPENCODEX_API_AUTH_TOKEN, replace the service token file with a distinct "
210
+ + "data-plane key, then re-run `ocx service install` and restart the proxy",
211
+ };
212
+ }
213
+
139
214
  /**
140
215
  * OAuth reliability checks for `ocx doctor`. Observe-only: never mutates
141
216
  * credentials, locks, or networking. Every WARN includes a recovery Action.
@@ -146,6 +221,8 @@ export async function collectOAuthDoctorChecks(
146
221
  ): Promise<OAuthDoctorCheck[]> {
147
222
  const checks: OAuthDoctorCheck[] = [];
148
223
 
224
+ checks.push(dataPlaneCredentialCollisionCheck());
225
+
149
226
  if (isOAuthCredentialStorageWritable()) {
150
227
  checks.push({ level: "OK", message: "OAuth credential storage directory is writable for atomic auth.json updates." });
151
228
  } else {
@@ -934,6 +1011,9 @@ export async function runDoctor(args: string[] = []): Promise<void> {
934
1011
  }
935
1012
 
936
1013
  console.log("opencodex doctor\n");
1014
+ // Reset per pass: the suite drives runDoctor several times in one process, and a sticky
1015
+ // flag would fail the second call because the first saw a problem.
1016
+ doctorSawFailure = false;
937
1017
 
938
1018
  // Ordering note: the memory/runtime section renders after "Running proxy
939
1019
  // process proxy env" below; helpers live above runDoctor for testability.
@@ -982,7 +1062,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
982
1062
  const startup = collectStartupHealth(doctorConfig);
983
1063
  console.log("\nCodex restart safety");
984
1064
  console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
985
- console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);
1065
+ console.log(` ${formatStartupRoutingDetail(startup)}`);
986
1066
 
987
1067
  console.log("\nCodex runtime selection");
988
1068
  {
@@ -1019,6 +1099,20 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1019
1099
  configFn: () => ({ port: doctorConfig.port, hostname: doctorConfig.hostname }),
1020
1100
  });
1021
1101
 
1102
+ // Mirrors `ocx status` through the same comparison rather than a second implementation:
1103
+ // two diagnostics disagreeing about whether an install is stale is worse than one (#2701).
1104
+ // No extra probe -- findLiveProxy already carried the version back.
1105
+ {
1106
+ const { packageVersion } = await import("./help");
1107
+ const { computeVersionSkew } = await import("./version-skew");
1108
+ const skew = computeVersionSkew(packageVersion(), live?.version);
1109
+ if (skew.skewed && skew.warning) {
1110
+ console.log(`!! ${skew.warning}`);
1111
+ } else if (skew.proxyVersion !== null) {
1112
+ console.log(`ok ocx ${skew.cliVersion} matches the running proxy`);
1113
+ }
1114
+ }
1115
+
1022
1116
  const currentProxyEnv = collectProxyEnv();
1023
1117
  const configuredProxy = collectConfiguredProxy();
1024
1118
  const runningProxyEnv = collectRunningProxyEnv({
@@ -1072,10 +1166,10 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1072
1166
  console.log(` ${probe.ok ? "ok " : "-- "} ${WHAM_USAGE_URL}`);
1073
1167
  console.log(` ${detail}, ${probe.durationMs}ms, ${probe.authenticated ? "authenticated" : "unauthenticated"}`);
1074
1168
 
1075
- // Design B upgrade visibility: threads still tagged opencodex are invisible to the native
1076
- // Codex app until the one-time migration lands. Read-only probe (readonly sqlite, 100ms
1077
- // busy timeout) reports state, never mutates.
1078
- console.log("\nCodex history migration");
1169
+ // Design B upgrade visibility: only the backup manifest authorizes restoring provider
1170
+ // metadata. Bare routed rows have unknown provenance and remain unchanged. This read-only
1171
+ // probe reports manifest work and database readability; it never mutates.
1172
+ console.log("\nCodex history metadata restore");
1079
1173
  // The history failure messages point here; make the visit worthwhile by
1080
1174
  // probing the coordinator namespace the locks live in. The probe exercises
1081
1175
  // identity, runtime-root, and permission checks without taking any lock or
@@ -1096,11 +1190,17 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1096
1190
  for (const line of formatCoordinatorDoctorLines(inspectCodexCoordinator())) console.log(line);
1097
1191
  const pending = countPendingOpencodexHistory();
1098
1192
  if (pending.failed) {
1099
- console.log(" -- state DB locked or unreadable (Codex app open?) — migration state unknown");
1193
+ if (pending.failureReason === "busy") {
1194
+ console.log(" -- history database, backup manifest, or rollout file is busy — exact metadata restore is pending");
1195
+ } else if (pending.failureReason === "permission") {
1196
+ console.log(" -- state DB or backup manifest access was denied — restore state unknown");
1197
+ } else {
1198
+ console.log(" -- backup manifest or restore target failed integrity checks — manual review required");
1199
+ }
1100
1200
  } else if (pending.pendingRows === 0 && pending.backupEntries === 0) {
1101
- console.log(" ok no legacy opencodex-tagged threads pending");
1201
+ console.log(" ok no manifest-backed provider metadata pending; untracked routed history is unchanged");
1102
1202
  } else {
1103
- console.log(` -- ${pending.pendingRows} thread(s) still tagged opencodex, ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"}`);
1203
+ console.log(` -- ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"} pending exact metadata restore`);
1104
1204
  }
1105
1205
 
1106
1206
  console.log("\nProject Codex configs");
@@ -1141,6 +1241,12 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1141
1241
  console.log("\nOAuth reliability");
1142
1242
  for (const check of await collectOAuthDoctorChecks()) {
1143
1243
  console.log(` [${check.level}] ${check.message}`);
1244
+ // A diagnostic that always exits 0 cannot gate anything, which defeats the point of
1245
+ // running it from a script (#2697's sibling defect). FAIL is the level reserved for a
1246
+ // surface that is unusable rather than degraded, so it -- and only it -- fails the
1247
+ // command. WARN stays exit 0 on purpose: warning on a degraded-but-working install
1248
+ // must not break a pipeline that is legitimately green.
1249
+ if (check.level === "FAIL") recordDoctorFailure();
1144
1250
  }
1145
1251
 
1146
1252
  // #857: a running Codex app-server can keep an older in-memory catalog than
@@ -1148,7 +1254,7 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1148
1254
  const { collectCodexAppServerCatalogState } = await import("../codex/app-server-processes");
1149
1255
  const catalogState = collectCodexAppServerCatalogState();
1150
1256
  if (catalogState.state === "stale") {
1151
- console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`)`);
1257
+ console.log(` [WARN] Codex app-server (PID(s): ${catalogState.processes.map(p => p.pid).join(", ")}) started before the on-disk catalog changed; its in-memory model list disagrees with ocx. Action: restart Codex (or run \`ocx sync --restart-codex\`; on Windows the desktop app may need \`ocx sync --restart-desktop-app\`)`);
1152
1258
  } else if (catalogState.state === "unknown") {
1153
1259
  console.log(" [WARN] Could not verify whether the running Codex app-server's model catalog is current (start time or catalog unreadable). Action: if the model list looks stale, restart Codex");
1154
1260
  } else if (catalogState.state === "fresh") {
@@ -1185,8 +1291,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1185
1291
  }
1186
1292
  }
1187
1293
  }
1188
- if (pending.failed || pending.pendingRows > 0 || pending.backupEntries > 0) {
1189
- hints.push("Legacy chat threads are still tagged opencodex (or the DB was locked). The running proxy retries the migration automatically; to force it now, close the Codex app and run 'ocx sync'.");
1294
+ if (pending.failed && pending.failureReason === "busy") {
1295
+ hints.push("Backed-up history metadata is pending or its state is unreadable. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
1296
+ } else if (pending.failed && pending.failureReason === "permission") {
1297
+ hints.push("Backed-up history metadata could not be inspected because access was denied. Fix access to the reported Codex history paths, then run 'ocx sync'; repeated retries do not repair permissions.");
1298
+ } else if (pending.failed) {
1299
+ hints.push("The history manifest or its target is invalid or changed. Preserve both, inspect the manifest/database/rollout identity, and do not repeatedly run 'ocx sync' until the mismatch is understood. Untracked routed history is not relabeled.");
1300
+ } else if (pending.backupEntries > 0) {
1301
+ hints.push("Backed-up history metadata is pending. The running proxy retries exact restoration automatically; to force it now, close the Codex app and run 'ocx sync'. Untracked routed history is not relabeled.");
1190
1302
  }
1191
1303
  if (dual.dualInstall && !dual.effectiveIsWindowsMount) {
1192
1304
  hints.push(`Codex is installed on BOTH WSL and Windows. Each side keeps its own ~/.codex (logins, config, catalog are separate); ocx here manages the Linux one. To share a single home, set CODEX_HOME=${dual.windowsCodexHomes[0] ?? `${dual.automountRoot}/c/Users/<you>/.codex`} in WSL (drvfs file locking is less reliable).`);
package/src/cli/help.ts CHANGED
@@ -5,7 +5,14 @@ import { findCommand } from "./registry";
5
5
 
6
6
  const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
7
7
 
8
- function packageVersion(): string {
8
+ /**
9
+ * Version of the `ocx` bundle this process is running from.
10
+ *
11
+ * Exported so `status`/`doctor` can compare it against the version the live proxy reports,
12
+ * which is how a stale `ocx` earlier on PATH becomes visible (#2701). Returns `"unknown"`
13
+ * rather than throwing; callers must treat that as "cannot compare", not as a mismatch.
14
+ */
15
+ export function packageVersion(): string {
9
16
  const raw = readFileSync(join(repoRoot, "package.json"), "utf8");
10
17
  const parsed = JSON.parse(raw) as { version?: unknown };
11
18
  return typeof parsed.version === "string" ? parsed.version : "unknown";
@@ -24,8 +31,8 @@ Usage:
24
31
  ocx stop Stop the proxy AND restore native Codex (plain codex works again)
25
32
  ocx restore Restore native Codex without stopping (alias: eject)
26
33
  ocx restore back Re-point codex at the running proxy (undo restore)
27
- ocx recover-history --legacy-openai
28
- Explicitly recover pre-backup syncResumeHistory rows
34
+ ocx recover-history --legacy-openai --yes
35
+ Force all user-message opencodex rows to OpenAI (legacy recovery)
29
36
  ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
30
37
  ocx service [sub] Run as a background service (default: install/update/start)
31
38
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
@@ -49,21 +56,25 @@ Usage:
49
56
  ocx restart Stop and restart the proxy
50
57
  ocx v2 <sub> multi_agent_v2 surface (status|on|off|mode|keep-native-v1|threads|mode-hint)
51
58
  ocx health [--json] Check proxy health (exit 0=healthy, 1=not)
59
+ ocx capabilities [--json] List declared capabilities and the API routes they drive
52
60
  ocx ready [--json] [--wait [--timeout <s>]] Check post-sync readiness (exit 0 only when ready)
53
61
  ocx provider <sub> Providers, connectivity, quota, and selected models
54
62
  ocx account <sub> Accounts, login/reauth, key pools, and quota controls
55
63
  ocx models <sub> Live/custom models, visibility, context, and shadow calls
64
+ ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
56
65
  ocx combo <sub> Combo failover/round-robin routing
57
66
  ocx agent <sub> Subagents, injection, effort caps, and sidecars
58
67
  ocx observe <sub> Logs, usage, storage, memory, and debug data
68
+ ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
59
69
  ocx route <sub> Routing features (combo, policy)
60
70
  ocx logs [filters] Alias of ocx observe logs
61
- ocx usage [--range <7d|30d|all>] Alias of ocx observe usage
62
- ocx storage [--json] Alias of ocx observe storage
71
+ ocx usage [--range <today|1d|7d|30d|all>] [--provider <name>] [--model <id>]
72
+ Token and estimated-cost report (alias of ocx observe usage)
73
+ ocx storage <sub> Storage report, cleanup, trash, and the cleanup policy
63
74
  ocx memory [--json] Alias of ocx observe memory
64
75
  ocx api-key <sub> Alias of ocx access key
65
76
  ocx access <sub> External API keys and endpoint information
66
- ocx export --client <id> Print a client config wired to the running proxy (11 clients)
77
+ ocx export --client <id> Print a client config wired to the running proxy (12 clients)
67
78
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
68
79
  ocx grok <sub> Grok Build model selection and apply
69
80
  ocx system <sub> Runtime settings, startup, sync, and updates
package/src/cli/index.ts CHANGED
@@ -12,6 +12,9 @@ import {
12
12
  codexAutoStartEnabled,
13
13
  getConfigDir,
14
14
  loadConfig,
15
+ saveConfig,
16
+ } from "../config";
17
+ import {
15
18
  readPid,
16
19
  readPidFileValue,
17
20
  readRuntimePort,
@@ -19,11 +22,11 @@ import {
19
22
  removePidIfValueIs,
20
23
  removeRuntimePort,
21
24
  removeRuntimePortIfPidIs,
22
- saveConfig,
23
25
  writePid,
24
26
  writeRuntimePort,
25
- } from "../config";
26
- import { collectStatus } from "./status";
27
+ } from "../config/process-state";
28
+ import { collectStatus, unusedProxyWarningLines } from "./status";
29
+ import { takeFlag } from "./runtime-api";
27
30
 
28
31
  import {
29
32
  discoverStableProxyForRestart,
@@ -43,8 +46,8 @@ import { runReady, type ReadyArgs } from "./ready";
43
46
  import { runCli } from "./root";
44
47
  import { ProxyOwnershipRefusedError, stopProxy } from "../lib/process-control";
45
48
  import { loadServiceTokenFromFile } from "../lib/service-secrets";
46
- import { diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
47
- import { startupHealthSummary } from "../codex/autostart-health";
49
+ import { assertNotAdminToken, diagnoseService, isServiceOwnershipError, serviceCommand, serviceEnvironmentOwnedHere, serviceStartableFromTray, serviceStatusSummary, stopServiceIfInstalled, uninstallServiceIfInstalled } from "../service";
50
+ import { formatStartupRoutingDetail, startupHealthSummary } from "../codex/autostart-health";
48
51
  import { drainAndShutdown, isRecyclingForExit, startServer } from "../server";
49
52
  import { injectSystemEnv, reconcileShellHook, revertSystemEnv, uninstallShellHook } from "../server/system-env";
50
53
  import { buildDesktop3pRegistry } from "../claude/desktop-3p";
@@ -221,6 +224,11 @@ async function handleStart(options: { block?: boolean } = {}) {
221
224
  // auth path reads OPENCODEX_API_AUTH_TOKEN from the environment.
222
225
  const serviceToken = loadServiceTokenFromFile(process.env);
223
226
  if (serviceToken) process.env.OPENCODEX_API_AUTH_TOKEN = serviceToken;
227
+ // The service wrapper (and WinSW via OCX_API_TOKEN_FILE) can still export a colliding
228
+ // token that install now refuses to write. Refuse it here too, before bind, so an
229
+ // already-broken file cannot fence /api/* closed at boot (#2696).
230
+ const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
231
+ if (present) assertNotAdminToken(present);
224
232
  const requestedPort = parsePortOption();
225
233
  const owner = await findProxyOwnerBeforeJournalRecovery();
226
234
  if (owner.live) {
@@ -828,8 +836,12 @@ async function handleUninstall() {
828
836
 
829
837
  async function handleStatus() {
830
838
  const statusArgs = args.slice(1);
831
- const wantsJson = statusArgs.length === 1 && statusArgs[0] === "--json";
832
- if (statusArgs.length > 1 || (statusArgs.length === 1 && !wantsJson)) {
839
+ // Order-independent: the previous form only honoured `--json` as the LONE argument, so
840
+ // `ocx status --json --anything` silently printed human output to a caller that asked
841
+ // for JSON. Take the flag out of argv, then reject whatever is left over -- which keeps
842
+ // the strict unknown-argument behaviour rather than trading one defect for another.
843
+ const wantsJson = takeFlag(statusArgs, "--json");
844
+ if (statusArgs.length > 0) {
833
845
  console.error("Usage: ocx status [--json]");
834
846
  process.exit(1);
835
847
  }
@@ -846,6 +858,18 @@ async function handleStatus() {
846
858
  console.log(`❌ Proxy: ${status.proxyLabel}`);
847
859
  }
848
860
  console.log(` Health: ${status.healthLabel}`);
861
+ // Printed here, not only in --json: a stale ocx on PATH is exactly the situation where
862
+ // the operator is reading human output and wondering why the CLI disagrees with the
863
+ // dashboard. Adding the JSON field alone would satisfy a test and help nobody (#2701).
864
+ if (status.json.versionSkew.warning) {
865
+ console.log(` ⚠️ ${status.json.versionSkew.warning}`);
866
+ }
867
+ for (const line of unusedProxyWarningLines({
868
+ proxyUp: Boolean(status.json.proxy.pid || status.json.proxy.health.ok),
869
+ routingKind: status.json.startup.routingKind,
870
+ })) {
871
+ console.log(` ${line}`);
872
+ }
849
873
  if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
850
874
  console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
851
875
  // The service summary a few lines below already tells a registered-but-not-serving
@@ -865,6 +889,7 @@ async function handleStatus() {
865
889
  console.log(` Default provider: ${status.json.defaultProvider}`);
866
890
  console.log(` Codex autostart: ${status.json.codexAutostart ? "enabled" : "disabled"}`);
867
891
  console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
892
+ console.log(` ${formatStartupRoutingDetail(status.json.startup)}`);
868
893
  console.log(` Service: ${status.json.service.summary}`);
869
894
  console.log(` ${status.json.codexShim.summary}`);
870
895
  console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
@@ -905,8 +930,13 @@ async function handleStatus() {
905
930
 
906
931
  async function handleRecoverHistory() {
907
932
  if (args[1] !== "--legacy-openai") {
908
- console.error("Usage: ocx recover-history --legacy-openai");
909
- console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
933
+ console.error("Usage: ocx recover-history --legacy-openai --yes");
934
+ console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
935
+ process.exit(1);
936
+ }
937
+ console.error("WARNING: this force-relabels every user-message opencodex row to OpenAI, normalizes exec to cli, and includes legitimate dedicated-provider history.");
938
+ if (args.length !== 3 || args[2] !== "--yes") {
939
+ console.error("Re-run with explicit confirmation: ocx recover-history --legacy-openai --yes");
910
940
  process.exit(1);
911
941
  }
912
942
  // Manifest-independent legacy ejection, serialized like every other history
package/src/cli/init.ts CHANGED
@@ -165,6 +165,7 @@ export async function runInit(): Promise<void> {
165
165
  port,
166
166
  providers: { [providerName]: providerConfig },
167
167
  defaultProvider: providerName,
168
+ modelDiscovery: { newModelPolicy: "off" },
168
169
  };
169
170
 
170
171
  saveConfig(config);
@@ -0,0 +1,230 @@
1
+ /**
2
+ * `ocx system-surface` — the remaining routes that had no CLI caller at all (wp7).
3
+ *
4
+ * These are grouped by what an operator is trying to do, not by which server module owns them:
5
+ * inspecting effective configuration, reading the generated client-config snippet, toggling the
6
+ * native client integrations, checking request pacing and routing analytics, and reading the
7
+ * Codex system prompt.
8
+ *
9
+ * One route here is deliberately READ-ONLY forever. `POST /api/github/star` spends the operator's
10
+ * GitHub identity, and no CLI flag can carry that consent — the server requires a real dashboard
11
+ * session for it. So `star` reads status and says what it cannot do, rather than offering a
12
+ * `--yes` that would be a lie.
13
+ */
14
+ import {
15
+ CliUsageError,
16
+ printData,
17
+ rejectArgs,
18
+ runCliAction,
19
+ runtimeRequest,
20
+ summaryLines,
21
+ takeFlag,
22
+ takeOption,
23
+ type RuntimeApiDeps,
24
+ } from "./runtime-api";
25
+
26
+ const USAGE = `Usage:
27
+ ocx inspect config [--json]
28
+ ocx inspect catalog [--json]
29
+ ocx inspect routing-analytics [--json]
30
+ ocx inspect pacing [--name <provider>] [--json]
31
+ ocx inspect key-providers [--json]
32
+ ocx inspect codex-prompt [--text] [--json]
33
+ ocx inspect client-config --client <id> [--json]
34
+ ocx inspect star [--json]
35
+ ocx inspect windows-tray [--json]
36
+ ocx integration native [list] [--json]
37
+ ocx integration native <claude|claude-desktop|codex|grok> <on|off> [--json]
38
+ ocx agent request-user-input [on|off] [--json]`;
39
+
40
+ /** A read that takes no arguments beyond `--json`. */
41
+ async function read(path: string, argv: string[], deps: RuntimeApiDeps): Promise<void> {
42
+ const args = [...argv];
43
+ const wantsJson = takeFlag(args, "--json");
44
+ rejectArgs(args, USAGE);
45
+ const result = await runtimeRequest(path, {}, deps);
46
+ printData(result, wantsJson, summaryLines(result));
47
+ }
48
+
49
+ const NATIVE_CLIENTS = ["claude", "claude-desktop", "codex", "grok"] as const;
50
+
51
+ interface NativeClientRow {
52
+ clientId?: string;
53
+ state?: string;
54
+ installed?: boolean;
55
+ desiredEnabled?: boolean;
56
+ configPath?: string;
57
+ disableBlocked?: unknown;
58
+ }
59
+
60
+ /**
61
+ * The shared depth-1 flattener renders an array as "N item(s)", so a bare
62
+ * `integration native list` printed `clients: 4 item(s)` -- the per-client state the operator
63
+ * asked for was in the payload and discarded before the terminal. Same defect class wp4 fixed
64
+ * for the account and key tables.
65
+ */
66
+ function nativeLines(payload: unknown): string[] {
67
+ const clients = (payload as { clients?: NativeClientRow[] } | null)?.clients;
68
+ if (!Array.isArray(clients) || clients.length === 0) return ["No native client integrations reported."];
69
+ const lines = ["CLIENT STATE INSTALLED DESIRED CONFIG"];
70
+ for (const row of clients) {
71
+ lines.push([
72
+ (row.clientId ?? "?").padEnd(16),
73
+ (row.state ?? "?").padEnd(10),
74
+ (row.installed === true ? "yes" : row.installed === false ? "no" : "?").padEnd(10),
75
+ (row.desiredEnabled === true ? "on" : row.desiredEnabled === false ? "off" : "?").padEnd(8),
76
+ row.configPath ?? "",
77
+ ].join(" ").trimEnd());
78
+ // A blocked disable is the reason a toggle did not take effect, so it is never silent.
79
+ if (row.disableBlocked !== null && row.disableBlocked !== undefined) {
80
+ lines.push(` disable blocked: ${typeof row.disableBlocked === "string" ? row.disableBlocked : JSON.stringify(row.disableBlocked)}`);
81
+ }
82
+ }
83
+ return lines;
84
+ }
85
+
86
+ async function nativeIntegration(argv: string[], deps: RuntimeApiDeps): Promise<void> {
87
+ const action = argv[0] && !argv[0].startsWith("-") ? argv[0] : "list";
88
+ const rest = argv[0] && !argv[0].startsWith("-") ? argv.slice(1) : argv;
89
+
90
+ if (action === "list") {
91
+ const args = [...rest];
92
+ const wantsJson = takeFlag(args, "--json");
93
+ rejectArgs(args, USAGE);
94
+ const result = await runtimeRequest("/api/native-integrations", {}, deps);
95
+ printData(result, wantsJson, nativeLines(result));
96
+ return;
97
+ }
98
+
99
+ if (!(NATIVE_CLIENTS as readonly string[]).includes(action)) {
100
+ throw new CliUsageError(`unknown native client ${action}; expected one of ${NATIVE_CLIENTS.join(", ")}`, USAGE);
101
+ }
102
+
103
+ const args = [...rest];
104
+ const wantsJson = takeFlag(args, "--json");
105
+ const state = args.shift();
106
+ rejectArgs(args, USAGE);
107
+ if (state !== "on" && state !== "off") {
108
+ throw new CliUsageError(`expected on or off after ${action}`, USAGE);
109
+ }
110
+
111
+ // Toggling rewrites the client's own config file, which is why each client has its own route
112
+ // rather than one route with a client parameter.
113
+ const result = await runtimeRequest(`/api/native-integrations/${action}`, {
114
+ method: "PUT",
115
+ headers: { "content-type": "application/json" },
116
+ body: JSON.stringify({ enabled: state === "on" }),
117
+ }, deps);
118
+ printData(result, wantsJson, summaryLines(result));
119
+ }
120
+
121
+ /**
122
+ * Exported unwrapped so `ocx agent request-user-input` can call it INSIDE its own
123
+ * `runCliAction`. Wrapping an already-wrapped handler reports one failure twice: the inner
124
+ * wrapper prints the error and returns a code, and the outer one prints again on the rethrow.
125
+ */
126
+ export async function requestUserInputAction(argv: string[], deps: RuntimeApiDeps): Promise<void> {
127
+ const args = [...argv];
128
+ const wantsJson = takeFlag(args, "--json");
129
+ const state = args.shift();
130
+ rejectArgs(args, USAGE);
131
+
132
+ const path = "/api/codex-auth/features/default-mode-request-user-input";
133
+ // No argument means show. A read must not write the value it is reporting.
134
+ if (state === undefined) {
135
+ const result = await runtimeRequest(path, {}, deps);
136
+ printData(result, wantsJson, summaryLines(result));
137
+ return;
138
+ }
139
+ if (state !== "on" && state !== "off") throw new CliUsageError("expected on or off", USAGE);
140
+
141
+ const result = await runtimeRequest(path, {
142
+ method: "PUT",
143
+ headers: { "content-type": "application/json" },
144
+ body: JSON.stringify({ enabled: state === "on" }),
145
+ }, deps);
146
+ printData(result, wantsJson, summaryLines(result));
147
+ }
148
+
149
+ async function clientConfig(argv: string[], deps: RuntimeApiDeps): Promise<void> {
150
+ const args = [...argv];
151
+ const wantsJson = takeFlag(args, "--json");
152
+ const client = takeOption(args, "--client");
153
+ rejectArgs(args, USAGE);
154
+ if (!client) throw new CliUsageError("--client is required", USAGE);
155
+ // The valid client list is not duplicated here: the route answers 400 naming every accepted id,
156
+ // which is more useful than a local list that can drift out of date.
157
+ const result = await runtimeRequest(`/api/client-config?client=${encodeURIComponent(client)}`, {}, deps);
158
+ printData(result, wantsJson, summaryLines(result));
159
+ }
160
+
161
+ async function pacing(argv: string[], deps: RuntimeApiDeps): Promise<void> {
162
+ const args = [...argv];
163
+ const wantsJson = takeFlag(args, "--json");
164
+ const name = takeOption(args, "--name");
165
+ rejectArgs(args, USAGE);
166
+ const suffix = name ? `?name=${encodeURIComponent(name)}` : "";
167
+ const result = await runtimeRequest(`/api/provider-request-pacing${suffix}`, {}, deps);
168
+ printData(result, wantsJson, summaryLines(result));
169
+ }
170
+
171
+ async function codexPrompt(argv: string[], deps: RuntimeApiDeps): Promise<void> {
172
+ const args = [...argv];
173
+ const wantsJson = takeFlag(args, "--json");
174
+ const asText = takeFlag(args, "--text");
175
+ rejectArgs(args, USAGE);
176
+ if (asText && wantsJson) throw new CliUsageError("--text and --json cannot be combined", USAGE);
177
+ if (asText) {
178
+ // The /text variant answers the prompt body itself, so it is printed verbatim rather than
179
+ // flattened through the summary renderer.
180
+ const result = await runtimeRequest<unknown>("/api/codex-prompt/text", {}, deps);
181
+ console.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
182
+ return;
183
+ }
184
+ const result = await runtimeRequest("/api/codex-prompt", {}, deps);
185
+ printData(result, wantsJson, summaryLines(result));
186
+ }
187
+
188
+ async function star(argv: string[], deps: RuntimeApiDeps): Promise<void> {
189
+ const args = [...argv];
190
+ const wantsJson = takeFlag(args, "--json");
191
+ rejectArgs(args, USAGE);
192
+ const result = await runtimeRequest<Record<string, unknown>>("/api/github/star", {}, deps);
193
+ if (wantsJson) {
194
+ printData(result, true);
195
+ return;
196
+ }
197
+ printData(result, false, summaryLines(result));
198
+ // Said plainly, because the natural next question is "then star it", and the answer is that no
199
+ // CLI invocation can: the POST requires a dashboard session precisely so an agent cannot spend
200
+ // the operator's identity on their behalf.
201
+ console.log("Starring is not available from the CLI: it uses your GitHub identity, so only you can do it from the dashboard.");
202
+ }
203
+
204
+ export async function handleInspectCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
205
+ return runCliAction(async () => {
206
+ const hasSub = argv[0] !== undefined && !argv[0].startsWith("-");
207
+ const sub = hasSub ? argv[0]! : "config";
208
+ const rest = hasSub ? argv.slice(1) : argv;
209
+ if (sub === "config") await read("/api/config", rest, deps);
210
+ else if (sub === "catalog") await read("/api/catalog", rest, deps);
211
+ else if (sub === "routing-analytics") await read("/api/routing-analytics", rest, deps);
212
+ else if (sub === "key-providers") await read("/api/key-providers", rest, deps);
213
+ else if (sub === "windows-tray") await read("/api/windows-tray", rest, deps);
214
+ else if (sub === "pacing") await pacing(rest, deps);
215
+ else if (sub === "client-config") await clientConfig(rest, deps);
216
+ else if (sub === "codex-prompt") await codexPrompt(rest, deps);
217
+ else if (sub === "star") await star(rest, deps);
218
+ else throw new CliUsageError(`unknown inspect command ${sub}`, USAGE);
219
+ });
220
+ }
221
+
222
+ export async function handleIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
223
+ return runCliAction(async () => {
224
+ const [sub, ...rest] = argv;
225
+ if (sub === "native" || sub === undefined) await nativeIntegration(sub === undefined ? [] : rest, deps);
226
+ else throw new CliUsageError(`unknown integration command ${sub}`, USAGE);
227
+ });
228
+ }
229
+
230
+ export const INSPECT_USAGE = USAGE;