@vantran-se/openchamber-web 1.24.3 → 2.0.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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-BjJHrzmU.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-CDOJy5Ay.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-D3S0Rjic.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-D0C9nU7K.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DnEhjXcC.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-DyxZVVVR.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-0pyr_mHY.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-yNj4nOP5.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-DvJKa3Ks.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DxTBMuu-.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-D7FkBMP6.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-CL2vsrIZ.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-Dh7WEsdM.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-Bsba8KFp.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-8_3csmr6.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-CHo2fCgW.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-DkQ_gqpn.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-CdzUmUyZ.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-B8IcVQAA.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-COVQhWGd.js +0 -4
  316. package/dist/assets/FilesView-B1V5JxFX.js +0 -13
  317. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-BrFjAVEf.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +0 -12
  320. package/dist/assets/OnboardingScreen-Dyxxiqdt.js +0 -1
  321. package/dist/assets/PlanView-LW2z_EAP.js +0 -5
  322. package/dist/assets/PluginPane-B4tKno2h.js +0 -3
  323. package/dist/assets/SettingsView-DEaMulN9.js +0 -29
  324. package/dist/assets/SettingsWindow-FLDJXOgQ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +0 -1
  326. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CkR4msKP.js +0 -8
  328. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +0 -1
  329. package/dist/assets/de-DgxgFyGG.js +0 -10
  330. package/dist/assets/es-BFsZj5F_.js +0 -16
  331. package/dist/assets/fr-jdZ5KKQi.js +0 -14
  332. package/dist/assets/index-CKHjPKbq.js +0 -1
  333. package/dist/assets/index-Cbci4i1W.js +0 -40
  334. package/dist/assets/index-DqaMWNrC.css +0 -1
  335. package/dist/assets/ja-C5kKrhO4.js +0 -16
  336. package/dist/assets/ko-DXDTT_0O.js +0 -16
  337. package/dist/assets/main-CZALtVek.js +0 -896
  338. package/dist/assets/main-CgXMBNvG.js +0 -2
  339. package/dist/assets/miniChat-CwTmDixi.js +0 -2
  340. package/dist/assets/mobile-CLf2vCtO.js +0 -2
  341. package/dist/assets/pl-D-7x4Y8Q.js +0 -16
  342. package/dist/assets/pt-BR-CSBZrHa8.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +0 -2
  344. package/dist/assets/renderMobileApp-V4hYd32H.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +0 -25
  346. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +0 -1
  347. package/dist/assets/tr-Dr-oJ22L.js +0 -7
  348. package/dist/assets/uk-9Nx83UcI.js +0 -16
  349. package/dist/assets/useAppFontEffects-khZXpFL_.js +0 -7333
  350. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-Bxy_Hj3r.js +0 -16
  354. package/dist/assets/zh-TW-0BB4gpHM.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -1,6 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import {
4
+ getAncestors,
5
+ findWorktreeRoot,
4
6
  CONFIG_FILE,
5
7
  OPENCODE_CONFIG_DIR,
6
8
  COMMAND_DIR,
@@ -11,47 +13,60 @@ import {
11
13
  readConfigLayers,
12
14
  writeConfig,
13
15
  getJsonEntrySource,
14
- getJsonWriteTarget,
15
16
  isPromptFileReference,
16
17
  resolvePromptFilePath,
17
18
  writePromptFile,
18
19
  } from './shared.js';
20
+ import {
21
+ toCommandEntity,
22
+ fromCommandEntity,
23
+ isLegacyCommandFrontmatter,
24
+ writeSectionEntry,
25
+ deleteSectionEntry,
26
+ parseModelSelection,
27
+ formatModelSelection,
28
+ isRecord,
29
+ } from './config-v2.js';
19
30
 
20
31
  // ============== COMMAND SCOPE HELPERS ==============
32
+ //
33
+ // OpenCode 2 discovers commands from `command/` and `commands/`. OpenChamber
34
+ // reads both and writes only `commands/`; a command already living in the v1
35
+ // directory is rewritten at its own path in v2 shape.
21
36
 
22
- /**
23
- * Ensure project-level command directory exists
24
- */
37
+ const USER_COMMAND_DIRS = [COMMAND_DIR, path.join(OPENCODE_CONFIG_DIR, 'command')];
38
+ const PROJECT_COMMAND_DIR_NAMES = ['commands', 'command'];
39
+
40
+ /** Ensure the v2 project command directory exists. */
25
41
  function ensureProjectCommandDir(workingDirectory) {
26
42
  const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
27
43
  if (!fs.existsSync(projectCommandDir)) {
28
44
  fs.mkdirSync(projectCommandDir, { recursive: true });
29
45
  }
30
- const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
31
- if (!fs.existsSync(legacyProjectCommandDir)) {
32
- fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
33
- }
34
46
  return projectCommandDir;
35
47
  }
36
48
 
37
- /**
38
- * Get project-level command path
39
- */
40
49
  function getProjectCommandPath(workingDirectory, commandName) {
41
- const pluralPath = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
42
- const legacyPath = path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
43
- if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
44
- return pluralPath;
50
+ const preferred = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
51
+ // Same walk as agents: every `.opencode` from the working directory up to
52
+ // the project root is a source; nested names (`team/review`) map onto paths.
53
+ const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
54
+ for (const base of getAncestors(workingDirectory, worktreeRoot)) {
55
+ for (const dirName of PROJECT_COMMAND_DIR_NAMES) {
56
+ const candidate = path.join(base, '.opencode', dirName, `${commandName}.md`);
57
+ if (fs.existsSync(candidate)) return candidate;
58
+ }
59
+ }
60
+ return preferred;
45
61
  }
46
62
 
47
- /**
48
- * Get user-level command path
49
- */
50
63
  function getUserCommandPath(commandName) {
51
- const pluralPath = path.join(COMMAND_DIR, `${commandName}.md`);
52
- const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'command', `${commandName}.md`);
53
- if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
54
- return pluralPath;
64
+ const preferred = path.join(COMMAND_DIR, `${commandName}.md`);
65
+ for (const dir of USER_COMMAND_DIRS) {
66
+ const candidate = path.join(dir, `${commandName}.md`);
67
+ if (fs.existsSync(candidate)) return candidate;
68
+ }
69
+ return preferred;
55
70
  }
56
71
 
57
72
  /**
@@ -65,12 +80,12 @@ function getCommandScope(commandName, workingDirectory) {
65
80
  return { scope: COMMAND_SCOPE.PROJECT, path: projectPath };
66
81
  }
67
82
  }
68
-
83
+
69
84
  const userPath = getUserCommandPath(commandName);
70
85
  if (fs.existsSync(userPath)) {
71
86
  return { scope: COMMAND_SCOPE.USER, path: userPath };
72
87
  }
73
-
88
+
74
89
  return { scope: null, path: null };
75
90
  }
76
91
 
@@ -78,41 +93,40 @@ function getCommandScope(commandName, workingDirectory) {
78
93
  * Get the path where a command should be written based on scope
79
94
  */
80
95
  function getCommandWritePath(commandName, workingDirectory, requestedScope) {
81
- // For updates: check existing location first (project takes precedence)
82
96
  const existing = getCommandScope(commandName, workingDirectory);
83
97
  if (existing.path) {
84
98
  return existing;
85
99
  }
86
-
87
- // For new commands or built-in overrides: use requested scope or default to user
100
+
88
101
  const scope = requestedScope || COMMAND_SCOPE.USER;
89
102
  if (scope === COMMAND_SCOPE.PROJECT && workingDirectory) {
90
- return {
91
- scope: COMMAND_SCOPE.PROJECT,
92
- path: getProjectCommandPath(workingDirectory, commandName)
103
+ return {
104
+ scope: COMMAND_SCOPE.PROJECT,
105
+ path: getProjectCommandPath(workingDirectory, commandName),
93
106
  };
94
107
  }
95
-
96
- return {
97
- scope: COMMAND_SCOPE.USER,
98
- path: getUserCommandPath(commandName)
108
+
109
+ return {
110
+ scope: COMMAND_SCOPE.USER,
111
+ path: getUserCommandPath(commandName),
99
112
  };
100
113
  }
101
114
 
115
+ // ============== READ ==============
116
+
102
117
  function getCommandSources(commandName, workingDirectory) {
103
118
  const projectPath = workingDirectory ? getProjectCommandPath(workingDirectory, commandName) : null;
104
- const projectExists = projectPath && fs.existsSync(projectPath);
119
+ const projectExists = Boolean(projectPath) && fs.existsSync(projectPath);
105
120
 
106
121
  const userPath = getUserCommandPath(commandName);
107
122
  const userExists = fs.existsSync(userPath);
108
123
 
109
124
  const mdPath = projectExists ? projectPath : (userExists ? userPath : null);
110
- const mdExists = !!mdPath;
125
+ const mdExists = Boolean(mdPath);
111
126
  const mdScope = projectExists ? COMMAND_SCOPE.PROJECT : (userExists ? COMMAND_SCOPE.USER : null);
112
127
 
113
128
  const layers = readConfigLayers(workingDirectory);
114
- const jsonSource = getJsonEntrySource(layers, 'command', commandName);
115
- const jsonSection = jsonSource.section;
129
+ const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
116
130
  const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
117
131
  const jsonScope = jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
118
132
 
@@ -121,39 +135,77 @@ function getCommandSources(commandName, workingDirectory) {
121
135
  exists: mdExists,
122
136
  path: mdPath,
123
137
  scope: mdScope,
124
- fields: []
138
+ legacy: false,
139
+ fields: [],
125
140
  },
126
141
  json: {
127
142
  exists: jsonSource.exists,
128
143
  path: jsonPath,
129
144
  scope: jsonSource.exists ? jsonScope : null,
130
- fields: []
145
+ sectionKey: jsonSource.sectionKey,
146
+ legacy: Boolean(jsonSource.legacy),
147
+ fields: [],
131
148
  },
132
149
  projectMd: {
133
150
  exists: projectExists,
134
- path: projectPath
151
+ path: projectPath,
135
152
  },
136
153
  userMd: {
137
154
  exists: userExists,
138
- path: userPath
139
- }
155
+ path: userPath,
156
+ },
140
157
  };
141
158
 
142
159
  if (mdExists) {
143
160
  const { frontmatter, body } = parseMdFile(mdPath);
144
- sources.md.fields = Object.keys(frontmatter);
145
- if (body) {
146
- sources.md.fields.push('template');
147
- }
161
+ sources.md.legacy = isLegacyCommandFrontmatter(frontmatter);
162
+ sources.md.fields = Object.keys(toCommandEntity(frontmatter, body));
148
163
  }
149
164
 
150
- if (jsonSection) {
151
- sources.json.fields = Object.keys(jsonSection);
165
+ if (jsonSource.exists) {
166
+ sources.json.fields = Object.keys(toCommandEntity(jsonSource.section));
152
167
  }
153
168
 
154
169
  return sources;
155
170
  }
156
171
 
172
+ /** Canonical v2 command entity plus where it came from. */
173
+ function getCommandConfig(commandName, workingDirectory) {
174
+ const { path: mdPath, scope } = getCommandScope(commandName, workingDirectory);
175
+ if (mdPath) {
176
+ const { frontmatter, body } = parseMdFile(mdPath);
177
+ return {
178
+ source: 'md',
179
+ scope,
180
+ path: mdPath,
181
+ legacy: isLegacyCommandFrontmatter(frontmatter),
182
+ config: toCommandEntity(frontmatter, body),
183
+ };
184
+ }
185
+
186
+ const layers = readConfigLayers(workingDirectory);
187
+ const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
188
+ if (jsonSource.exists) {
189
+ return {
190
+ source: 'json',
191
+ scope: jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER,
192
+ path: jsonSource.path,
193
+ legacy: Boolean(jsonSource.legacy),
194
+ config: toCommandEntity(jsonSource.section),
195
+ };
196
+ }
197
+
198
+ return { source: 'none', scope: null, path: null, legacy: false, config: {} };
199
+ }
200
+
201
+ // ============== WRITE ==============
202
+
203
+ function writeCommandMd(targetPath, entity) {
204
+ const { fields, template } = fromCommandEntity(entity);
205
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
206
+ writeMdFile(targetPath, fields, template);
207
+ }
208
+
157
209
  function createCommand(commandName, config, workingDirectory, scope) {
158
210
  ensureDirs();
159
211
 
@@ -169,8 +221,7 @@ function createCommand(commandName, config, workingDirectory, scope) {
169
221
  }
170
222
 
171
223
  const layers = readConfigLayers(workingDirectory);
172
- const jsonSource = getJsonEntrySource(layers, 'command', commandName);
173
- if (jsonSource.exists) {
224
+ if (getJsonEntrySource(layers, 'commands', commandName).exists) {
174
225
  throw new Error(`Command ${commandName} already exists in opencode.json`);
175
226
  }
176
227
 
@@ -186,110 +237,78 @@ function createCommand(commandName, config, workingDirectory, scope) {
186
237
  targetScope = COMMAND_SCOPE.USER;
187
238
  }
188
239
 
189
- const { template, scope: _scopeFromConfig, ...frontmatter } = config;
190
-
191
- writeMdFile(targetPath, frontmatter, template || '');
240
+ const { scope: _ignoredScope, ...entity } = isRecord(config) ? config : {};
241
+ writeCommandMd(targetPath, entity);
192
242
  console.log(`Created new command: ${commandName} (scope: ${targetScope}, path: ${targetPath})`);
243
+ return { scope: targetScope, path: targetPath };
193
244
  }
194
245
 
195
- function updateCommand(commandName, updates, workingDirectory) {
196
- ensureDirs();
197
-
198
- const { scope, path: mdPath } = getCommandWritePath(commandName, workingDirectory);
199
- const mdExists = mdPath && fs.existsSync(mdPath);
246
+ // Clearing a v1 field has to reach its v2 location: `subtask` is `subagent`,
247
+ // and `variant` is the suffix on `model`.
248
+ function deleteCommandField(entity, field) {
249
+ if (field === 'variant') {
250
+ const parsed = parseModelSelection(entity.model);
251
+ if (!parsed) return;
252
+ const stripped = formatModelSelection({ providerID: parsed.providerID, modelID: parsed.modelID });
253
+ if (stripped) entity.model = stripped;
254
+ return;
255
+ }
256
+ delete entity[field === 'subtask' ? 'subagent' : field];
257
+ }
200
258
 
201
- const layers = readConfigLayers(workingDirectory);
202
- const jsonSource = getJsonEntrySource(layers, 'command', commandName);
203
- const jsonSection = jsonSource.section;
204
- const hasJsonFields = jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0;
205
- const jsonTarget = jsonSource.exists
206
- ? { config: jsonSource.config, path: jsonSource.path }
207
- : getJsonWriteTarget(layers, workingDirectory ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER);
208
- let config = jsonTarget.config || {};
259
+ function applyCommandUpdates(entity, updates) {
260
+ const next = { ...entity };
261
+ for (const [field, value] of Object.entries(isRecord(updates) ? updates : {})) {
262
+ if (field === 'scope' || value === undefined) continue;
263
+ if (value === null) {
264
+ deleteCommandField(next, field);
265
+ continue;
266
+ }
267
+ // `subtask` is the v1 spelling of `subagent`; accept it from older clients.
268
+ next[field === 'subtask' ? 'subagent' : field] = value;
269
+ }
270
+ return toCommandEntity(next);
271
+ }
209
272
 
210
- const isBuiltinOverride = !mdExists && !hasJsonFields;
273
+ function updateCommand(commandName, updates, workingDirectory) {
274
+ ensureDirs();
211
275
 
212
- let targetPath = mdPath;
213
- let targetScope = scope;
276
+ const current = getCommandConfig(commandName, workingDirectory);
277
+ const entity = applyCommandUpdates(current.config, updates);
214
278
 
215
- if (!mdExists && isBuiltinOverride) {
216
- targetPath = getUserCommandPath(commandName);
217
- targetScope = COMMAND_SCOPE.USER;
279
+ if (current.source === 'md') {
280
+ writeCommandMd(current.path, entity);
281
+ console.log(`Updated command: ${commandName} (md: ${current.path})`);
282
+ return { source: 'md', scope: current.scope, path: current.path };
218
283
  }
219
284
 
220
- const mdData = mdExists ? parseMdFile(mdPath) : (isBuiltinOverride ? { frontmatter: {}, body: '' } : null);
221
-
222
- let mdModified = false;
223
- let jsonModified = false;
224
- const creatingNewMd = isBuiltinOverride;
225
-
226
- for (const [field, value] of Object.entries(updates)) {
227
- if (field === 'template') {
228
- const normalizedValue = typeof value === 'string' ? value : (value == null ? '' : String(value));
229
-
230
- if (mdExists || creatingNewMd) {
231
- if (mdData) {
232
- mdData.body = normalizedValue;
233
- mdModified = true;
234
- }
235
- continue;
236
- } else if (isPromptFileReference(jsonSection?.template)) {
237
- const templateFilePath = resolvePromptFilePath(jsonSection.template);
238
- if (!templateFilePath) {
239
- throw new Error(`Invalid template file reference for command ${commandName}`);
240
- }
241
- writePromptFile(templateFilePath, normalizedValue);
242
- continue;
243
- } else if (isPromptFileReference(normalizedValue)) {
244
- if (!config.command) config.command = {};
245
- if (!config.command[commandName]) config.command[commandName] = {};
246
- config.command[commandName].template = normalizedValue;
247
- jsonModified = true;
248
- continue;
285
+ if (current.source === 'json') {
286
+ const layers = readConfigLayers(workingDirectory);
287
+ const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
288
+ const config = jsonSource.config || {};
289
+ const rawTemplate = jsonSource.section?.template;
290
+ if (isPromptFileReference(rawTemplate)) {
291
+ const templateFilePath = resolvePromptFilePath(rawTemplate);
292
+ if (!templateFilePath) {
293
+ throw new Error(`Invalid template file reference for command ${commandName}`);
249
294
  }
250
-
251
- if (!config.command) config.command = {};
252
- if (!config.command[commandName]) config.command[commandName] = {};
253
- config.command[commandName].template = normalizedValue;
254
- jsonModified = true;
255
- continue;
256
- }
257
-
258
- const inMd = mdData?.frontmatter?.[field] !== undefined;
259
- const inJson = jsonSection?.[field] !== undefined;
260
-
261
- if (inJson) {
262
- if (!config.command) config.command = {};
263
- if (!config.command[commandName]) config.command[commandName] = {};
264
- config.command[commandName][field] = value;
265
- jsonModified = true;
266
- } else if (inMd || creatingNewMd) {
267
- if (mdData) {
268
- mdData.frontmatter[field] = value;
269
- mdModified = true;
270
- }
271
- } else {
272
- if ((mdExists || creatingNewMd) && mdData) {
273
- mdData.frontmatter[field] = value;
274
- mdModified = true;
275
- } else {
276
- if (!config.command) config.command = {};
277
- if (!config.command[commandName]) config.command[commandName] = {};
278
- config.command[commandName][field] = value;
279
- jsonModified = true;
295
+ if (entity.template !== current.config.template) {
296
+ writePromptFile(templateFilePath, entity.template ?? '');
280
297
  }
298
+ entity.template = rawTemplate;
281
299
  }
300
+ writeSectionEntry(config, 'commands', commandName, entity);
301
+ const targetPath = jsonSource.path || CONFIG_FILE;
302
+ writeConfig(config, targetPath);
303
+ console.log(`Updated command: ${commandName} (json: ${targetPath})`);
304
+ return { source: 'json', scope: current.scope, path: targetPath };
282
305
  }
283
306
 
284
- if (mdModified && mdData) {
285
- writeMdFile(targetPath, mdData.frontmatter, mdData.body);
286
- }
287
-
288
- if (jsonModified) {
289
- writeConfig(config, jsonTarget.path || CONFIG_FILE);
290
- }
291
-
292
- console.log(`Updated command: ${commandName} (scope: ${targetScope}, md: ${mdModified}, json: ${jsonModified})`);
307
+ // Built-in override: materialize a user-level v2 markdown command.
308
+ const { scope, path: targetPath } = getCommandWritePath(commandName, workingDirectory, COMMAND_SCOPE.USER);
309
+ writeCommandMd(targetPath, entity);
310
+ console.log(`Created command override: ${commandName} (scope: ${scope}, path: ${targetPath})`);
311
+ return { source: 'md', scope, path: targetPath };
293
312
  }
294
313
 
295
314
  function deleteCommand(commandName, workingDirectory) {
@@ -312,10 +331,9 @@ function deleteCommand(commandName, workingDirectory) {
312
331
  }
313
332
 
314
333
  const layers = readConfigLayers(workingDirectory);
315
- const jsonSource = getJsonEntrySource(layers, 'command', commandName);
316
- if (jsonSource.exists && jsonSource.config && jsonSource.path) {
317
- if (!jsonSource.config.command) jsonSource.config.command = {};
318
- delete jsonSource.config.command[commandName];
334
+ const jsonSource = getJsonEntrySource(layers, 'commands', commandName);
335
+ if (jsonSource.exists && jsonSource.config && jsonSource.path
336
+ && deleteSectionEntry(jsonSource.config, 'commands', commandName)) {
319
337
  writeConfig(jsonSource.config, jsonSource.path);
320
338
  console.log(`Removed command from opencode.json: ${commandName}`);
321
339
  deleted = true;
@@ -328,6 +346,7 @@ function deleteCommand(commandName, workingDirectory) {
328
346
 
329
347
  export {
330
348
  getCommandSources,
349
+ getCommandConfig,
331
350
  createCommand,
332
351
  updateCommand,
333
352
  deleteCommand,
@@ -0,0 +1,16 @@
1
+ export type OpenCodeCompatibility = {
2
+ state: 'unavailable' | 'compatible' | 'incompatible';
3
+ version: string | null;
4
+ installation: 'managed' | 'external' | 'bundled';
5
+ minimumVersion: string;
6
+ canInstall: boolean;
7
+ };
8
+ type Launch = { binary: string; args: string[] };
9
+ type CliOptions = { cwd?: string; env?: NodeJS.ProcessEnv };
10
+ export function isSupportedOpenCodeVersion(version: string): boolean;
11
+ export function readOpenCodeInfo(response: Response): Promise<{ version: string } | null>;
12
+ export function readOpenCodeCliVersion(launch: Launch, options?: CliOptions): Promise<string>;
13
+ export class UnsupportedOpenCodeVersionError extends Error { version: string; constructor(version: string); }
14
+ export function requireOpenCodeV2(launch: Launch, options?: CliOptions): Promise<string>;
15
+ export function readExternalOpenCodeVersion(baseUrl: string, headers: Record<string, string>, fetchImpl?: typeof fetch): Promise<string | null>;
16
+ export function describeOpenCodeCompatibility(version: string | null, installation: OpenCodeCompatibility['installation'], canInstall: boolean): OpenCodeCompatibility;
@@ -0,0 +1,88 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { z } from 'zod';
4
+
5
+ const execute = promisify(execFile);
6
+ const versionSchema = z.string().regex(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/);
7
+ const infoSchema = z.object({ version: versionSchema });
8
+ const legacyHealthSchema = z.object({ version: versionSchema, healthy: z.boolean() });
9
+
10
+ /**
11
+ * Oldest OpenCode OpenChamber runs against. 2.0.15 added `PATCH /api/session`
12
+ * metadata, which now holds every OpenChamber per-session record.
13
+ */
14
+ const MINIMUM_OPENCODE_VERSION = '2.0.15';
15
+
16
+ const releaseParts = (version) => version.split(/[-+]/, 1)[0].split('.').map(Number);
17
+
18
+ const compareRelease = (left, right) => {
19
+ const a = releaseParts(left);
20
+ const b = releaseParts(right);
21
+ for (let index = 0; index < 3; index += 1) {
22
+ if (a[index] !== b[index]) return a[index] - b[index];
23
+ }
24
+ return 0;
25
+ };
26
+
27
+ const isOlderThanMinimum = (version) => versionSchema.safeParse(version).success
28
+ && compareRelease(version, MINIMUM_OPENCODE_VERSION) < 0;
29
+
30
+ /** 2.x at or above the minimum. A future major is a contract OpenChamber has not met yet. */
31
+ export const isSupportedOpenCodeVersion = (version) => versionSchema.safeParse(version).success
32
+ && releaseParts(version)[0] === 2
33
+ && !isOlderThanMinimum(version);
34
+
35
+ export const readOpenCodeInfo = async (response) => {
36
+ if (!response.ok) return null;
37
+ const parsed = infoSchema.safeParse(await response.json().catch(() => null));
38
+ return parsed.success ? parsed.data : null;
39
+ };
40
+
41
+ export const readOpenCodeCliVersion = async (launch, options = {}) => {
42
+ const { stdout } = await execute(launch.binary, [...launch.args, '--version'], {
43
+ ...options, encoding: 'utf8', timeout: 15_000, maxBuffer: 16 * 1024, windowsHide: true,
44
+ });
45
+ const match = /^(?:opencode\s+v?)?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/.exec(stdout.trim());
46
+ if (!match) throw new Error('Could not determine the installed OpenCode version.');
47
+ return match[1];
48
+ };
49
+
50
+ export class UnsupportedOpenCodeVersionError extends Error {
51
+ constructor(version) {
52
+ super(`OpenCode ${version} is installed. OpenChamber requires OpenCode ${MINIMUM_OPENCODE_VERSION} or newer.`);
53
+ this.name = 'UnsupportedOpenCodeVersionError';
54
+ this.version = version;
55
+ }
56
+ }
57
+
58
+ export const requireOpenCodeV2 = async (launch, options) => {
59
+ const version = await readOpenCodeCliVersion(launch, options);
60
+ if (!isSupportedOpenCodeVersion(version)) throw new UnsupportedOpenCodeVersionError(version);
61
+ return version;
62
+ };
63
+
64
+ // External URLs have no local executable. A legacy probe identifies v1 only
65
+ // from its JSON contract; neither HTML fallbacks nor auth failures imply v1.
66
+ export const readExternalOpenCodeVersion = async (baseUrl, headers, fetchImpl = fetch) => {
67
+ const request = (pathname) => fetchImpl(new URL(pathname, baseUrl), {
68
+ headers: { ...headers, Accept: 'application/json' }, redirect: 'error', signal: AbortSignal.timeout(5000),
69
+ });
70
+ const response = await request('/api/info');
71
+ if (response.status === 401 || response.status === 403) return null;
72
+ const info = await readOpenCodeInfo(response);
73
+ if (info) return info.version;
74
+ const legacy = await request('/global/health');
75
+ if (!legacy.ok) return null;
76
+ const parsed = legacyHealthSchema.safeParse(await legacy.json().catch(() => null));
77
+ return parsed.success && parsed.data.version.startsWith('1.') ? parsed.data.version : null;
78
+ };
79
+
80
+ export const describeOpenCodeCompatibility = (version, installation, canInstall) => ({
81
+ state: version === null ? 'unavailable' : isSupportedOpenCodeVersion(version) ? 'compatible' : 'incompatible',
82
+ version,
83
+ installation,
84
+ minimumVersion: MINIMUM_OPENCODE_VERSION,
85
+ // The installer fetches the latest release, which clears both a 1.x CLI and
86
+ // a 2.x one older than the minimum.
87
+ canInstall: version !== null && isOlderThanMinimum(version) && installation === 'managed' && canInstall,
88
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { describeOpenCodeCompatibility, readOpenCodeCliVersion, readExternalOpenCodeVersion, readOpenCodeInfo, requireOpenCodeV2, UnsupportedOpenCodeVersionError } from './compatibility.js';
3
+
4
+ const cli = (output, code = 0) => ({ binary: process.execPath, args: ['-e', `console.log(${JSON.stringify(output)}); process.exit(${code})`, '--'] });
5
+
6
+ describe('OpenCode compatibility', () => {
7
+ it('recognizes both CLI version formats before launching a server', async () => {
8
+ expect(await readOpenCodeCliVersion(cli('1.18.32'))).toBe('1.18.32');
9
+ expect(await requireOpenCodeV2(cli('opencode v2.0.15'))).toBe('2.0.15');
10
+ await expect(requireOpenCodeV2(cli('2.0.14'))).rejects.toBeInstanceOf(UnsupportedOpenCodeVersionError);
11
+ await expect(requireOpenCodeV2(cli('1.18.32'))).rejects.toBeInstanceOf(UnsupportedOpenCodeVersionError);
12
+ await expect(readOpenCodeCliVersion(cli('2.0.14', 1))).rejects.toThrow();
13
+ await expect(readOpenCodeCliVersion(cli('error, requires 2.0.14'))).rejects.toThrow();
14
+ });
15
+
16
+ it('does not accept HTTP 200 HTML or malformed JSON as readiness', async () => {
17
+ expect(await readOpenCodeInfo(new Response('<html>OpenCode</html>'))).toBeNull();
18
+ expect(await readOpenCodeInfo(Response.json({ healthy: true }))).toBeNull();
19
+ expect(await readOpenCodeInfo(Response.json({ version: 'not a version' }))).toBeNull();
20
+ expect(await readOpenCodeInfo(Response.json({ version: '2.0.14' }))).toEqual({ version: '2.0.14' });
21
+ });
22
+
23
+ it('identifies external v1 through its legacy health contract', async () => {
24
+ const requests = [];
25
+ const fetchImpl = async (url) => {
26
+ requests.push(url.pathname);
27
+ return url.pathname === '/api/info' ? new Response('<html/>') : Response.json({ healthy: true, version: '1.18.32' });
28
+ };
29
+ expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, fetchImpl)).toBe('1.18.32');
30
+ expect(requests).toEqual(['/api/info', '/global/health']);
31
+ });
32
+
33
+ it.each([401, 403])('does not call an auth failure v1 (%s)', async (status) => {
34
+ let requests = 0;
35
+ expect(await readExternalOpenCodeVersion('http://localhost:4096', {}, async () => {
36
+ requests += 1;
37
+ return new Response(null, { status });
38
+ })).toBeNull();
39
+ expect(requests).toBe(1);
40
+ });
41
+
42
+ it('accepts 2.x from the minimum on and nothing older or of another major', () => {
43
+ for (const version of ['2.0.15', '2.0.16', '2.1.0', '2.0.15-beta.1']) {
44
+ expect(describeOpenCodeCompatibility(version, 'managed', true).state).toBe('compatible');
45
+ }
46
+ for (const version of ['1.18.32', '2.0.14', '2.0.9', '3.0.0']) {
47
+ expect(describeOpenCodeCompatibility(version, 'managed', true).state).toBe('incompatible');
48
+ }
49
+ expect(describeOpenCodeCompatibility('2.0.14', 'managed', true).minimumVersion).toBe('2.0.15');
50
+ });
51
+
52
+ it('only offers installation for a known managed CLI older than the minimum', () => {
53
+ expect(describeOpenCodeCompatibility('1.18.32', 'managed', true)).toMatchObject({ state: 'incompatible', canInstall: true });
54
+ expect(describeOpenCodeCompatibility('2.0.14', 'managed', true)).toMatchObject({ state: 'incompatible', canInstall: true });
55
+ for (const installation of ['external', 'bundled']) {
56
+ expect(describeOpenCodeCompatibility('1.18.32', installation, true).canInstall).toBe(false);
57
+ }
58
+ for (const version of [null, '2.0.15', '3.0.0']) {
59
+ expect(describeOpenCodeCompatibility(version, 'managed', true).canInstall).toBe(false);
60
+ }
61
+ });
62
+ });