@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -43,6 +43,11 @@ import {
43
43
  raycastConfigPath,
44
44
  zcodeConfigPath,
45
45
  zcodeHomeDir,
46
+ zcodeProviderStorePath,
47
+ buildZcodeStoreContribution,
48
+ zcodeStoreSchemaEstablished,
49
+ type BuildContribution,
50
+ type ConfigFormat,
46
51
  type ExportClientId,
47
52
  } from "../clients/config-export";
48
53
 
@@ -58,6 +63,28 @@ export interface IntegrationClientSpec {
58
63
  configPath: (env?: NodeJS.ProcessEnv, home?: string) => string;
59
64
  /** Directory whose existence is the cheap "is it installed?" signal. */
60
65
  detectDir: (env?: NodeJS.ProcessEnv, home?: string) => string;
66
+ /**
67
+ * The provider store this client reads INSTEAD of `configPath`.
68
+ *
69
+ * A client that moves its store between releases usually keeps a one-shot
70
+ * import from the old location, and that import is exactly what makes the old
71
+ * write look like it still works: it runs once, on an install that has never
72
+ * created the new file, and never again. Everything after it lands in a file
73
+ * the client does not open.
74
+ *
75
+ * A declaration carries everything needed to write the store, not only its
76
+ * location: the text format, the contribution shape its reader understands,
77
+ * and the predicate that says whether a document on disk is a version whose
78
+ * shape has been observed. The last one is what keeps this honest — a store
79
+ * we cannot establish is reported as the reason the write cannot reach the
80
+ * client, never merged into on a guess.
81
+ */
82
+ currentStore?: {
83
+ path: (env?: NodeJS.ProcessEnv, home?: string) => string;
84
+ format: ConfigFormat;
85
+ establishes: (parsed: unknown) => boolean;
86
+ buildContribution: BuildContribution;
87
+ };
61
88
  /** Patch only this block-map YAML leaf; never re-render the shared file. */
62
89
  sourcePreservingYaml?: { path: readonly string[] };
63
90
  /** Coordinate the complete mutation through a sibling config lock. */
@@ -231,6 +258,17 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
231
258
  id: "zcode",
232
259
  configPath: (env = process.env, home = homedir()) => zcodeConfigPath(env, home),
233
260
  detectDir: (env = process.env, home = homedir()) => zcodeHomeDir(env, home),
261
+ /*
262
+ * ZCode 3.14 reads its providers from `v2/provider_config.json` and reaches
263
+ * `v2/config.json` only through the import that seeded it. Where the new
264
+ * file exists the import is spent, so our write is read by nobody (#5348).
265
+ */
266
+ currentStore: {
267
+ path: (env = process.env, home = homedir()) => zcodeProviderStorePath(env, home),
268
+ format: "json",
269
+ establishes: zcodeStoreSchemaEstablished,
270
+ buildContribution: buildZcodeStoreContribution,
271
+ },
234
272
  },
235
273
  prime: {
236
274
  id: "prime",
@@ -11,10 +11,18 @@
11
11
  import { createClineIO } from "./cline-io";
12
12
  import { parseClineDocument } from "./cline-document";
13
13
  import { ClientPathError, EXPORT_CLIENTS, opencodeProxyBaseUrl, type ExportModel, type ManagedContribution } from "../clients/config-export";
14
+ import type { ConfigFormat } from "../clients/config-export";
14
15
  import type { OcxConfig } from "../types";
15
16
  import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
16
17
  import { SNAPSHOT_RETENTION } from "./journal";
17
- import { AmbiguousSelectorError, parseSegment, selectIndex, type PathSegment } from "./merge";
18
+ import {
19
+ AmbiguousSelectorError,
20
+ InvalidSelectorError,
21
+ parseSegment,
22
+ readPath,
23
+ selectIndex,
24
+ type PathSegment,
25
+ } from "./merge";
18
26
  import { canonicalContribution, fingerprint, semanticContribution, type OwnershipRecord } from "./ownership";
19
27
  import {
20
28
  protectedContributionFingerprint,
@@ -28,6 +36,7 @@ import {
28
36
  unresolvedPathHintFor,
29
37
  type IntegrationClientId,
30
38
  } from "./registry";
39
+ import { resolveIntegrationTarget, type IntegrationTarget } from "./target";
31
40
  import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
32
41
 
33
42
  export type IntegrationState = "absent" | "current" | "stale" | "conflict" | "unsafe";
@@ -50,6 +59,21 @@ export interface IntegrationStatus {
50
59
  appliedAt?: string;
51
60
  lastOpId?: string;
52
61
  reason?: StateReason;
62
+ /**
63
+ * The store this client reads instead of `configPath`.
64
+ *
65
+ * Present only when the integration is NOT writing that store: either our
66
+ * block is still in the config file, or the store is not a document whose
67
+ * shape has been observed. Where the store is written, `configPath` names it
68
+ * and there is nothing to report beside the state.
69
+ *
70
+ * Orthogonal to `state`, which answers "what is on disk, and did we put it
71
+ * there?" — and answers it correctly here: the block can be byte-for-byte
72
+ * current in a file the client stopped opening. That pair is not a
73
+ * contradiction, it is the whole of #5348, so the surface that reports
74
+ * `current` has to be able to report this beside it.
75
+ */
76
+ supersededBy?: string;
53
77
  /** Snapshot files retained for this client; -1 when they cannot be inspected. */
54
78
  snapshotCount: number;
55
79
  /** Pruning is behind, so older (possibly credential-bearing) snapshots remain. */
@@ -64,37 +88,14 @@ function assertNever(segment: never): never {
64
88
  throw new Error(`unknown path segment ${JSON.stringify(segment)}`);
65
89
  }
66
90
 
67
- /** The element a selector names, or `undefined` when none matches. */
68
- function selectElement(items: readonly unknown[], segment: PathSegment & { kind: "select" }): unknown {
69
- return items[selectIndex(items, segment.field, segment.value)];
70
- }
71
-
72
91
  /**
73
- * Same segment grammar as `setPath`: a plain key reads through a record, a
74
- * `[field=value]` selector reads through an array. Because the classifier and
75
- * the writer share this one function, status and mutation cannot disagree
76
- * about which element is ours.
92
+ * The path reader lives with the path grammar, in `merge`, and is re-exported
93
+ * here because the classifier was its original home and every caller imports
94
+ * it from this module. One implementation is the point: a reader that resolved
95
+ * a selector differently from the writer would report one element as ours and
96
+ * then rewrite another.
77
97
  */
78
- export function readPath(doc: unknown, path: readonly string[]): unknown {
79
- let cursor: unknown = doc;
80
- for (const raw of path) {
81
- const segment = parseSegment(raw);
82
- switch (segment.kind) {
83
- case "key":
84
- if (!isPlainRecord(cursor)) return undefined;
85
- cursor = cursor[segment.key];
86
- break;
87
- case "select":
88
- if (!Array.isArray(cursor)) return undefined;
89
- cursor = selectElement(cursor, segment);
90
- break;
91
- default:
92
- return assertNever(segment);
93
- }
94
- if (cursor === undefined) return undefined;
95
- }
96
- return cursor;
97
- }
98
+ export { readPath };
98
99
 
99
100
  /** Does the document carry any fragment we would write? */
100
101
  export function hasOurFragments(doc: unknown, contribution: ManagedContribution): boolean {
@@ -136,7 +137,7 @@ export function blockedContainerPath(
136
137
  case "key":
137
138
  return (value as Record<string, unknown>)[segment.key];
138
139
  case "select":
139
- return selectElement(value as readonly unknown[], segment);
140
+ return (value as readonly unknown[])[selectIndex(value as readonly unknown[], segment.criteria)];
140
141
  default:
141
142
  return assertNever(segment);
142
143
  }
@@ -285,6 +286,14 @@ export function classifyIntegration(input: {
285
286
  */
286
287
  configPath?: string;
287
288
  clientId?: IntegrationClientId;
289
+ /**
290
+ * Text format of the file being classified, which is not always the client's
291
+ * config format: a client that moved its providers keeps a second document
292
+ * whose format is declared with the store. Only the comment-capability of the
293
+ * format is read here, and reading the wrong one would decide a sibling edit
294
+ * the wrong way.
295
+ */
296
+ format?: ConfigFormat;
288
297
  }): { state: IntegrationState; reason?: StateReason } {
289
298
  if (input.fileText !== null && !input.fileIsRegular) {
290
299
  return { state: "unsafe", reason: "not-regular-file" };
@@ -310,8 +319,13 @@ export function classifyIntegration(input: {
310
319
  }
311
320
  }
312
321
  } catch (error) {
313
- if (!(error instanceof AmbiguousSelectorError)) throw error;
314
- return { state: "unsafe", reason: "ambiguous-selector" };
322
+ if (error instanceof AmbiguousSelectorError) {
323
+ return { state: "unsafe", reason: "ambiguous-selector" };
324
+ }
325
+ if (error instanceof InvalidSelectorError) {
326
+ return { state: "unsafe", reason: "unparseable" };
327
+ }
328
+ throw error;
315
329
  }
316
330
  if (!hasOurFragments(input.parsed, input.contribution)) return { state: "absent" };
317
331
 
@@ -393,7 +407,7 @@ export function classifyIntegration(input: {
393
407
  * stands and re-owns the file. This also lets disable proceed on a
394
408
  * drifted file — removal still touches only the recorded fragment paths.
395
409
  */
396
- if (EXPORT_CLIENTS[clientId].format !== "json") {
410
+ if ((input.format ?? EXPORT_CLIENTS[clientId].format) !== "json") {
397
411
  return { state: "conflict", reason: "foreign-edit" };
398
412
  }
399
413
  return { state: "stale" };
@@ -491,7 +505,6 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
491
505
  retryPendingPrunesOnce(store);
492
506
  let io = input.io ?? store.io();
493
507
  const spec = INTEGRATION_CLIENTS[input.clientId];
494
- const exportSpec = EXPORT_CLIENTS[input.clientId];
495
508
  const retention = retentionOf(input.clientId, store);
496
509
  /*
497
510
  * Resolution can refuse — a relative OPENCLAW_* selector names a file whose
@@ -499,14 +512,25 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
499
512
  * every client for its state, so letting that escape would answer 500 for
500
513
  * the whole Integrations page because one client is misconfigured.
501
514
  */
502
- let configPath: string;
503
515
  let installed: boolean;
516
+ let effective: IntegrationTarget;
517
+ let record: OwnershipRecord | null;
504
518
  try {
505
519
  // One resolution for both, so a client whose paths come from mutable state
506
520
  // cannot report one account's install beside another account's config path.
507
521
  const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home);
508
- configPath = paths.configPath;
509
522
  installed = io.statKind(paths.detectDir) === "dir";
523
+ if (input.clientId === "cline") io = createClineIO(io, paths.configPath, store);
524
+ /*
525
+ * The record is one of the inputs the target is chosen from, so it is read
526
+ * here rather than after the file. The status this function reports is about
527
+ * whichever file the next mutation would act on; reading a different one
528
+ * would let the badge and the switch disagree.
529
+ */
530
+ record = store.readRecords()[input.clientId] ?? null;
531
+ effective = resolveIntegrationTarget({
532
+ clientId: input.clientId, configPath: paths.configPath, io, record, env: input.env, home: input.home,
533
+ });
510
534
  } catch (error) {
511
535
  if (!(error instanceof ClientPathError)) throw error;
512
536
  /*
@@ -531,30 +555,32 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
531
555
  };
532
556
  }
533
557
 
534
- if (input.clientId === "cline") io = createClineIO(io, configPath, store);
535
- const target = loadTarget(io, configPath);
536
- if (!target.ok) {
558
+ const configPath = effective.configPath;
559
+ const loaded = loadTarget(io, configPath);
560
+ if (!loaded.ok) {
537
561
  return {
538
562
  clientId: input.clientId,
539
563
  state: "unsafe",
540
564
  installed,
541
565
  configPath,
542
- reason: target.why === "read-failed" ? "unparseable" : "not-regular-file",
566
+ reason: loaded.why === "read-failed" ? "unparseable" : "not-regular-file",
543
567
  ...retention,
544
568
  };
545
569
  }
546
570
 
547
- const parsed = input.clientId === "cline" ? parseClineDocument(target.before) : parseConfig(target.before, exportSpec.format);
548
- const contribution = exportSpec.buildContribution(exportContextOf(input));
549
- const record = store.readRecords()[input.clientId] ?? null;
571
+ const parsed = input.clientId === "cline"
572
+ ? parseClineDocument(loaded.before)
573
+ : parseConfig(loaded.before, effective.format);
574
+ const contribution = effective.buildContribution(exportContextOf(input));
550
575
  const { state, reason } = classifyIntegration({
551
- fileText: target.before,
576
+ fileText: loaded.before,
552
577
  fileIsRegular: true,
553
578
  parsed,
554
579
  record,
555
580
  contribution,
556
581
  configPath,
557
582
  clientId: input.clientId,
583
+ format: effective.format,
558
584
  });
559
585
 
560
586
  return {
@@ -563,6 +589,13 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
563
589
  installed,
564
590
  configPath,
565
591
  ...(reason ? { reason } : {}),
592
+ /*
593
+ * Only when the client reads a DIFFERENT file than the one this status is
594
+ * about. A store we are writing needs no notice; the path already names it.
595
+ */
596
+ ...(effective.ineffective && effective.ineffective.store !== configPath
597
+ ? { supersededBy: effective.ineffective.store }
598
+ : {}),
566
599
  ...(record ? { appliedAt: record.appliedAt, lastOpId: record.opId } : {}),
567
600
  ...retention,
568
601
  };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Which file an integration operation reads, writes and records — and, when
3
+ * that is not the file the client reads, which file is.
4
+ *
5
+ * A client may move its provider list between releases and keep the old file
6
+ * reachable only through a one-shot import. Everything the integration checks
7
+ * still passes against the old file: it is writable, the block merges, the
8
+ * ownership record describes it correctly. The only thing that changed is
9
+ * whether anything reads it (#5348).
10
+ *
11
+ * So the target is chosen before anything is classified, and it is chosen by
12
+ * three facts in this order:
13
+ *
14
+ * 1. No declared store, or no store on disk — the config file, exactly as
15
+ * before. A client that has never run still imports what we write there.
16
+ * 2. Our own block already sits in the config file this record names. Stay
17
+ * there. Disable removes what we wrote from where we wrote it, and the
18
+ * caller reports the write as ineffective rather than orphaning a block in
19
+ * one file while writing another.
20
+ * 3. Otherwise the store, but only when its schema can be established. A store
21
+ * we cannot parse or whose version we have not observed is reported as the
22
+ * reason the write cannot reach the client, because merging an invented
23
+ * nesting into a file holding the user's other providers would trade a
24
+ * silent no-op for a silent loss.
25
+ */
26
+ import { EXPORT_CLIENTS, ClientPathError, type BuildContribution, type ConfigFormat } from "../clients/config-export";
27
+ import { PARSE_FAILED, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
28
+ import { AmbiguousSelectorError, InvalidSelectorError, readPath } from "./merge";
29
+ import type { OwnershipRecord } from "./ownership";
30
+ import { INTEGRATION_CLIENTS, type IntegrationClientId } from "./registry";
31
+
32
+ export interface IntegrationTarget {
33
+ /** The file this operation reads, writes, journals and records ownership for. */
34
+ readonly configPath: string;
35
+ /** Text format of that file, which is not always the client's config format. */
36
+ readonly format: ConfigFormat;
37
+ /** The contribution shape that file's reader understands. */
38
+ readonly buildContribution: BuildContribution;
39
+ /**
40
+ * Set when a write to the file above would not reach the client.
41
+ *
42
+ * `why` is carried rather than re-derived because the cases have different
43
+ * remedies and only this function knows which one it took. Disable is not
44
+ * gated on it: removing bytes this project wrote from the file it wrote them
45
+ * to is as effective as it ever was.
46
+ */
47
+ readonly ineffective: IneffectiveWrite | null;
48
+ }
49
+
50
+ export interface IneffectiveWrite {
51
+ /** The provider store the client reads. */
52
+ readonly store: string;
53
+ /**
54
+ * `owned-config-file` — this project's block is still in the config file, so
55
+ * the operation stays there and the remedy is to remove it first, rather than
56
+ * leave a block in one file while writing another.
57
+ *
58
+ * `unestablished-schema` — the store is not a document whose shape has been
59
+ * observed, so there is nothing safe to merge into it. This one is reachable
60
+ * with the store itself as the target: a client that bumps its schema after
61
+ * we wrote the store leaves our block there, removable, and the file no
62
+ * longer one we may merge into.
63
+ */
64
+ readonly why: "owned-config-file" | "unestablished-schema";
65
+ }
66
+
67
+ function configFileTarget(
68
+ clientId: IntegrationClientId,
69
+ configPath: string,
70
+ ineffective: IneffectiveWrite | null,
71
+ ): IntegrationTarget {
72
+ const exportSpec = EXPORT_CLIENTS[clientId];
73
+ return {
74
+ configPath,
75
+ format: exportSpec.format,
76
+ buildContribution: exportSpec.buildContribution,
77
+ ineffective,
78
+ };
79
+ }
80
+
81
+ function storeTarget(
82
+ declared: NonNullable<(typeof INTEGRATION_CLIENTS)[IntegrationClientId]["currentStore"]>,
83
+ configPath: string,
84
+ ineffective: IneffectiveWrite | null = null,
85
+ ): IntegrationTarget {
86
+ return {
87
+ configPath,
88
+ format: declared.format,
89
+ buildContribution: declared.buildContribution,
90
+ ineffective,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Does the file this record names still hold any fragment it claims?
96
+ *
97
+ * Uncertainty answers yes. An unreadable or unparseable config file is a state
98
+ * the classifier is about to refuse on, and it must refuse on the file our
99
+ * record is about rather than silently move the operation to a different one.
100
+ * An ambiguous or invalid selector is the same kind of answer.
101
+ */
102
+ function recordedBlockStillPresent(
103
+ io: IntegrationIO,
104
+ format: ConfigFormat,
105
+ configPath: string,
106
+ record: OwnershipRecord,
107
+ ): boolean {
108
+ const loaded = loadTarget(io, configPath);
109
+ if (!loaded.ok) return true;
110
+ const parsed = parseConfig(loaded.before, format);
111
+ if (parsed === PARSE_FAILED) return true;
112
+ try {
113
+ return record.fragmentPaths.some(path => readPath(parsed, path) !== undefined);
114
+ } catch (error) {
115
+ if (error instanceof AmbiguousSelectorError || error instanceof InvalidSelectorError) return true;
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Resolve the target for one operation.
122
+ *
123
+ * `configPath` is the client's config file as the registry resolves it now, and
124
+ * `record` is the stored ownership as read, before any path filtering — the
125
+ * filtering depends on the answer this function returns.
126
+ *
127
+ * The store is observed through the caller's own `IntegrationIO`, never the
128
+ * real filesystem directly, so status and mutation cannot disagree about
129
+ * whether a write can land.
130
+ */
131
+ export function resolveIntegrationTarget(args: {
132
+ clientId: IntegrationClientId;
133
+ configPath: string;
134
+ io: IntegrationIO;
135
+ record: OwnershipRecord | null;
136
+ env?: NodeJS.ProcessEnv;
137
+ home?: string;
138
+ }): IntegrationTarget {
139
+ const { clientId, configPath, io, record } = args;
140
+ const declared = INTEGRATION_CLIENTS[clientId].currentStore;
141
+ if (!declared) return configFileTarget(clientId, configPath, null);
142
+ const storePath = declared.path(args.env, args.home);
143
+ const kind = io.statKind(storePath);
144
+ if (kind === "missing") return configFileTarget(clientId, configPath, null);
145
+ // Only proven absence permits a legacy write. Unreadable or non-file stores
146
+ // cannot establish what the client reads; preserve the recorded removal target.
147
+ if (kind !== "file") {
148
+ const ineffective: IneffectiveWrite = { store: storePath, why: "unestablished-schema" };
149
+ return record?.clientId === clientId && record.configPath === storePath
150
+ ? storeTarget(declared, storePath, ineffective)
151
+ : configFileTarget(clientId, configPath, ineffective);
152
+ }
153
+ const loaded = loadTarget(io, storePath);
154
+ const parsed = loaded.ok ? parseConfig(loaded.before, declared.format) : PARSE_FAILED;
155
+ const established = parsed !== PARSE_FAILED && declared.establishes(parsed);
156
+ const unestablished: IneffectiveWrite = { store: storePath, why: "unestablished-schema" };
157
+ /*
158
+ * Our own block decides the target before the store's schema does, whichever
159
+ * file holds it. Disable has to remove what we wrote from where we wrote it,
160
+ * and an apply must never leave a block in one file while writing another.
161
+ */
162
+ const owned = record !== null && record.clientId === clientId ? record : null;
163
+ if (owned?.configPath === storePath && recordedBlockStillPresent(io, declared.format, storePath, owned)) {
164
+ return storeTarget(declared, storePath, established ? null : unestablished);
165
+ }
166
+ if (
167
+ owned?.configPath === configPath
168
+ && recordedBlockStillPresent(io, EXPORT_CLIENTS[clientId].format, configPath, owned)
169
+ ) {
170
+ return configFileTarget(clientId, configPath, { store: storePath, why: "owned-config-file" });
171
+ }
172
+ if (!established) {
173
+ return configFileTarget(clientId, configPath, unestablished);
174
+ }
175
+ return storeTarget(declared, storePath);
176
+ }
177
+
178
+ /**
179
+ * The target a path NAMES, or null when this client may not write there.
180
+ *
181
+ * Undo acts on the file its journal row recorded, so it needs the meaning of a
182
+ * historical path rather than the choice above: which contribution shape
183
+ * describes those bytes, and whether this client still resolves that location
184
+ * at all. Resolving a path the client no longer names is what would let a row
185
+ * recorded for one home delete a file in another, so an unrecognised path is
186
+ * null and the caller refuses.
187
+ */
188
+ export function declaredIntegrationTarget(args: {
189
+ clientId: IntegrationClientId;
190
+ configPath: string;
191
+ resolvedConfigPath: string;
192
+ env?: NodeJS.ProcessEnv;
193
+ home?: string;
194
+ }): IntegrationTarget | null {
195
+ const { clientId, configPath, resolvedConfigPath } = args;
196
+ if (configPath === resolvedConfigPath) return configFileTarget(clientId, configPath, null);
197
+ const declared = INTEGRATION_CLIENTS[clientId].currentStore;
198
+ if (!declared) return null;
199
+ try {
200
+ if (declared.path(args.env, args.home) !== configPath) return null;
201
+ } catch (error) {
202
+ // A store the operator relocated with a path we cannot resolve proves
203
+ // nothing about the row, so the row is not a legal target.
204
+ if (error instanceof ClientPathError) return null;
205
+ throw error;
206
+ }
207
+ return storeTarget(declared, configPath);
208
+ }