@bitkyc08/opencodex 2.77.0 → 2.78.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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
package/src/cli/index.ts CHANGED
@@ -8,6 +8,9 @@ import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
10
10
  import { packageVersion } from "../lib/package-version";
11
+ import { admitUpdateRestartChild } from "./update-restart-child";
12
+ import { UpdateRestartRequired } from "./update-restart-candidate";
13
+ import { describeUpdateRestartFailure, restartFromCurrentInstallation } from "./update-restart";
11
14
  import { findGuiDist } from "../server/gui-static";
12
15
  import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
13
16
 
@@ -85,9 +88,9 @@ import {
85
88
  runProxyRestart,
86
89
  runTrayProxyStart,
87
90
  type ProxyRestartLive,
88
- type ProxyRestartResult,
89
91
  type ProxyRestartStartOutcome,
90
92
  } from "./tray-proxy";
93
+ import { reportRestartFailure } from "./restart-failure";
91
94
  import { requestBoundSystemRestart } from "./system-restart-client";
92
95
  import { installCrashGuards } from "../lib/crash-guard";
93
96
  import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
@@ -142,6 +145,7 @@ import { honorSiblingMarker, markSiblingStart, siblingOfLivePort, siblingRuntime
142
145
  import { consumeSiblingHandoff } from "../codex/sibling-handoff";
143
146
  import {
144
147
  reconcileClientStartupBeforeReady,
148
+ syncCodexBeforeCatalogObservation,
145
149
  syncClaudeAgentDefsAtProxyStartup,
146
150
  } from "./claude-agent-startup-sync";
147
151
  import {
@@ -200,6 +204,7 @@ async function refreshOwnedRaycastCatalog(
200
204
  }
201
205
  }
202
206
 
207
+ const updateRestartChild = admitUpdateRestartChild(process.argv.slice(2));
203
208
  initializeNodeLauncherContext();
204
209
  // The compiled executable is also the capture-only MCP server's launcher.
205
210
  // Handle this private entrypoint before CLI preflight or command dispatch.
@@ -371,6 +376,7 @@ async function findProxyOwnerBeforeJournalRecovery(
371
376
  }
372
377
 
373
378
  async function handleStart(options: { block?: boolean } = {}) {
379
+ updateRestartChild?.check();
374
380
  // A supervised service child defers to a foreign recorded owner before doing
375
381
  // anything else. 'ocx service start' refuses this activation path already, but
376
382
  // the process managers below it — the Windows boot wrapper's restart loop and
@@ -466,6 +472,7 @@ async function handleStart(options: { block?: boolean } = {}) {
466
472
  },
467
473
  });
468
474
 
475
+ updateRestartChild?.check();
469
476
  const clientState = readClientConnectionState();
470
477
  if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
471
478
  throw new Error(`client startup refused: ${clientState.reason}`);
@@ -550,7 +557,17 @@ async function handleStart(options: { block?: boolean } = {}) {
550
557
  let server: ReturnType<typeof serverModule.startServer>;
551
558
  for (let attempt = 0; ; attempt++) {
552
559
  try {
560
+ if (updateRestartChild && siblingStart) throw new Error("update_restart_sibling_refused");
561
+ updateRestartChild?.check(port, loadConfig().hostname ?? "");
553
562
  server = serverModule.startServer(port, { localAttestationSecret, readinessGate });
563
+ if (updateRestartChild) {
564
+ try { updateRestartChild.check(port, server.hostname); }
565
+ catch (error) {
566
+ try { await server.stop(true); }
567
+ catch (rollback) { throw new StartOwnershipRollbackUncertainError([error, rollback]); }
568
+ throw error;
569
+ }
570
+ }
554
571
  break;
555
572
  } catch (err) {
556
573
  try { await serverModule.waitForFailedStartRollback(err); }
@@ -578,14 +595,14 @@ async function handleStart(options: { block?: boolean } = {}) {
578
595
  }
579
596
  return { server, serverModule, port, readinessGate, localAttestationSecret, config };
580
597
  },
581
- writePid: () => writePid(process.pid),
582
- writeRuntime: bound => writeRuntimePort({
598
+ writePid: () => { updateRestartChild?.check(); writePid(process.pid); },
599
+ writeRuntime: bound => { updateRestartChild?.check(bound.port, bound.config.hostname ?? ""); writeRuntimePort({
583
600
  pid: process.pid,
584
601
  port: bound.port,
585
602
  hostname: bound.config.hostname,
586
603
  attestationSecret: bound.localAttestationSecret,
587
604
  ...siblingRuntimeField(),
588
- }),
605
+ }); updateRestartChild?.complete(); },
589
606
  stopBound: bound => bound.server.stop(true),
590
607
  removeRuntime: () => removeRuntimePortIfPidIs(process.pid),
591
608
  removePid: () => removePidIfValueIs(process.pid),
@@ -615,12 +632,14 @@ async function handleStart(options: { block?: boolean } = {}) {
615
632
  // Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
616
633
  let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
617
634
  let routingHealer: { stop(): void } | undefined; // routing-healer.ts; stopped first in syncCleanup
635
+ let catalogHealer: { stop(): void } | undefined;
618
636
 
619
637
  let cleaned = false;
620
638
  let cleanupSucceeded = true;
621
639
  const syncCleanup = () => {
622
640
  if (cleaned) return cleanupSucceeded;
623
641
  cleaned = true;
642
+ try { catalogHealer?.stop(); } catch { /* best-effort */ }
624
643
  try { routingHealer?.stop(); } catch { /* best-effort */ }
625
644
  try { guardian.stop(); } catch { /* best-effort */ }
626
645
  try { historyGuardian?.stop(); } catch { /* best-effort */ }
@@ -703,9 +722,14 @@ async function handleStart(options: { block?: boolean } = {}) {
703
722
  // deferred until the best-effort Claude roster and Desktop registry settle. This
704
723
  // keeps /readyz closed across startup initialization without making an optional
705
724
  // Claude integration failure prevent the proxy from starting.
725
+ const catalogHealerModule = siblingStart ? null : await import("../codex/catalog-self-heal");
706
726
  const startupSync = await reconcileClientStartupBeforeReady(
707
727
  readinessGate,
708
- gate => syncCodexOnStartIfEnabled(port, config, undefined, gate),
728
+ gate => syncCodexBeforeCatalogObservation(gate,
729
+ forwarding => syncCodexOnStartIfEnabled(port, config, undefined, forwarding),
730
+ () => {
731
+ if (catalogHealerModule && !siblingStart && !cleaned) catalogHealer = catalogHealerModule.startCodexCatalogSelfHeal({ port });
732
+ }),
709
733
  () => systemEnv.injected
710
734
  ? Promise.resolve(null)
711
735
  : syncClaudeAgentDefsAtProxyStartup(config, port),
@@ -895,28 +919,6 @@ const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIM
895
919
 
896
920
  /** Reserve confirmation time within the shared restart deadline. */
897
921
  const RESTART_REOBSERVE_RESERVE_MS = 10_000;
898
- function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
899
- if (result.phase === "identity") {
900
- console.error("❌ Refusing to restart because the running proxy identity could not be attested.");
901
- } else if (result.phase === "request") {
902
- const code = result.error instanceof Error ? result.error.message : "";
903
- if (code === "restart_capability_unsupported") {
904
- console.error("❌ The running proxy predates process-bound restart support; no unsafe fallback was attempted.");
905
- console.error(" After confirming this home owns the proxy, run `ocx stop` and then `ocx start` once.");
906
- } else if (code === "restart_version_skew") {
907
- console.error("❌ The running proxy reports a different OpenCodex version than this CLI; restarting in place would respawn the old installation.");
908
- console.error(" Run `ocx stop` and then `ocx start` from this installation instead.");
909
- } else if (code === "restart_package_tree_unsettled") {
910
- console.error("❌ The proxy's package files are still being replaced; wait for the install to finish, then run `ocx restart` again.");
911
- } else {
912
- console.error("❌ Proxy restart request could not be confirmed; no fallback stop/start was attempted.");
913
- }
914
- } else if (result.phase === "replacement") {
915
- console.error("❌ Proxy restart was accepted, but no identity-verified replacement became healthy in time.");
916
- } else {
917
- console.error("❌ Proxy was not running and the fallback start did not become healthy.");
918
- }
919
- }
920
922
  async function handleProxyRestart(
921
923
  startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
922
924
  ): Promise<boolean> {
@@ -939,6 +941,15 @@ async function handleProxyRestart(
939
941
  findLive: () => findLiveProxy({ deadlineAt: end, attempts: 2, acceptPackageTreeFenced: true }), expired: () => Date.now() >= end,
940
942
  })),
941
943
  });
944
+ if (!result.ok && result.phase === "request" && result.error instanceof UpdateRestartRequired) {
945
+ const candidate = result.error.candidate();
946
+ console.log(`🔄 Running proxy ${candidate.target.version} is older than this CLI (${candidate.cliVersion}); restarting it from the current installation...`);
947
+ const update = await restartFromCurrentInstallation(candidate, deadlineAt, detachedStartEnvironment());
948
+ if (update.ok) console.log(`✅ Proxy updated to ${update.live.version} (PID ${update.live.pid}).`);
949
+ else console.error(`❌ ${describeUpdateRestartFailure(update.code, update.reason)} (${update.code})`);
950
+ process.exitCode = update.ok ? 0 : 1;
951
+ return update.ok;
952
+ }
942
953
  if (!result.ok) reportRestartFailure(result);
943
954
  process.exitCode = result.ok ? 0 : 1;
944
955
  return result.ok;
@@ -0,0 +1,35 @@
1
+ import { printData } from "./runtime-api";
2
+ import { ObservationError, withObserveStream, type ObserveStreamDeps } from "./observe-stream";
3
+ import type { DebugLogEntry } from "../lib/debug-log-buffer";
4
+
5
+ function parseRows(body: unknown, after: number, limit: number): DebugLogEntry[] {
6
+ if (!Array.isArray(body) || body.length > limit) throw new ObservationError("Invalid injection log window. Restart follow after checking the runtime.");
7
+ let previous = after;
8
+ return body.map(value => {
9
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new ObservationError("Invalid injection log row.");
10
+ const { seq, at, line } = value as Record<string, unknown>;
11
+ if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq <= previous || seq <= 0
12
+ || typeof at !== "number" || !Number.isFinite(at) || typeof line !== "string") {
13
+ throw new ObservationError("Invalid injection log order or row. Restart follow after checking the runtime.");
14
+ }
15
+ previous = seq;
16
+ return { seq, at, line };
17
+ });
18
+ }
19
+
20
+ export async function followInjection(limit: number, jsonl: boolean, deps: ObserveStreamDeps): Promise<number> {
21
+ return withObserveStream(deps, async stream => {
22
+ let after = 0;
23
+ while (true) {
24
+ const query = new URLSearchParams({ limit: String(limit), after: String(after) });
25
+ const rows = parseRows(await stream.get("/api/debug/injection-logs", query), after, limit);
26
+ for (const row of rows) {
27
+ stream.signal.throwIfAborted();
28
+ if (jsonl) console.log(JSON.stringify(row));
29
+ else printData(row, false, [`${row.seq} ${row.at} ${row.line}`]);
30
+ after = row.seq;
31
+ }
32
+ await stream.wait();
33
+ }
34
+ });
35
+ }
@@ -35,6 +35,7 @@ const USAGE = `Usage:
35
35
  ocx inspect windows-tray [--json]
36
36
  ocx integration native [list] [--json]
37
37
  ocx integration native <claude|claude-desktop|codex|grok> <on|off> [--json]
38
+ ocx integration native cursor [status|local-installer] [--json]
38
39
  ocx agent request-user-input [on|off] [--json]`;
39
40
 
40
41
  /** A read that takes no arguments beyond `--json`. */
@@ -220,6 +221,10 @@ export async function handleInspectCommand(argv: string[], deps: RuntimeApiDeps
220
221
  }
221
222
 
222
223
  export async function handleIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
224
+ if (argv[0] === "native" && argv[1] === "cursor") {
225
+ const { handleCursorIntegrationCommand } = await import("./integration-cursor");
226
+ return handleCursorIntegrationCommand(argv.slice(2), deps);
227
+ }
223
228
  return runCliAction(async () => {
224
229
  const [sub, ...rest] = argv;
225
230
  if (sub === "native" || sub === undefined) await nativeIntegration(sub === undefined ? [] : rest, deps);
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { redactUserPath } from "../lib/redact";
3
+ import type { RefusalReason } from "../integrations/mutation-plan";
4
+ import { refreshAsideProfilesThroughServer } from "./aside-profiles";
5
+ import { runCatalogAction } from "./catalog-command-result";
6
+ import { CliUsageError, RuntimeApiError, printData, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
7
+
8
+ const USAGE = "Usage: ocx integration client sync --client aside [--json]";
9
+ const refusalText = {
10
+ not_installed: "Aside is not installed for this profile.",
11
+ conflict: "Profile configuration conflicts with the managed integration.",
12
+ unsafe: "Profile configuration cannot be changed safely.",
13
+ non_loopback: "The integration requires a loopback destination.",
14
+ superseded_store: "The client now uses a different configuration store.",
15
+ drift_requires_confirm: "Profile configuration changed; inspect it before an explicit restore.",
16
+ snapshot_expired: "The recovery snapshot has expired.",
17
+ write_failed: "The profile write failed. Inspect its state before retrying.",
18
+ } satisfies Record<RefusalReason, string>;
19
+ const profileId = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
20
+ const text = z.string().max(32768);
21
+ const resultSchema = z.discriminatedUnion("ok", [
22
+ z.object({ client: z.literal("aside"), profileId, ok: z.literal(true), changed: z.boolean(),
23
+ reason: text.optional().transform(value => value === undefined ? undefined
24
+ : value === "managed block is absent; refresh did not reconnect it"
25
+ ? "Managed block remains absent; synchronization did not reconnect it."
26
+ : "Profile synchronization returned a notice; inspect integration status."),
27
+ }),
28
+ z.object({ client: z.literal("aside"), profileId, ok: z.literal(false),
29
+ state: z.enum(["absent", "current", "stale", "conflict", "unsafe"]),
30
+ refusalReason: z.enum(["not_installed", "conflict", "unsafe", "non_loopback", "superseded_store", "drift_requires_confirm", "snapshot_expired", "write_failed"]),
31
+ reason: text.optional(), residual: z.boolean().optional(),
32
+ snapshotPath: text.min(1).transform(redactUserPath).optional(),
33
+ }).transform(row => ({ ...row, reason: refusalText[row.refusalReason] })),
34
+ ]);
35
+ const resultsSchema = z.array(resultSchema).refine(rows => new Set(rows.map(row => row.profileId)).size === rows.length);
36
+
37
+ /** Narrow owner seam: production always uses the established attested exchange. */
38
+ export interface AsideSyncCliDeps extends RuntimeApiDeps {
39
+ refreshAsideProfilesImpl?: typeof refreshAsideProfilesThroughServer;
40
+ }
41
+
42
+ /** argv begins after sync. No local writer or base-URL pre-resolution. */
43
+ export function handleIntegrationAsideSync(argv: string[], deps: AsideSyncCliDeps = {}): Promise<number> {
44
+ return runCatalogAction(async () => {
45
+ const args = [...argv];
46
+ const wantsJson = takeFlag(args, "--json");
47
+ const client = takeOption(args, "--client");
48
+ if (args.length || client !== "aside") throw new CliUsageError("Sync requires --client aside and accepts only --json", USAGE);
49
+ // Sync is an aggregate operation: every owner/transport failure is exit 1,
50
+ // not the record-not-found/conflict exits used by addressed journal writes.
51
+ const outcomes = await (deps.refreshAsideProfilesImpl ?? refreshAsideProfilesThroughServer)(deps)
52
+ .catch(error => {
53
+ if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
54
+ throw new Error("Aside synchronization did not complete");
55
+ });
56
+ const parsed = resultsSchema.safeParse(outcomes);
57
+ if (!parsed.success) throw new Error("Invalid Aside synchronization outcome");
58
+ const results = parsed.data;
59
+ const lines = results.length === 0 ? ["No eligible Aside profiles to synchronize. Check integration status and profile sync preferences."]
60
+ : results.flatMap(row => [
61
+ `Aside profile ${row.profileId}: ${row.ok ? row.changed ? "updated" : "unchanged" : "failed"}.`,
62
+ ...(row.reason ? [row.reason] : []),
63
+ ...(!row.ok && row.residual ? ["Recovery is incomplete; inspect the profile before retrying."] : []),
64
+ ...(!row.ok && row.snapshotPath ? [`Backup (redacted path): ${row.snapshotPath}`] : []),
65
+ ]);
66
+ printData({ results }, wantsJson, lines);
67
+ return results.some(row => !row.ok) ? 1 : 0;
68
+ });
69
+ }
@@ -0,0 +1,106 @@
1
+ /** Read-only Cursor integration observations; never downloads or configures Cursor. */
2
+ import type { CursorIntegrationStatus } from "../server/management/cursor-integration-routes";
3
+ import type { CursorLocalInstallerHint } from "../integrations/cursor-local-installer";
4
+ import { runCatalogAction } from "./catalog-command-result";
5
+ import { CliUsageError, printData, runtimeRequest, takeFlag, type RuntimeApiDeps } from "./runtime-api";
6
+
7
+ const USAGE = "Usage: ocx integration native cursor [status|local-installer] [--json]";
8
+ const GUIDE_URL = "https://lidge-jun.github.io/opencodex/guides/cursor-private-inference/";
9
+ const PLACEHOLDER = "opencodex-loopback";
10
+
11
+ function record(value: unknown): Record<string, unknown> {
12
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Cursor response");
13
+ return value as Record<string, unknown>;
14
+ }
15
+ function text(value: unknown): string {
16
+ if (typeof value !== "string" || !value.length) throw new Error("Invalid Cursor text");
17
+ return value;
18
+ }
19
+ function nullableText(value: unknown): string | null { return value === null ? null : text(value); }
20
+ function boolean(value: unknown): boolean {
21
+ if (typeof value !== "boolean") throw new Error("Invalid Cursor flag");
22
+ return value;
23
+ }
24
+ function integer(value: unknown): number {
25
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid Cursor number");
26
+ return value;
27
+ }
28
+ function strings(value: unknown): string[] {
29
+ if (!Array.isArray(value)) throw new Error("Invalid Cursor list");
30
+ return value.map(text);
31
+ }
32
+
33
+ function statusDto(raw: unknown): CursorIntegrationStatus {
34
+ const value = record(raw), privateInference = record(value.privateInference), regular = record(value.regularCursor);
35
+ const gateway = record(value.gateway), table = record(value.effortTable);
36
+ if (!Array.isArray(value.models) || (gateway.apiKeyMode !== "credential" && gateway.apiKeyMode !== "placeholder")
37
+ || gateway.placeholder !== PLACEHOLDER || (table.source !== "bundle" && table.source !== "static")
38
+ || value.guideUrl !== GUIDE_URL) throw new Error("Invalid Cursor status");
39
+ const baseUrl = text(gateway.baseUrl), parsedUrl = new URL(baseUrl);
40
+ if (!["http:", "https:"].includes(parsedUrl.protocol) || parsedUrl.username || parsedUrl.password
41
+ || parsedUrl.search || parsedUrl.hash || parsedUrl.pathname !== "/v1") throw new Error("Invalid Cursor gateway");
42
+ const last = value.lastSeen === null ? null : record(value.lastSeen);
43
+ return {
44
+ privateInference: { installed: boolean(privateInference.installed), path: nullableText(privateInference.path), version: nullableText(privateInference.version) },
45
+ regularCursor: { installed: boolean(regular.installed), path: nullableText(regular.path) },
46
+ gateway: { baseUrl, apiKeyMode: gateway.apiKeyMode, placeholder: PLACEHOLDER },
47
+ lastSeen: last === null ? null : { at: integer(last.at), userAgent: text(last.userAgent) },
48
+ effortTable: { source: table.source, version: nullableText(table.version), families: table.families === null ? null : integer(table.families) },
49
+ models: value.models.map(rawModel => {
50
+ const model = record(rawModel), context = model.context === null ? null : record(model.context);
51
+ return { id: text(model.id), reasoning: model.reasoning === null ? null : strings(model.reasoning), family: nullableText(model.family),
52
+ tableLess: boolean(model.tableLess), effortRows: strings(model.effortRows),
53
+ context: context === null ? null : { defaultWindow: integer(context.defaultWindow), longWindow: integer(context.longWindow) } };
54
+ }),
55
+ guideUrl: GUIDE_URL,
56
+ };
57
+ }
58
+
59
+ function installerDto(raw: unknown): CursorLocalInstallerHint {
60
+ const value = record(raw), available = boolean(value.available);
61
+ if (value.reason !== null && (typeof value.reason !== "string" || !["no-regular-install", "unsupported-platform", "unreachable", "unusable-response"].includes(value.reason))) {
62
+ throw new Error("Invalid Cursor installer reason");
63
+ }
64
+ const url = nullableText(value.url), version = nullableText(value.version);
65
+ if (available) {
66
+ if (url === null || version === null || value.reason !== null || url.length > 4096 || version.length > 256) throw new Error("Invalid Cursor installer");
67
+ const parsed = new URL(url);
68
+ if (parsed.protocol !== "https:" || parsed.hostname !== "downloads.cursor.com" || parsed.port || parsed.username || parsed.password
69
+ || !parsed.pathname.startsWith("/local-mode/")) throw new Error("Invalid Cursor installer URL");
70
+ } else if (url !== null || version !== null) throw new Error("Invalid unavailable installer");
71
+ return { available, url, version, reason: value.reason as CursorLocalInstallerHint["reason"] };
72
+ }
73
+
74
+ /** Arguments start immediately after `integration native cursor`. */
75
+ export async function handleCursorIntegrationCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
76
+ return runCatalogAction(async () => {
77
+ const args = [...argv], wantsJson = takeFlag(args, "--json"), action = args.shift() ?? "status";
78
+ if (args.length || (action !== "status" && action !== "local-installer")) {
79
+ throw new CliUsageError("Cursor supports only status and local-installer reads.", USAGE);
80
+ }
81
+ const raw = await runtimeRequest(`/api/native-integrations/cursor${action === "local-installer" ? "/local-installer" : ""}`,
82
+ { method: "GET", redirect: "error" }, deps);
83
+ if (action === "local-installer") {
84
+ const result = installerDto(raw);
85
+ printData(result, wantsJson, result.available
86
+ ? [`Cursor installer available: ${result.version}`, `URL: ${result.url}`, "Nothing was downloaded or installed."]
87
+ : [`No installer advertised. Reason: ${result.reason ?? "none (Private Inference is already installed)"}.`, "Nothing was downloaded or installed."]);
88
+ } else {
89
+ const result = statusDto(raw);
90
+ printData(result, wantsJson, [
91
+ `Cursor Private Inference: ${result.privateInference.installed ? "installed" : "not found"}`,
92
+ `Regular Cursor: ${result.regularCursor.installed ? "installed" : "not found"}`,
93
+ `Gateway: ${result.gateway.baseUrl}`,
94
+ result.gateway.apiKeyMode === "credential"
95
+ ? "API key: an existing proxy credential is required. The placeholder is not an access credential."
96
+ : `API key placeholder: ${PLACEHOLDER} (not a secret).`,
97
+ `Last seen: ${result.lastSeen ? `${result.lastSeen.userAgent} at ${result.lastSeen.at}` : "no Cursor request observed since proxy startup"}`,
98
+ `Models: ${result.models.length}`,
99
+ ...result.models.map(model => `${model.id}: reasoning ${model.reasoning?.join(", ") ?? "not advertised"}`),
100
+ `Guide: ${result.guideUrl}`,
101
+ ...(!result.privateInference.installed ? ["Next: ocx integration native cursor local-installer"] : []),
102
+ ]);
103
+ }
104
+ return 0;
105
+ });
106
+ }
@@ -0,0 +1,60 @@
1
+ /** Input shared by integration preview and journal commands; no writer imports. */
2
+ import { CliUsageError, takeOptionWithSyntax } from "./runtime-api";
3
+
4
+ export function validateAsideProfile(profile: string | undefined, client: string | undefined, usage: string): void {
5
+ if (profile === undefined) return;
6
+ if (client !== "aside") throw new CliUsageError("--profile requires --client aside", usage);
7
+ if (!/^(0|[1-9][0-9]*)$/.test(profile) || !Number.isSafeInteger(Number(profile))) {
8
+ throw new CliUsageError("--profile must be a nonnegative integer account ID", usage);
9
+ }
10
+ }
11
+
12
+ export function clientIntegrationPath(client: string, profile?: string): string {
13
+ const base = `/api/client-integrations/${encodeURIComponent(client)}`;
14
+ return client === "aside" ? `${base}/profiles${profile === undefined ? "" : `/${encodeURIComponent(profile)}`}` : base;
15
+ }
16
+
17
+ export function integrationOption(args: string[], flag: string): string | undefined {
18
+ return takeOptionWithSyntax(args, flag)?.value;
19
+ }
20
+
21
+ export function integrationFlag(args: string[], flag: string): boolean {
22
+ const occurrences = args.filter(arg => arg === flag || arg.startsWith(`${flag}=`));
23
+ if (occurrences.length > 1) throw new CliUsageError(`${flag} was given more than once`);
24
+ if (occurrences.length === 0) return false;
25
+ if (occurrences[0] !== flag) throw new CliUsageError(`${flag} does not take a value`);
26
+ args.splice(args.indexOf(flag), 1);
27
+ return true;
28
+ }
29
+
30
+ export function integrationFingerprint(args: string[]): string | undefined {
31
+ const value = integrationOption(args, "--plan-fingerprint");
32
+ if (value !== undefined && !/^p[0-9]+:[0-9a-f]{32}$/.test(value)) {
33
+ throw new CliUsageError("--plan-fingerprint requires a bound preview token");
34
+ }
35
+ return value;
36
+ }
37
+
38
+ /** Entries replace the map; omission preserves it; explicit clear sends {}. */
39
+ export function integrationDroidDefaults(args: string[]): Record<string, string> | undefined {
40
+ const clear = integrationFlag(args, "--clear-reasoning-defaults");
41
+ const entries: Array<[string, string]> = [];
42
+ while (true) {
43
+ const index = args.findIndex(arg => arg === "--reasoning-default" || arg.startsWith("--reasoning-default="));
44
+ if (index < 0) break;
45
+ const token = args[index]!;
46
+ const inline = token.startsWith("--reasoning-default=");
47
+ const raw = inline ? token.slice("--reasoning-default=".length) : args[index + 1];
48
+ if (!raw || (!inline && raw.startsWith("--"))) throw new CliUsageError("--reasoning-default requires MODEL=EFFORT");
49
+ args.splice(index, inline ? 1 : 2);
50
+ const separator = raw.lastIndexOf("=");
51
+ if (separator < 1 || separator === raw.length - 1 || !raw.slice(0, separator).trim() || !raw.slice(separator + 1).trim()) {
52
+ throw new CliUsageError("--reasoning-default requires nonempty MODEL=EFFORT");
53
+ }
54
+ const model = raw.slice(0, separator);
55
+ if (entries.some(([key]) => key === model)) throw new CliUsageError("--reasoning-default contains a duplicate model");
56
+ entries.push([model, raw.slice(separator + 1)]);
57
+ }
58
+ if (clear && entries.length) throw new CliUsageError("--clear-reasoning-defaults cannot be combined with --reasoning-default");
59
+ return clear || entries.length ? Object.fromEntries(entries) : undefined;
60
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ import { isExportClientId } from "../clients/config-export";
3
+ import { runCatalogAction } from "./catalog-command-result";
4
+ import { clientIntegrationPath, validateAsideProfile } from "./integration-input";
5
+ import { CliUsageError, printData, runtimeRequest, takeFlag, takeOption, type RuntimeApiDeps } from "./runtime-api";
6
+
7
+ const USAGE = "Usage: ocx integration client history remove --op <opId> --yes [--client aside [--profile <id>]] [--json]";
8
+ const receiptSchema = z.object({
9
+ ok: z.literal(true), opId: z.string(),
10
+ clientId: z.string().refine(isExportClientId),
11
+ profileId: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(),
12
+ snapshotRemoved: z.boolean(),
13
+ });
14
+
15
+ /** argv begins after history remove (also used by the journal alias). */
16
+ export function handleIntegrationJournalRemove(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
17
+ return runCatalogAction(async () => {
18
+ const args = [...argv];
19
+ const wantsJson = takeFlag(args, "--json");
20
+ const confirmed = takeFlag(args, "--yes");
21
+ const opId = takeOption(args, "--op");
22
+ const client = takeOption(args, "--client");
23
+ const profile = takeOption(args, "--profile");
24
+ if (args.length) throw new CliUsageError("Unknown or repeated journal removal arguments", USAGE);
25
+ if (!opId?.trim() || opId !== opId.trim() || opId.length > 512 || /[\u0000-\u001f\u007f-\u009f]/u.test(opId)) {
26
+ throw new CliUsageError("--op requires a nonempty operation ID without control characters", USAGE);
27
+ }
28
+ if (!confirmed) throw new CliUsageError("Journal removal retires a recovery record; pass --yes to confirm", USAGE);
29
+ if (client !== undefined && client !== "aside") throw new CliUsageError("Journal removal accepts only --client aside or no client selector", USAGE);
30
+ validateAsideProfile(profile, client, USAGE);
31
+ const path = client === "aside" ? `${clientIntegrationPath(client, profile)}/journal` : "/api/client-integrations/journal";
32
+ const parsed = receiptSchema.safeParse(await runtimeRequest(`${path}?opId=${encodeURIComponent(opId)}`, {
33
+ method: "DELETE", redirect: "error",
34
+ }, deps));
35
+ if (!parsed.success) throw new Error("Invalid journal removal receipt");
36
+ const receipt = parsed.data;
37
+ if (receipt.opId !== opId || (client !== undefined && receipt.clientId !== client)
38
+ || (profile !== undefined && receipt.profileId !== Number(profile))
39
+ || (receipt.clientId !== "aside" && receipt.profileId !== undefined)) throw new Error("Journal removal identity mismatch");
40
+ printData(receipt, wantsJson, [
41
+ `Journal operation ${receipt.opId} retired (${receipt.clientId}${receipt.profileId === undefined ? "" : ` profile ${receipt.profileId}`}).`,
42
+ receipt.snapshotRemoved ? "Snapshot cleanup completed." : "Snapshot cleanup is incomplete. The record is already retired; inspect integration status before further cleanup.",
43
+ ]);
44
+ return receipt.snapshotRemoved ? 0 : 1;
45
+ }, {
46
+ integration_journal_newest_protected: "The newest recovery record cannot be removed. Keep it available for restore.",
47
+ integration_operation_not_found: "The operation was not found in the selected journal. Read integration history before retrying.",
48
+ });
49
+ }