@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/cli/doctor.ts CHANGED
@@ -13,10 +13,15 @@ import { dirname, join } from "node:path";
13
13
  import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
14
14
  import { findLiveProxy } from "../server/proxy-liveness";
15
15
  import { gracefulStopHost } from "../lib/process-control";
16
+ import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
17
+ import type { BunRuntimeSource } from "../lib/bun-runtime";
16
18
  import { maskAccountId } from "../lib/privacy";
17
19
  import { PROXY_ENV_KEYS, proxyEnvPresent } from "../lib/proxy-env";
18
20
  import { configuredAdminToken } from "../lib/admin-secrets";
19
21
  import { readCodexTokens } from "../codex/auth-collision";
22
+ import { withNativeMainSharedClaim } from "../codex/native-main-claim";
23
+ import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "../codex/native-profile-store";
24
+ import { NativeProfileError } from "../codex/native-profile-types";
20
25
  import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
21
26
  import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
22
27
  import { countPendingOpencodexHistory } from "../codex/history-provider";
@@ -479,36 +484,75 @@ export type WhamProbeResult = {
479
484
  authenticated: boolean;
480
485
  };
481
486
 
487
+ type NativeMainDoctorClaim = <T>(operation: () => Promise<T>) => Promise<T>;
488
+
489
+ export interface WhamProbeDeps {
490
+ withNativeMainClaim?: NativeMainDoctorClaim;
491
+ probeNativeMainRecoveryState?: typeof probeNativeProfileRecoveryState;
492
+ }
493
+
482
494
  /**
483
495
  * Replicate the runtime WHAM fetch shape (same URL, 8s timeout, main-token
484
496
  * headers when present) so the probe fails exactly where the real path fails.
485
497
  * `fetchImpl` is injectable for testing.
486
498
  */
487
- export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamProbeResult> {
488
- const tokens = readCodexTokens();
489
- const headers: Record<string, string> = {};
490
- if (tokens) {
491
- headers.Authorization = `Bearer ${tokens.access_token}`;
492
- headers["ChatGPT-Account-Id"] = tokens.account_id;
493
- }
499
+ export async function probeWham(
500
+ fetchImpl: typeof fetch = fetch,
501
+ deps: WhamProbeDeps = {},
502
+ ): Promise<WhamProbeResult> {
494
503
  const start = performance.now();
504
+ let authenticated = false;
495
505
  try {
496
- const resp = await fetchImpl(WHAM_USAGE_URL, { headers, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
497
- const durationMs = Math.round(performance.now() - start);
498
- return {
499
- ok: resp.ok,
500
- status: resp.status,
501
- durationMs,
502
- classification: resp.ok ? "ok" : `http_${resp.status}`,
503
- authenticated: !!tokens,
504
- };
506
+ const context = resolveNativeProfileContext();
507
+ const withClaim = deps.withNativeMainClaim
508
+ ?? (<T>(operation: () => Promise<T>) => withNativeMainSharedClaim(context, operation));
509
+ return await withClaim(async () => {
510
+ const recoveryState = (deps.probeNativeMainRecoveryState ?? probeNativeProfileRecoveryState)(context);
511
+ if (recoveryState !== "none") {
512
+ return {
513
+ ok: false,
514
+ status: null,
515
+ durationMs: Math.round(performance.now() - start),
516
+ classification: `native_main_recovery_${recoveryState}`,
517
+ authenticated: false,
518
+ };
519
+ }
520
+ const tokens = readCodexTokens();
521
+ const headers: Record<string, string> = {};
522
+ if (tokens) {
523
+ headers.Authorization = `Bearer ${tokens.access_token}`;
524
+ headers["ChatGPT-Account-Id"] = tokens.account_id;
525
+ }
526
+ authenticated = !!tokens;
527
+ const resp = await fetchImpl(WHAM_USAGE_URL, { headers, signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
528
+ const durationMs = Math.round(performance.now() - start);
529
+ return {
530
+ ok: resp.ok,
531
+ status: resp.status,
532
+ durationMs,
533
+ classification: resp.ok ? "ok" : `http_${resp.status}`,
534
+ authenticated,
535
+ };
536
+ });
505
537
  } catch (err) {
506
538
  const durationMs = Math.round(performance.now() - start);
539
+ if (
540
+ err instanceof NativeProfileError
541
+ && (err.code === "NATIVE_MAIN_CLAIM_BUSY" || err.code === "NATIVE_MAIN_CLAIM_UNAVAILABLE")
542
+ ) {
543
+ return {
544
+ ok: false,
545
+ status: null,
546
+ durationMs,
547
+ classification: err.code.toLowerCase(),
548
+ authenticated: false,
549
+ };
550
+ }
507
551
  const name = err instanceof Error ? err.name : String(err);
508
552
  const classification = name === "TimeoutError" || name === "AbortError"
509
553
  ? "timeout"
510
554
  : "connect_error";
511
- return { ok: false, status: null, durationMs, classification, authenticated: !!tokens };
555
+ return { ok: false, status: null, durationMs, classification, authenticated };
512
556
  }
513
557
  }
514
558
 
@@ -523,6 +567,8 @@ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamPr
523
567
  export type ServiceMemoryData = {
524
568
  pid: number;
525
569
  bunVersion: string;
570
+ /** Launch-time provenance; absent for services installed before the marker existed. */
571
+ bunRuntimeSource?: BunRuntimeSource;
526
572
  platform: string;
527
573
  rss: number;
528
574
  heapUsed: number;
@@ -579,6 +625,9 @@ export async function fetchServiceMemory(
579
625
  data: {
580
626
  pid: body.pid,
581
627
  bunVersion: body.bunVersion,
628
+ // Allowlisted independently of the server: an unrecognized wire value is
629
+ // treated as absent rather than echoed into user-facing guidance.
630
+ bunRuntimeSource: BUN_RUNTIME_SOURCES.find(source => source === body.bunRuntimeSource),
582
631
  platform: typeof body.platform === "string" ? body.platform : "unknown",
583
632
  rss: body.rss,
584
633
  heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
@@ -652,12 +701,22 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
652
701
  } else {
653
702
  lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
654
703
  }
655
- // Version-claiming (never binary-claiming): the endpoint cannot distinguish
656
- // the bundled binary from an OPENCODEX_BUN_PATH override of the same version.
657
704
  if (d.platform === "win32" && d.eagerRelay?.reason === "auto-known-bad") {
658
705
  lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
659
- lines.push(" Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),");
660
- lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
706
+ // The remediation depends on how the SERVICE was launched, which only the
707
+ // launch-time marker can answer. Telling someone to set OPENCODEX_BUN_PATH
708
+ // when it is already set is the bug this branch exists to avoid (#848).
709
+ if (d.bunRuntimeSource === "override") {
710
+ lines.push(` OPENCODEX_BUN_PATH is already active for this service — the override runtime is itself an affected version (unvalidated — own risk).`);
711
+ lines.push(" Options: point the override at a different runtime, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
712
+ } else if (d.bunRuntimeSource === undefined) {
713
+ lines.push(" this service records no runtime origin (installed before provenance tracking), so OpenCodex cannot tell whether an override is already active.");
714
+ lines.push(" Reinstall the service to record it, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
715
+ } else {
716
+ const origin = d.bunRuntimeSource === "process" ? "the runtime that launched it" : "the bundled runtime";
717
+ lines.push(` the service is using ${origin}. Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),`);
718
+ lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
719
+ }
661
720
  }
662
721
  return lines;
663
722
  }
@@ -671,11 +730,21 @@ export function proxyDownRestartHint(input: {
671
730
  proxyRunning: boolean;
672
731
  port: number;
673
732
  serviceViable: boolean;
733
+ /** Absent means "unknown"; the hint then keeps its pre-repair wording. */
734
+ serviceInstalled?: boolean;
735
+ serviceConflict?: boolean;
674
736
  }): string | null {
675
737
  if (input.proxyRunning) return null;
738
+ // `serviceViable` alone conflates "no service at all" with "registered but stale or
739
+ // stopped". Only the first wants `install`: re-registering an existing service costs a
740
+ // UAC prompt on Windows and can switch a WinSW backend to Task Scheduler. A conflict
741
+ // still needs uninstall-then-install, which repairService() refuses outright.
742
+ const installedButBroken = input.serviceInstalled === true && input.serviceConflict !== true;
676
743
  const restart = input.serviceViable
677
744
  ? "Restart it with 'ocx service start' (service installed) or 'ocx start'."
678
- : "Restart it with 'ocx start', or install the persistent service: 'ocx service install'.";
745
+ : installedButBroken
746
+ ? "Restart it with 'ocx start', or refresh the installed service: 'ocx service repair'."
747
+ : "Restart it with 'ocx start', or install the persistent service: 'ocx service install'.";
679
748
  return `The ocx proxy is not running. Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
680
749
  }
681
750
 
@@ -891,6 +960,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
891
960
  proxyRunning: Boolean(live),
892
961
  port: live?.port ?? doctorConfig.port ?? 10100,
893
962
  serviceViable: startup.serviceViable,
963
+ serviceInstalled: startup.serviceInstalled,
964
+ serviceConflict: startup.serviceConflict,
894
965
  });
895
966
  if (proxyDown) hints.push(proxyDown);
896
967
  for (const row of providerApiKeys) {
@@ -1,13 +1,19 @@
1
1
  /**
2
- * `ocx export --client <opencode|pi>` — print a client config for the live proxy.
2
+ * `ocx export --client <id>` — print a client config for the live proxy.
3
+ *
4
+ * Six clients, four formats: opencode and Pi are JSON, Hermes and Gajae YAML,
5
+ * OpenClaw JSON5, Kimi TOML.
3
6
  *
4
7
  * Two consumers, one payload (devlog 260731_client_config_export/020):
5
8
  *
6
- * - **Agent** (`--json`): stdout is exactly the client config JSON and nothing else, so
7
- * `ocx export --client pi --json > models.json` is safe to pipe. Every diagnostic
8
- * including the `--out` write note goes to stderr.
9
- * - **Human** (no flag): the JSON leads, then the destination path, the merge warning,
10
- * the env export line, and the model/degraded counts.
9
+ * - **Agent** (`--json`): stdout is exactly the client config as JSON and nothing else,
10
+ * so `ocx export --client pi --json > models.json` is safe to pipe. This is JSON for
11
+ * every client, including the YAML/JSON5/TOML onesthe flag is about machine
12
+ * readability, not the client's native format. Every diagnostic including the
13
+ * `--out` write note goes to stderr.
14
+ * - **Human** (no flag) and `--out`: the client's NATIVE serialization leads, then the
15
+ * destination path, the merge warning, the env export line, and the model/degraded
16
+ * counts.
11
17
  *
12
18
  * The command never writes the user's real config path. `--out` is an explicit target and
13
19
  * refuses to clobber an existing file without `--force`, because the common mistake
@@ -15,14 +21,15 @@
15
21
  *
16
22
  * Serialization itself belongs to src/clients/config-export.ts; this module only resolves
17
23
  * the base URL, filters the catalog, and renders. No secret is ever serialized: the config
18
- * carries the client's documented env reference and the real key stays in the environment.
24
+ * carries the client's documented env reference or, for Kimi, which cannot hold one, a
25
+ * loopback placeholder — and the real key stays in the environment.
19
26
  */
20
27
  import { writeFileSync } from "node:fs";
21
28
  import { loadConfig } from "../config";
22
29
  import {
23
30
  EXPORT_CLIENTS,
24
31
  EXPORT_CLIENT_IDS,
25
- buildClientConfig,
32
+ buildClientConfigText,
26
33
  isExportClientId,
27
34
  opencodeProxyBaseUrl,
28
35
  type ExportClientId,
@@ -164,16 +171,23 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
164
171
  throw new RuntimeApiError("Management API returned an unexpected /api/models payload.", 502, rows);
165
172
  }
166
173
  const models = exportModelsFromProxyRows(rows, config);
167
- const clientConfig = buildClientConfig(client, { baseUrl: proxyV1BaseUrl(root), models, config });
168
- const text = JSON.stringify(clientConfig, null, 2);
174
+ // The text is the client's OWN format YAML, TOML and JSON5 clients would
175
+ // otherwise receive a JSON rendering their parser reads differently.
176
+ const built = buildClientConfigText(client, { baseUrl: proxyV1BaseUrl(root), models, config });
177
+ const clientConfig = built.document;
178
+ const text = built.text;
169
179
 
170
- if (out !== undefined) writeExport(out, `${text}\n`, force);
180
+ // Every serializer already ends with exactly one newline.
181
+ if (out !== undefined) writeExport(out, text, force);
171
182
  // stderr, so `--json` stdout stays byte-exact for a redirect.
172
183
  if (out !== undefined && wantsJson) console.error(`Wrote ${out}`);
173
184
 
174
185
  const degraded = models.filter(model => !hasContextLimit(model)).length;
186
+ // `--json` keeps emitting the DOCUMENT at the top level: a script that
187
+ // pipes it into a config file must not have to unwrap an envelope we added
188
+ // for our own convenience. Format metadata rides in the human lines below.
175
189
  printData(clientConfig, wantsJson, [
176
- text,
190
+ text.trimEnd(),
177
191
  "",
178
192
  ...(out !== undefined ? [`Wrote ${out}`] : []),
179
193
  `Destination: ${spec.destination(process.env)}`,
package/src/cli/help.ts CHANGED
@@ -110,7 +110,7 @@ const helpEntries: Record<string, HelpEntry> = {
110
110
  ],
111
111
  },
112
112
  account: {
113
- usage: "ocx account <list|current|use|refresh|auto-switch|login|reauth|code|cancel|remove|add-key|reset-credits> ...",
113
+ usage: "ocx account <list|current|use|refresh|auto-switch|login|reauth|code|cancel|remove|add-key|reset-credits|main> ...",
114
114
  summary: "List and switch provider accounts and API-key pools (GUI parity).",
115
115
  details: [
116
116
  "list [provider] Codex account pool, OAuth accounts and API keys (identifiers shown masked as the API returns them).",
@@ -122,6 +122,7 @@ const helpEntries: Record<string, HelpEntry> = {
122
122
  "add-key <provider> [--label <label>] Add a key read only from piped stdin.",
123
123
  "login/reauth/code/cancel Run browser or manual-code auth from a headless shell.",
124
124
  "reset-credits <id|main> [--consume --yes] Inspect or consume Codex reset credits.",
125
+ "main <subcommand> Manage the physical native Codex login separately from Pool routing.",
125
126
  "Codex pool selection applies to the next request after clearing existing affinity; in-flight requests keep their captured account.",
126
127
  ],
127
128
  },
@@ -170,17 +171,17 @@ const helpEntries: Record<string, HelpEntry> = {
170
171
  },
171
172
  "api-key": { usage: "ocx api-key <list|create|remove> ...", summary: "Alias of ocx access key." },
172
173
  export: {
173
- usage: "ocx export --client <opencode|pi> [--json] [--out <path>] [--force]",
174
- summary: "Print a client config (opencode, Pi) wired to the running proxy.",
174
+ usage: "ocx export --client <opencode|pi|hermes|openclaw|kimi|gajae> [--json] [--out <path>] [--force]",
175
+ summary: "Print a client config (opencode, Pi, Hermes, OpenClaw, Kimi Code, Gajae Code) wired to the running proxy.",
175
176
  details: [
176
177
  "--json prints only the config JSON on stdout, so it is safe to redirect to a file.",
177
178
  "--out <path> writes the config there and refuses to replace an existing file without --force.",
178
- "The config never contains a key; it references the client's env var, which you export before launching.",
179
+ "The config never contains a key; it references the client's env var, which you export before launching. Kimi cannot hold an env reference, so it carries a loopback placeholder instead.",
179
180
  "The destination path is printed for merging by hand — ocx never writes your real client config.",
180
181
  ],
181
182
  },
182
183
  grok: { usage: "ocx grok <status|exclude|include|set|clear|apply> ...", summary: "Manage and apply the Grok Build model fence." },
183
- integration: { usage: "ocx integration <claude|grok> ...", summary: "Manage supported client integrations." },
184
+ integration: { usage: "ocx integration <claude|grok|client> ...", summary: "Manage supported client integrations." },
184
185
  system: {
185
186
  usage: "ocx system <status|settings|startup|diagnostics|sync|update> ...",
186
187
  summary: "Manage headless runtime settings, startup, sync, diagnostics, and updates.",
@@ -296,7 +297,8 @@ Usage:
296
297
  ocx agent <sub> Subagents, injection, effort caps, and sidecars
297
298
  ocx observe <sub> Logs, usage, storage, memory, and debug data
298
299
  ocx access <sub> External API keys and endpoint information
299
- ocx export --client <id> Print an opencode/Pi config wired to the running proxy
300
+ ocx export --client <id> Print a client config wired to the running proxy (6 clients)
301
+ ocx integration client <sub> Enable, disable, inspect or roll back a client integration
300
302
  ocx grok <sub> Grok Build model selection and apply
301
303
  ocx system <sub> Runtime settings, startup, sync, and updates
302
304
  ocx config <sub> Validated configuration show/get/set/import/export
package/src/cli/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { spawn } from "node:child_process";
3
- import { currentExternalCodexModelProvider, restoreNativeCodex, shouldInjectApiAuthHeader } from "../codex/inject";
3
+ import { currentExternalCodexModelProvider, restoreNativeCodex, restoreNativeCodexAsync, shouldInjectApiAuthHeader } from "../codex/inject";
4
4
  import { stripGrokConfig } from "../grok/inject";
5
- import { restoreLegacyOpenaiHistory } from "../codex/history-provider";
5
+ import { resolveCodexHistoryJobTarget, runCodexHistoryJob } from "../codex/history-job";
6
6
  import { reconcileJournal } from "../codex/journal";
7
7
  import {
8
8
  codexAutoStartEnabled,
@@ -41,9 +41,11 @@ import { maybeShowStarPrompt } from "./star-prompt";
41
41
  import { scheduleCatalogPrewarm } from "./catalog-prewarm";
42
42
  import { maybeShowUpdatePrompt } from "../update/notify";
43
43
  import { syncModelsToCodex } from "../codex/sync";
44
+ import { shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
44
45
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
45
46
  import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
46
47
  import { removeOwnedConfigState } from "../lib/config-ownership";
48
+ import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
47
49
 
48
50
  const args = process.argv.slice(2);
49
51
  const command = args[0];
@@ -315,7 +317,7 @@ async function handleStart(options: { block?: boolean } = {}) {
315
317
  installShellHook();
316
318
 
317
319
  await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
318
- await syncModelsToCodex(port).catch(() => {});
320
+ await syncCodexOnStartIfEnabled(port, config);
319
321
  if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
320
322
  historyGuardian = startHistoryMigrationGuardian();
321
323
  }
@@ -335,7 +337,10 @@ async function handleStart(options: { block?: boolean } = {}) {
335
337
  // absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
336
338
  // Deliberately a SIBLING of the Desktop-3P block above: nesting it there meant a catalog
337
339
  // failure skipped the fence entirely, even though syncGrokConfig handles that case itself.
338
- try {
340
+ //
341
+ // Gated on the persisted switch: without this, turning Grok off lasted exactly
342
+ // one restart, because the toggle removed the fence and start wrote it back.
343
+ if (shouldSyncGrokOnStart(config)) try {
339
344
  const { syncGrokConfig } = await import("../grok/sync");
340
345
  const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
341
346
  if (r.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)");
@@ -385,7 +390,7 @@ async function handleEnsure() {
385
390
  detached: true,
386
391
  stdio: "ignore",
387
392
  windowsHide: true,
388
- env: { ...process.env, OCX_SERVICE: "1" },
393
+ env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
389
394
  });
390
395
  child.unref();
391
396
 
@@ -427,7 +432,7 @@ async function handleTrayProxyStart(): Promise<void> {
427
432
  detached: true,
428
433
  stdio: "ignore",
429
434
  windowsHide: true,
430
- env: { ...process.env, OCX_SERVICE: "1" },
435
+ env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
431
436
  });
432
437
  child.unref();
433
438
  },
@@ -524,7 +529,7 @@ async function handleStop() {
524
529
  }
525
530
  }
526
531
  if (!ownershipBlocked) {
527
- const r = restoreNativeCodex();
532
+ const r = await restoreNativeCodexAsync();
528
533
  if (r.success) console.log(`↩️ ${r.message}`);
529
534
  else {
530
535
  stopFailed = true;
@@ -586,8 +591,8 @@ async function handleUninstall() {
586
591
  });
587
592
  }
588
593
 
589
- await runStep("native Codex restored", () => {
590
- const r = restoreNativeCodex();
594
+ await runStep("native Codex restored", async () => {
595
+ const r = await restoreNativeCodexAsync();
591
596
  if (!r.success) throw new Error(r.message);
592
597
  });
593
598
 
@@ -659,7 +664,14 @@ async function handleStatus() {
659
664
  console.log(` Health: ${status.healthLabel}`);
660
665
  if (!(status.json.proxy.pid || status.json.proxy.health.ok)) {
661
666
  console.log(" ↳ Not running — Codex/Claude requests will fail with connection errors.");
662
- console.log(" Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
667
+ // The service summary a few lines below already tells a registered-but-not-serving
668
+ // user to repair. Printing "install the persistent service" unconditionally
669
+ // contradicted it in the same report, and install re-registers: UAC on Windows and a
670
+ // possible WinSW-to-scheduler switch for someone who already has a service.
671
+ const installed = status.json.startup.serviceInstalled && !status.json.startup.serviceConflict;
672
+ console.log(installed
673
+ ? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
674
+ : " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
663
675
  }
664
676
  console.log(` Dashboard: ${status.json.dashboard.url}`);
665
677
  console.log(` Config: ${status.json.paths.config}`);
@@ -707,13 +719,22 @@ async function handleStatus() {
707
719
  }
708
720
  }
709
721
 
710
- function handleRecoverHistory() {
722
+ async function handleRecoverHistory() {
711
723
  if (args[1] !== "--legacy-openai") {
712
724
  console.error("Usage: ocx recover-history --legacy-openai");
713
725
  console.error("Only use this if an older syncResumeHistory build already remapped OpenAI Codex App history to opencodex before backup support existed.");
714
726
  process.exit(1);
715
727
  }
716
- const r = restoreLegacyOpenaiHistory();
728
+ // Manifest-independent legacy ejection, serialized like every other history
729
+ // mutation. It is a separate operation from generic restore precisely because
730
+ // it must not read, consume or replace the backup manifest.
731
+ const outcome = await runCodexHistoryJob({
732
+ ...resolveCodexHistoryJobTarget(),
733
+ operation: "recover-legacy-openai",
734
+ });
735
+ const r = outcome.kind === "converged"
736
+ ? { rows: outcome.rows, files: outcome.files, failed: undefined }
737
+ : { rows: 0, files: 0, failed: true as const };
717
738
  if (r.failed) {
718
739
  console.error(
719
740
  "⚠️ Recovery SKIPPED: the Codex history DB is locked (Codex app/IDE open?). Close it and rerun this command.",
@@ -764,7 +785,7 @@ switch (command) {
764
785
  }
765
786
  let r: { success: boolean; message: string };
766
787
  try {
767
- r = restoreNativeCodex();
788
+ r = await restoreNativeCodexAsync();
768
789
  } catch (err) {
769
790
  r = { success: false, message: err instanceof Error ? err.message : String(err) };
770
791
  }
@@ -789,7 +810,7 @@ switch (command) {
789
810
  break;
790
811
  }
791
812
  case "recover-history":
792
- handleRecoverHistory();
813
+ await handleRecoverHistory();
793
814
  break;
794
815
  case "uninstall":
795
816
  case "remove":
@@ -847,9 +868,14 @@ switch (command) {
847
868
  }
848
869
  case "sync-cache": {
849
870
  const restartCodex = args.slice(1).includes("--restart-codex");
850
- const { invalidateCodexModelsCache } = await import("../codex/catalog");
871
+ const { withCatalogWriteSerialization } = await import("../codex/catalog-write-serialization");
872
+ const { invalidateCodexModelsCacheWithPermit } = await import("../codex/catalog/sync");
873
+ const { getCodexHome } = await import("../codex/paths");
874
+ const owningCodexHome = getCodexHome();
875
+ const invalidated = withCatalogWriteSerialization(owningCodexHome, permit =>
876
+ invalidateCodexModelsCacheWithPermit(permit, owningCodexHome));
851
877
  // Only warn/restart when models_cache was actually rewritten from a readable catalog.
852
- if (invalidateCodexModelsCache()) {
878
+ if (invalidated.kind === "completed" && invalidated.value) {
853
879
  const { afterCatalogWriteHandleAppServers } = await import("../codex/app-server-processes");
854
880
  afterCatalogWriteHandleAppServers({ restart: restartCodex, log: console });
855
881
  }
@@ -867,7 +893,7 @@ switch (command) {
867
893
  detached: true,
868
894
  stdio: "ignore",
869
895
  windowsHide: true,
870
- env: process.env,
896
+ env: withProcessRuntimeProvenance(process.env),
871
897
  });
872
898
  child.unref();
873
899
  live = await waitForProxy();
@@ -999,13 +1025,18 @@ switch (command) {
999
1025
  break;
1000
1026
  }
1001
1027
  case "route": {
1002
- if (args[1] !== "combo") {
1003
- console.error("Usage: ocx route combo <subcommand>");
1028
+ if (args[1] !== "combo" && args[1] !== "policy") {
1029
+ console.error("Usage: ocx route <combo|policy> <subcommand>");
1004
1030
  process.exitCode = 2;
1005
1031
  break;
1006
1032
  }
1007
- const { handleComboCommand } = await import("./combo");
1008
- process.exitCode = await handleComboCommand(args.slice(2));
1033
+ if (args[1] === "combo") {
1034
+ const { handleComboCommand } = await import("./combo");
1035
+ process.exitCode = await handleComboCommand(args.slice(2));
1036
+ } else {
1037
+ const { handleRoutePolicyCommand } = await import("./route-policy");
1038
+ process.exitCode = await handleRoutePolicyCommand(args.slice(2));
1039
+ }
1009
1040
  break;
1010
1041
  }
1011
1042
  case "agent": {
@@ -1054,8 +1085,11 @@ switch (command) {
1054
1085
  } else if (integration === "claude") {
1055
1086
  const { handleClaudeConfigCommand } = await import("./integrations");
1056
1087
  process.exitCode = await handleClaudeConfigCommand(args.slice(2));
1088
+ } else if (integration === "client") {
1089
+ const { handleClientIntegrationCommand } = await import("./integrations");
1090
+ process.exitCode = await handleClientIntegrationCommand(args.slice(2));
1057
1091
  } else {
1058
- console.error("Usage: ocx integration <claude|grok> <subcommand>");
1092
+ console.error("Usage: ocx integration <claude|grok|client> <subcommand>");
1059
1093
  process.exitCode = 2;
1060
1094
  }
1061
1095
  break;
@@ -27,6 +27,12 @@ const GROK_USAGE = `Usage:
27
27
  ocx grok clear [--json]
28
28
  ocx grok apply [--json]`;
29
29
 
30
+ const CLIENT_USAGE = `Usage:
31
+ ocx integration client [status] [--client <id>] [--json]
32
+ ocx integration client <enable|disable> --client <id> [--json]
33
+ ocx integration client history [--client <id>] [--json]
34
+ ocx integration client restore --op <opId> [--confirm-drift] [--json]`;
35
+
30
36
  function parseMap(raw: string): Record<string, string> {
31
37
  if (raw === "-") return {};
32
38
  const map: Record<string, string> = {};
@@ -139,4 +145,81 @@ export async function handleGrokCommand(argv: string[], deps: RuntimeApiDeps = {
139
145
  });
140
146
  }
141
147
 
142
- export const INTEGRATION_USAGE = { claude: CLAUDE_USAGE, grok: GROK_USAGE };
148
+ /**
149
+ * The headless half of the client-integration toggle.
150
+ *
151
+ * Every safety property lives behind the management API — ownership, the
152
+ * pre-write snapshot, the journal, the drift refusal — so this command is a
153
+ * thin caller and deliberately re-implements none of it. That is also why
154
+ * `restore` surfaces the drift refusal as an error telling the user to pass
155
+ * `--confirm-drift` rather than retrying on their behalf: replacing edits a
156
+ * user made after the snapshot is exactly the decision they have to make.
157
+ */
158
+ export async function handleClientIntegrationCommand(
159
+ argv: string[],
160
+ deps: RuntimeApiDeps = {},
161
+ ): Promise<number> {
162
+ return runCliAction(async () => {
163
+ const args = [...argv];
164
+ const action = (args.shift() ?? "status").toLowerCase();
165
+ const wantsJson = takeFlag(args, "--json");
166
+
167
+ if (action === "status" || action === "show" || action === "list") {
168
+ const client = takeOption(args, "--client");
169
+ rejectArgs(args, CLIENT_USAGE);
170
+ const path = client
171
+ ? `/api/client-integrations/${encodeURIComponent(client)}`
172
+ : "/api/client-integrations";
173
+ const result = await runtimeRequest(path, {}, deps);
174
+ const rows = (result as { clients?: Array<Record<string, unknown>> }).clients;
175
+ printData(result, wantsJson, rows
176
+ ? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
177
+ : summaryLines(result));
178
+ return;
179
+ }
180
+
181
+ if (action === "history" || action === "journal") {
182
+ const client = takeOption(args, "--client");
183
+ rejectArgs(args, CLIENT_USAGE);
184
+ const query = client ? `?client=${encodeURIComponent(client)}` : "";
185
+ const result = await runtimeRequest(`/api/client-integrations/journal${query}`, {}, deps);
186
+ const operations = (result as { operations?: Array<Record<string, unknown>> }).operations ?? [];
187
+ printData(result, wantsJson, operations.length === 0
188
+ ? ["No integration operations recorded yet."]
189
+ : operations.map(row => {
190
+ // `snapshot` is resolved against the disk by the route, so "expired"
191
+ // here means the bytes are genuinely gone, not merely old.
192
+ const backup = row.snapshot === "expired" ? "backup expired" : `op ${String(row.opId)}`;
193
+ return `${String(row.at)} ${String(row.clientId)} ${String(row.kind)} (${backup})`;
194
+ }));
195
+ return;
196
+ }
197
+
198
+ if (action === "restore") {
199
+ const opId = takeOption(args, "--op") ?? takeOption(args, "--op-id");
200
+ const confirmDrift = takeFlag(args, "--confirm-drift");
201
+ rejectArgs(args, CLIENT_USAGE);
202
+ if (!opId) throw new CliUsageError("--op <opId> is required", CLIENT_USAGE);
203
+ const result = await runtimeRequest("/api/client-integrations/restore", {
204
+ method: "POST",
205
+ body: JSON.stringify({ opId, confirmDrift }),
206
+ }, deps);
207
+ printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? "Restored.")]);
208
+ return;
209
+ }
210
+
211
+ if (action !== "enable" && action !== "disable") {
212
+ throw new CliUsageError(`unknown client integration command ${action}`, CLIENT_USAGE);
213
+ }
214
+ const client = takeOption(args, "--client");
215
+ rejectArgs(args, CLIENT_USAGE);
216
+ if (!client) throw new CliUsageError("--client <id> is required", CLIENT_USAGE);
217
+ const result = await runtimeRequest(`/api/client-integrations/${encodeURIComponent(client)}`, {
218
+ method: "PUT",
219
+ body: JSON.stringify({ enabled: action === "enable" }),
220
+ }, deps);
221
+ printData(result, wantsJson, [String((result as Record<string, unknown>).message ?? `${client} ${action}d.`)]);
222
+ });
223
+ }
224
+
225
+ export const INTEGRATION_USAGE = { claude: CLAUDE_USAGE, grok: GROK_USAGE, client: CLIENT_USAGE };