@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.3

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 (311) hide show
  1. package/README.md +13 -7
  2. package/bin/cli-opencode.test.js +208 -0
  3. package/bin/cli.js +62 -9
  4. package/bin/lib/cli-startup.js +105 -2
  5. package/bin/lib/cli-startup.test.js +179 -0
  6. package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-B_JQoreg.js} +1 -1
  7. package/dist/assets/AssistantView-ClQAfGOT.js +3 -0
  8. package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-BNnda8qO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-VYQTSAkr.js} +1 -1
  10. package/dist/assets/{Icon-BgdfPhNs.js → Icon-BiEQEcih.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-CJHSsWin.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-DwrHFGUp.js} +3 -3
  13. package/dist/assets/{MarkstreamRendererImpl-D2dsW5Lg.js → MarkstreamRendererImpl-CSnbQMu3.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-BXr6AGLT.js +1 -0
  15. package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-D7OE9OP_.js} +2 -2
  16. package/dist/assets/MobileSurface-D2jUkX9l.js +2 -0
  17. package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-DF3iIFCd.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-BjHmlwlw.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-DLtWO_Gp.js +2 -0
  20. package/dist/assets/SettingsView-BRxELjow.js +25 -0
  21. package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-BI5swspb.js} +1 -1
  22. package/dist/assets/{TerminalView-CDdzF4yi.js → TerminalView-BU3x8-fr.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-BEWhBcRd.js} +2 -2
  24. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +1 -0
  25. package/dist/assets/{button-CI7Q-Zw_.js → button-DKuz3L8l.js} +1 -1
  26. package/dist/assets/collapsible-DXbDynN4.js +3 -0
  27. package/dist/assets/context-menu-CqolsLCm.js +1 -0
  28. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-CoDkOnyS.js} +1 -1
  29. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DDlVKyOE.js} +1 -1
  30. package/dist/assets/{diagram-DdLO8HZ9.js → diagram-D7T8HQgj.js} +1 -1
  31. package/dist/assets/{dialog-B6pnIYmp.js → dialog-DQYl3W_o.js} +1 -1
  32. package/dist/assets/es-CN3E_zXj.js +15 -0
  33. package/dist/assets/{fileMentionTouchSelection-DNeiqns-.js → fileMentionTouchSelection-BZ6ovBYH.js} +1 -1
  34. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-JrpcsOCs.js} +1 -1
  35. package/dist/assets/fr-9zZvQZD2.js +13 -0
  36. package/dist/assets/gitApi-CM42qUed.js +10 -0
  37. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-CIl0FL10.js} +1 -1
  38. package/dist/assets/{input-D-__fwRi.js → input-CzQy8UhV.js} +1 -1
  39. package/dist/assets/insertionBoundaries-CtkZ9720.js +1 -0
  40. package/dist/assets/ja-Dnjk_82N.js +15 -0
  41. package/dist/assets/ko-BxCEE99C.js +15 -0
  42. package/dist/assets/main-BILgBI3B.js +169 -0
  43. package/dist/assets/main-BeveiW6F.js +2 -0
  44. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  45. package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-Cj1I5NDw.js} +1 -1
  46. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  47. package/dist/assets/mcpQueries-D5mQymgM.js +1 -0
  48. package/dist/assets/miniChat-BgrzM3u7.js +2 -0
  49. package/dist/assets/mobile-DtYoQE8C.js +2 -0
  50. package/dist/assets/multirun-jy8dEI96.js +4 -0
  51. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-C9LQpiOS.js} +2 -2
  52. package/dist/assets/pl-CNNsWnPM.js +15 -0
  53. package/dist/assets/platform-BQghHRfp.js +6 -0
  54. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  55. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +2 -0
  56. package/dist/assets/renderMobileApp-u3c4GtFC.js +28 -0
  57. package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-BIGBRGQI.js} +1 -1
  58. package/dist/assets/{runtimeEndpointReset-ChOxt-J7.js → runtimeEndpointReset-D2savkuw.js} +12 -12
  59. package/dist/assets/{sessionChangeSummary-NnxcqCTI.js → sessionChangeSummary-eIbpSfrO.js} +1 -1
  60. package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-myHe5ltu.js} +1 -1
  61. package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-BQtmHx9r.js} +1 -1
  62. package/dist/assets/src-CLN7Z-zx.css +2 -0
  63. package/dist/assets/store-DLCiZaA6.js +15 -0
  64. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-BxcdiduX.js} +1 -1
  65. package/dist/assets/{textarea-bHYxZtfH.js → textarea-CakKvqFw.js} +1 -1
  66. package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-Cx5gTbdY.js} +1 -1
  67. package/dist/assets/uk-rwQrxeYM.js +15 -0
  68. package/dist/assets/{url-_z7evzdK.js → url-CDHnjlsm.js} +1 -1
  69. package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-CNeo0aZs.js} +59 -45
  70. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  71. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +1 -0
  72. package/dist/assets/useEffectiveDirectory-B4LvR8Gt.js +1 -0
  73. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-CUnQ4TVm.js} +1 -1
  74. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +414 -0
  75. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  76. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +2 -0
  77. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  78. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  79. package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-CJZbJDW3.js} +2 -2
  80. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  81. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  82. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  83. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  85. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  86. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  87. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  88. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  89. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  90. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  91. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  92. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  93. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  94. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  95. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  96. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  97. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  98. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  99. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  100. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  101. package/dist/index.html +13 -13
  102. package/dist/mini-chat.html +13 -13
  103. package/dist/mobile.html +13 -13
  104. package/package.json +2 -2
  105. package/server/index.js +392 -20
  106. package/server/lib/assistants/DOCUMENTATION.md +26 -21
  107. package/server/lib/assistants/assign.js +2 -1
  108. package/server/lib/assistants/contact-store.js +37 -19
  109. package/server/lib/assistants/contracts.js +11 -1
  110. package/server/lib/assistants/projection-history.test.js +454 -0
  111. package/server/lib/assistants/routes.js +11 -5
  112. package/server/lib/assistants/service.js +850 -294
  113. package/server/lib/assistants/service.test.js +588 -274
  114. package/server/lib/conversations/DOCUMENTATION.md +5 -5
  115. package/server/lib/conversations/service.js +87 -57
  116. package/server/lib/conversations/service.test.js +132 -100
  117. package/server/lib/conversations/validation.js +11 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +51 -5
  120. package/server/lib/event-stream/runtime.js +57 -20
  121. package/server/lib/event-stream/runtime.test.js +112 -0
  122. package/server/lib/llm/DOCUMENTATION.md +95 -84
  123. package/server/lib/llm/catalog.js +111 -45
  124. package/server/lib/llm/catalog.test.js +121 -59
  125. package/server/lib/llm/completions.js +41 -12
  126. package/server/lib/llm/completions.test.js +45 -4
  127. package/server/lib/llm/generate.js +516 -453
  128. package/server/lib/llm/generate.test.js +705 -484
  129. package/server/lib/llm/routes.js +8 -4
  130. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  131. package/server/lib/message-queue/opencode-adapter.js +191 -104
  132. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  133. package/server/lib/message-queue/runtime.js +15 -2
  134. package/server/lib/message-queue/runtime.test.js +6 -6
  135. package/server/lib/message-queue/worker.js +7 -1
  136. package/server/lib/notifications/runtime.js +5 -0
  137. package/server/lib/opencode/DOCUMENTATION.md +103 -16
  138. package/server/lib/opencode/agent-document.d.ts +28 -0
  139. package/server/lib/opencode/agent-document.js +280 -0
  140. package/server/lib/opencode/agent-document.test.js +137 -0
  141. package/server/lib/opencode/agents.js +96 -0
  142. package/server/lib/opencode/auth-state-runtime.js +5 -0
  143. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  144. package/server/lib/opencode/config-entity-routes.js +111 -102
  145. package/server/lib/opencode/config-entity-routes.test.js +123 -10
  146. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  147. package/server/lib/opencode/ensure-cli.js +238 -0
  148. package/server/lib/opencode/ensure-cli.test.js +119 -0
  149. package/server/lib/opencode/env-runtime.js +208 -111
  150. package/server/lib/opencode/env-runtime.test.js +211 -14
  151. package/server/lib/opencode/feature-routes-runtime.js +80 -5
  152. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  153. package/server/lib/opencode/http-server-connections.js +98 -0
  154. package/server/lib/opencode/http-server-connections.test.js +321 -0
  155. package/server/lib/opencode/lifecycle.js +539 -95
  156. package/server/lib/opencode/lifecycle.test.js +698 -6
  157. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  158. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  159. package/server/lib/opencode/network-runtime.js +28 -16
  160. package/server/lib/opencode/network-runtime.test.js +68 -4
  161. package/server/lib/opencode/openchamber-routes.js +7 -1
  162. package/server/lib/opencode/opencode2-pin.js +139 -0
  163. package/server/lib/opencode/opencode2-pin.test.js +88 -0
  164. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
  165. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  166. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  167. package/server/lib/opencode/plugin-compatibility.js +15 -0
  168. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  169. package/server/lib/opencode/plugin-routes.js +3 -18
  170. package/server/lib/opencode/plugin-routes.test.js +9 -9
  171. package/server/lib/opencode/plugins.js +4 -3
  172. package/server/lib/opencode/plugins.test.js +12 -0
  173. package/server/lib/opencode/provider-catalog.js +58 -12
  174. package/server/lib/opencode/provider-catalog.test.js +48 -0
  175. package/server/lib/opencode/proxy.js +368 -28
  176. package/server/lib/opencode/proxy.test.js +338 -0
  177. package/server/lib/opencode/routes.js +303 -76
  178. package/server/lib/opencode/routes.test.js +315 -50
  179. package/server/lib/opencode/runtime-contract.js +401 -0
  180. package/server/lib/opencode/runtime-contract.test.js +214 -0
  181. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  182. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  183. package/server/lib/opencode/server-utils-runtime.js +35 -0
  184. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  185. package/server/lib/opencode/shared-service.js +147 -0
  186. package/server/lib/opencode/shared-service.test.js +93 -0
  187. package/server/lib/opencode/shared.js +3 -3
  188. package/server/lib/opencode/shutdown-runtime.js +15 -6
  189. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  190. package/server/lib/opencode/skill-routes.js +26 -40
  191. package/server/lib/opencode/skill-routes.test.js +71 -7
  192. package/server/lib/opencode/skills.test.js +15 -0
  193. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  194. package/server/lib/opencode/static-routes-runtime.js +3 -1
  195. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  196. package/server/lib/opencode/v1-migration-gate.js +157 -0
  197. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  198. package/server/lib/opencode/v2-client.js +60 -0
  199. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  200. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  201. package/server/lib/opencode/watcher.js +2 -1
  202. package/server/lib/opencode/watcher.test.js +1 -1
  203. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  204. package/server/lib/permission-auto-accept/runtime.js +14 -7
  205. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  206. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  207. package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
  208. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  209. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  210. package/server/lib/scheduled-tasks/routes.js +27 -0
  211. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  212. package/server/lib/scheduled-tasks/runtime.js +381 -310
  213. package/server/lib/scheduled-tasks/runtime.test.js +600 -309
  214. package/server/lib/session-goal/DOCUMENTATION.md +173 -22
  215. package/server/lib/session-goal/capability.js +57 -0
  216. package/server/lib/session-goal/capability.test.js +33 -0
  217. package/server/lib/session-goal/routes.js +240 -2
  218. package/server/lib/session-goal/routes.test.js +211 -0
  219. package/server/lib/session-goal/runtime.js +1617 -113
  220. package/server/lib/session-goal/runtime.test.js +1500 -70
  221. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  222. package/server/lib/session-index/event-ingest.js +89 -16
  223. package/server/lib/session-index/event-ingest.test.js +73 -0
  224. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  225. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  226. package/server/lib/session-index/routes.js +63 -10
  227. package/server/lib/session-index/routes.test.js +8 -8
  228. package/server/lib/session-index/service.js +7 -1
  229. package/server/lib/session-index/sync-runtime.js +171 -41
  230. package/server/lib/session-index/sync-runtime.test.js +164 -9
  231. package/server/lib/session-metadata/DOCUMENTATION.md +86 -0
  232. package/server/lib/session-metadata/routes.js +193 -0
  233. package/server/lib/session-metadata/routes.test.js +156 -0
  234. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  235. package/server/lib/session-metadata/session-archive.js +654 -0
  236. package/server/lib/session-metadata/session-archive.test.js +359 -0
  237. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  238. package/server/lib/session-metadata/session-metadata-store.js +375 -0
  239. package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
  240. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  241. package/server/lib/session-metadata/session-projection.js +255 -0
  242. package/server/lib/session-metadata/session-projection.test.js +64 -0
  243. package/server/lib/session-metadata/system-session.js +55 -0
  244. package/server/lib/session-metadata/system-session.test.js +55 -0
  245. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  246. package/server/lib/session-title/native-runtime.test.js +167 -0
  247. package/server/lib/session-title/runtime.js +67 -42
  248. package/server/lib/session-title/session-access.js +46 -0
  249. package/server/lib/session-title/session-access.test.js +91 -0
  250. package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
  251. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  252. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  253. package/server/lib/session-turn-pages/routes.js +123 -75
  254. package/server/lib/session-turn-pages/routes.test.js +566 -0
  255. package/server/lib/session-turn-pages/service.js +6 -2
  256. package/server/lib/session-turn-pages/service.test.js +34 -0
  257. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  258. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  259. package/server/lib/small-model/DOCUMENTATION.md +101 -146
  260. package/server/lib/small-model/catalog.js +84 -54
  261. package/server/lib/small-model/catalog.test.js +111 -9
  262. package/server/lib/small-model/custom-api.js +68 -0
  263. package/server/lib/small-model/custom-api.test.js +64 -0
  264. package/server/lib/small-model/index.js +125 -241
  265. package/server/lib/small-model/resolve.js +35 -120
  266. package/server/lib/small-model/resolve.test.js +20 -110
  267. package/server/lib/small-model/routes.js +1 -3
  268. package/server/lib/small-model/summary-settings.test.js +157 -253
  269. package/server/opencode-proxy.test.js +63 -12
  270. package/dist/assets/AssistantView-BUveAgc4.js +0 -3
  271. package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
  272. package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
  273. package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
  274. package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
  275. package/dist/assets/SettingsView-PkXIavr7.js +0 -25
  276. package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
  277. package/dist/assets/collapsible-D5Phaykh.js +0 -1
  278. package/dist/assets/context-menu-CubSDW2g.js +0 -1
  279. package/dist/assets/es-DA3MqOo0.js +0 -15
  280. package/dist/assets/fr-Dbz0CdIA.js +0 -13
  281. package/dist/assets/gitApi-DXyC4MiV.js +0 -7
  282. package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
  283. package/dist/assets/ja-DumanVQA.js +0 -15
  284. package/dist/assets/ko-C0IkiUjQ.js +0 -15
  285. package/dist/assets/main-BPS5ylib.js +0 -2
  286. package/dist/assets/main-Cpd9VWYs.js +0 -169
  287. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  288. package/dist/assets/miniChat-B8_KLWiY.js +0 -2
  289. package/dist/assets/mobile-CtktKrXn.js +0 -2
  290. package/dist/assets/multirun-CYfP9Cfm.js +0 -4
  291. package/dist/assets/pl-B9No6MQp.js +0 -15
  292. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  293. package/dist/assets/projectMeta-S1hJ7oBp.js +0 -3
  294. package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
  295. package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
  296. package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
  297. package/dist/assets/src-rS3zCcsC.css +0 -2
  298. package/dist/assets/store-5B_LDnLM.js +0 -15
  299. package/dist/assets/uk-C_1KkqyP.js +0 -15
  300. package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
  301. package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
  302. package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
  303. package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
  304. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  305. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  306. package/dist/assets/zh-CN-uWILgBdt.js +0 -15
  307. package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
  308. package/server/lib/small-model/call.js +0 -427
  309. package/server/lib/small-model/call.test.js +0 -382
  310. package/server/lib/small-model/opencode-session.js +0 -339
  311. package/server/lib/small-model/opencode-session.test.js +0 -177
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![GitHub release](https://img.shields.io/github/v/release/yee94/openchamber?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2YxZWNlYyIgdmlld0JveD0iMCAwIDI1NiAyNTYiPjxwYXRoIGQ9Ik0xMjgsMTI5LjA5VjIzMmE4LDgsMCwwLDEtMy44NC0xbC04OC00OC4xOGE4LDgsMCwwLDEtNC4xNi03VjgwLjE4YTgsOCwwLDAsMSwuNy0zLjI1WiIgb3BhY2l0eT0iMC4yIj48L3BhdGg%2BPHBhdGggZD0iTTIyMy42OCw2Ni4xNSwxMzUuNjgsMThhMTUuODgsMTUuODgsMCwwLDAtMTUuMzYsMGwtODgsNDguMTdhMTYsMTYsMCwwLDAtOC4zMiwxNHY5NS42NGExNiwxNiwwLDAsMCw4LjMyLDE0bDg4LDQ4LjE3YTE1Ljg4LDE1Ljg4LDAsMCwwLDE1LjM2LDBsODgtNDguMTdhMTYsMTYsMCwwLDAsOC4zMi0xNFY4MC4xOEExNiwxNiwwLDAsMCwyMjMuNjgsNjYuMTVaTTEyOCwzMmw4MC4zNCw0NC0yOS43NywxNi4zLTgwLjM1LTQ0Wk0xMjgsMTIwLDQ3LjY2LDc2bDMzLjktMTguNTYsODAuMzQsNDRaTTQwLDkwbDgwLDQzLjc4djg1Ljc5TDQwLDE3NS44MlptMTc2LDg1Ljc4aDBsLTgwLDQzLjc5VjEzMy44MmwzMi0xNy41MVYxNTJhOCw4LDAsMCwwLDE2LDBWMTA3LjU1TDIxNiw5MHY4NS43N1oiPjwvcGF0aD48L3N2Zz4%3D&logoColor=FFFCF0&labelColor=100F0F&color=205EA6)](https://github.com/yee94/openchamber/releases/latest)
5
5
  [![Discord](https://img.shields.io/badge/Discord-join.svg?style=flat&labelColor=100F0F&color=8B7EC8&logo=discord&logoColor=FFFCF0)](https://discord.gg/ZYRSdnwwKA)
6
6
 
7
- Run [OpenCode](https://opencode.ai) in your browser. Install the CLI, open `localhost:3000`, done. Works on desktop browsers, tablets, and phones as a PWA.
7
+ Run [OpenCode](https://opencode.ai) in your browser. Install the `opencode` v2 CLI, open `localhost:3000`, done. Works on desktop browsers, tablets, and phones as a PWA.
8
8
 
9
9
  Full project overview, screenshots, and all features: [github.com/yee94/openchamber](https://github.com/yee94/openchamber)
10
10
 
@@ -16,10 +16,14 @@ curl -fsSL https://raw.githubusercontent.com/yee94/openchamber/main/scripts/inst
16
16
 
17
17
  Or install manually: `bun add -g @openchambery/web` (or npm, pnpm, yarn).
18
18
 
19
- > **Prerequisites:** [OpenCode CLI](https://opencode.ai) installed, Node.js 22+.
19
+ > **Prerequisites:** [`opencode`](https://opencode.ai/v2/docs/) v2 CLI installed (PATH or `OPENCODE_BINARY`; 1.x versions are rejected), Node.js 22+.
20
20
 
21
21
  ## Usage
22
22
 
23
+ By default, OpenChamber connects to the official shared OpenCode background service used by Desktop and TUI. Closing OpenChamber leaves OpenCode running, and the next launch reuses it. If the service cannot start, OpenChamber reports the failure for retry instead of launching a private fallback. In-app OpenCode upgrades install into OpenChamber's own cache and replace the shared service through the official service command; this also reconnects other clients using that service. User-installed CLI files are not overwritten.
24
+
25
+ Explicit `OPENCODE_HOST` / `OPENCODE_PORT` connections keep their existing behavior. `OPENCHAMBER_OPENCODE_SHARED_SERVICE=0` selects the private managed lifecycle when required by an existing deployment.
26
+
23
27
  ```bash
24
28
  openchamber # Start on port 3000
25
29
  openchamber --port 8080 # Custom port
@@ -33,7 +37,7 @@ openchamber connect-url --port 3000 # Add this server to OpenChamber Desktop
33
37
  openchamber connect-url --server http://host:3000 --qr
34
38
  openchamber connect-url --port 3000 --qr
35
39
  openchamber logs # Follow latest instance logs
36
- OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
40
+ OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external opencode v2 server
37
41
  OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
38
42
  openchamber stop # Stop server
39
43
  openchamber update # Update to latest version
@@ -109,6 +113,8 @@ Generating a client token does not automatically password-protect the hosted bro
109
113
  <details>
110
114
  <summary>Connect to external OpenCode server</summary>
111
115
 
116
+ The external host must be **OpenCode v2**. OpenChamber runs v2 health (`GET /api/info`) and V1 migration (`GET /api/experimental/migration/v1`) checks; a 1.x server will not pass. Automatic discovery uses the official `opencode` command only. `OPENCODE_BINARY` and `settings.opencodeBinary` can override the executable path; admission is checked by version.
117
+
112
118
  ```bash
113
119
  OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
114
120
  OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
@@ -116,8 +122,8 @@ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
116
122
 
117
123
  | Variable | Description |
118
124
  |----------|-------------|
119
- | `OPENCODE_HOST` | Full base URL of external server (overrides `OPENCODE_PORT`) |
120
- | `OPENCODE_PORT` | Port of external server |
125
+ | `OPENCODE_HOST` | Full base URL of external OpenCode v2 server (overrides `OPENCODE_PORT`) |
126
+ | `OPENCODE_PORT` | Port of external OpenCode v2 server |
121
127
  | `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server |
122
128
  | `OPENCHAMBER_OPENCODE_HOSTNAME` | Bind hostname for managed OpenCode server (default: `127.0.0.1`, use `0.0.0.0` for LAN/remote access — trusted networks only) |
123
129
  | `OPENCHAMBER_HOST` | Bind hostname for the OpenChamber web server (default: `127.0.0.1`; use `0.0.0.0` for LAN/remote access — trusted networks only) |
@@ -166,12 +172,12 @@ openchamber stop # Stop background server
166
172
  <details>
167
173
  <summary>systemd service (VPN / LAN access)</summary>
168
174
 
169
- Use `--foreground` to keep the CLI process alive so systemd (or any other process manager) can track and restart it. Combine with `OPENCODE_HOST` to connect to an OpenCode instance running as a separate service.
175
+ Use `--foreground` to keep the CLI process alive so systemd (or any other process manager) can track and restart it. Combine with `OPENCODE_HOST` to connect to an OpenCode v2 instance running as a separate service.
170
176
 
171
177
  **`~/.config/systemd/user/opencode.service`**
172
178
  ```ini
173
179
  [Unit]
174
- Description=OpenCode Server
180
+ Description=OpenCode 2 Server
175
181
 
176
182
  [Service]
177
183
  Type=simple
@@ -0,0 +1,208 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ import { checkOpenCodeCLI } from './cli.js';
7
+
8
+ function createTempDir(prefix) {
9
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
10
+ }
11
+
12
+ function writeVersionBinary(filePath, version) {
13
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
14
+ if (process.platform === 'win32') {
15
+ fs.writeFileSync(filePath, `@echo off\r\necho ${version}\r\n`);
16
+ return;
17
+ }
18
+ fs.writeFileSync(filePath, `#!${process.execPath}\nconsole.log(${JSON.stringify(version)});\n`);
19
+ fs.chmodSync(filePath, 0o755);
20
+ }
21
+
22
+ async function withIsolatedOpenCodeEnv(fn) {
23
+ const previousBinary = process.env.OPENCODE_BINARY;
24
+ const previousPath = process.env.PATH;
25
+ const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
26
+ const previousAutoInstall = process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL;
27
+ const dataDir = createTempDir('openchamber-cli-opencode-data-');
28
+ process.env.OPENCHAMBER_DATA_DIR = dataDir;
29
+ process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL = '0';
30
+ try {
31
+ return await fn({ dataDir });
32
+ } finally {
33
+ if (typeof previousBinary === 'string') {
34
+ process.env.OPENCODE_BINARY = previousBinary;
35
+ } else {
36
+ delete process.env.OPENCODE_BINARY;
37
+ }
38
+ if (typeof previousPath === 'string') {
39
+ process.env.PATH = previousPath;
40
+ } else {
41
+ delete process.env.PATH;
42
+ }
43
+ if (typeof previousDataDir === 'string') {
44
+ process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
45
+ } else {
46
+ delete process.env.OPENCHAMBER_DATA_DIR;
47
+ }
48
+ if (typeof previousAutoInstall === 'string') {
49
+ process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL = previousAutoInstall;
50
+ } else {
51
+ delete process.env.OPENCHAMBER_OPENCODE2_AUTO_INSTALL;
52
+ }
53
+ fs.rmSync(dataDir, { recursive: true, force: true });
54
+ }
55
+ }
56
+
57
+ describe('checkOpenCodeCLI v2 gate', () => {
58
+ it('uses the same binary validation for TTY, non-TTY, --quiet, and --json notice styles', async () => {
59
+ await withIsolatedOpenCodeEnv(async () => {
60
+ const dir = createTempDir('openchamber-cli-mode-parity-');
61
+ const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
62
+ writeVersionBinary(binary, '1.18.4');
63
+ process.env.OPENCODE_BINARY = binary;
64
+ const pathDir = createTempDir('openchamber-cli-mode-path-');
65
+ writeVersionBinary(path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2'), '2.0.12');
66
+ process.env.PATH = pathDir;
67
+
68
+ const notices = [];
69
+ const modes = [
70
+ { json: true, quiet: false },
71
+ { json: false, quiet: true },
72
+ { json: false, quiet: false },
73
+ ];
74
+ try {
75
+ for (const mode of modes) {
76
+ await expect(checkOpenCodeCLI((notice) => notices.push({ mode, notice }))).rejects.toMatchObject({
77
+ code: 'OPENCODE_BINARY_INVALID',
78
+ message: expect.stringMatching(/1\.x.*OpenCode v2/s),
79
+ });
80
+ }
81
+ expect(notices).toEqual([]);
82
+ } finally {
83
+ fs.rmSync(dir, { recursive: true, force: true });
84
+ fs.rmSync(pathDir, { recursive: true, force: true });
85
+ }
86
+ });
87
+ });
88
+
89
+ it('fails closed for opencode / opencode.exe / opencode.cmd and does not fall back to PATH', async () => {
90
+ await withIsolatedOpenCodeEnv(async () => {
91
+ const names = process.platform === 'win32'
92
+ ? ['opencode.exe', 'opencode.cmd']
93
+ : ['opencode', 'opencode.exe', 'opencode.cmd'];
94
+ const pathDir = createTempDir('openchamber-cli-legacy-path-');
95
+ const v2 = path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
96
+ writeVersionBinary(v2, '2.0.12');
97
+ process.env.PATH = pathDir;
98
+
99
+ try {
100
+ for (const name of names) {
101
+ const dir = createTempDir(`openchamber-cli-legacy-${name}-`);
102
+ const binary = path.join(dir, name);
103
+ writeVersionBinary(binary, '1.18.4');
104
+ process.env.OPENCODE_BINARY = binary;
105
+ try {
106
+ await expect(checkOpenCodeCLI()).rejects.toMatchObject({
107
+ code: 'OPENCODE_BINARY_INVALID',
108
+ message: expect.stringMatching(/1\.x.*OpenCode v2/s),
109
+ });
110
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
111
+ } finally {
112
+ fs.rmSync(dir, { recursive: true, force: true });
113
+ }
114
+ }
115
+ } finally {
116
+ fs.rmSync(pathDir, { recursive: true, force: true });
117
+ }
118
+ });
119
+ });
120
+
121
+ it('discovers official opencode from PATH and ignores the opencode2 alias', async () => {
122
+ await withIsolatedOpenCodeEnv(async () => {
123
+ const pathDir = createTempDir('openchamber-cli-path-opencode2-');
124
+ const legacy = path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
125
+ const binary = path.join(pathDir, process.platform === 'win32' ? 'opencode.cmd' : 'opencode');
126
+ writeVersionBinary(legacy, '2.0.15');
127
+ writeVersionBinary(binary, '2.0.12');
128
+ process.env.PATH = pathDir;
129
+ delete process.env.OPENCODE_BINARY;
130
+
131
+ try {
132
+ await expect(checkOpenCodeCLI()).resolves.toBe(binary);
133
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
134
+ } finally {
135
+ fs.rmSync(pathDir, { recursive: true, force: true });
136
+ }
137
+ });
138
+ });
139
+
140
+ it('fails closed when settings.opencodeBinary is a 1.x basename', async () => {
141
+ await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
142
+ const dir = createTempDir('openchamber-cli-settings-1x-');
143
+ const binary = path.join(dir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
144
+ writeVersionBinary(binary, '1.18.4');
145
+ fs.writeFileSync(path.join(dataDir, 'settings.json'), JSON.stringify({ opencodeBinary: binary }));
146
+ delete process.env.OPENCODE_BINARY;
147
+ const pathDir = createTempDir('openchamber-cli-settings-path-');
148
+ writeVersionBinary(path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2'), '2.0.12');
149
+ process.env.PATH = pathDir;
150
+ try {
151
+ await expect(checkOpenCodeCLI()).rejects.toMatchObject({
152
+ code: 'OPENCODE_BINARY_INVALID',
153
+ message: expect.stringMatching(/1\.x.*OpenCode v2/s),
154
+ });
155
+ } finally {
156
+ fs.rmSync(dir, { recursive: true, force: true });
157
+ fs.rmSync(pathDir, { recursive: true, force: true });
158
+ }
159
+ });
160
+ });
161
+
162
+ it('accepts a configured opencode2 path after the v2 version gate', async () => {
163
+ await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
164
+ const dir = createTempDir('openchamber-cli-settings-v2-');
165
+ const binary = path.join(dir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
166
+ writeVersionBinary(binary, '2.0.12');
167
+ fs.writeFileSync(path.join(dataDir, 'settings.json'), JSON.stringify({ opencodeBinary: binary }));
168
+ delete process.env.OPENCODE_BINARY;
169
+ process.env.PATH = createTempDir('openchamber-cli-settings-empty-');
170
+ try {
171
+ await expect(checkOpenCodeCLI()).resolves.toBe(binary);
172
+ expect(process.env.OPENCODE_BINARY).toBe(binary);
173
+ } finally {
174
+ fs.rmSync(dir, { recursive: true, force: true });
175
+ }
176
+ });
177
+ });
178
+
179
+ it('names opencode2 when PATH lookup finds nothing', async () => {
180
+ await withIsolatedOpenCodeEnv(async () => {
181
+ delete process.env.OPENCODE_BINARY;
182
+ process.env.PATH = createTempDir('openchamber-cli-missing-');
183
+ await expect(checkOpenCodeCLI()).rejects.toThrow(/opencode/);
184
+ });
185
+ });
186
+
187
+ it('installs pinned opencode2 when PATH has no usable binary', async () => {
188
+ await withIsolatedOpenCodeEnv(async ({ dataDir }) => {
189
+ delete process.env.OPENCODE_BINARY;
190
+ process.env.PATH = createTempDir('openchamber-cli-install-missing-');
191
+ const installed = path.join(dataDir, 'opencode-cli', '2.0.12', process.platform === 'win32' ? 'opencode2.exe' : 'opencode2');
192
+ writeVersionBinary(installed, '2.0.12');
193
+ const notices = [];
194
+ await expect(checkOpenCodeCLI((notice) => notices.push(notice), {
195
+ ensurePinnedOpenCode2Cli: async () => ({
196
+ path: installed,
197
+ version: '2.0.12',
198
+ source: 'installed',
199
+ installed: true,
200
+ }),
201
+ })).resolves.toBe(installed);
202
+ expect(process.env.OPENCODE_BINARY).toBe(installed);
203
+ expect(notices).toEqual([
204
+ expect.objectContaining({ code: 'OPENCODE_CLI_INSTALLED' }),
205
+ ]);
206
+ });
207
+ });
208
+ });
package/bin/cli.js CHANGED
@@ -18,7 +18,12 @@ import {
18
18
  findClosestMatch,
19
19
  } from './lib/cli-args.js';
20
20
  import { readDesktopLocalPortFromSettings } from './lib/cli-paths.js';
21
- import { resolveExplicitBinary, searchPathFor } from './lib/cli-executables.js';
21
+ import { searchPathFor } from './lib/cli-executables.js';
22
+ import {
23
+ assertOpenCode2Binary,
24
+ readConfiguredOpenCodeBinary,
25
+ } from './lib/cli-startup.js';
26
+ import { ensurePinnedOpenCode2Cli } from '../server/lib/opencode/ensure-cli.js';
22
27
  import { startupCommand } from './lib/commands-startup.js';
23
28
  import { logsCommand } from './lib/commands-logs.js';
24
29
  import { statusCommand } from './lib/commands-status.js';
@@ -75,14 +80,16 @@ function importFromFilePath(filePath) {
75
80
  return import(pathToFileURL(filePath).href);
76
81
  }
77
82
 
78
- async function checkOpenCodeCLI(onNotice) {
83
+ // Binary validation is policy, not presentation: TTY, non-TTY, --quiet, and
84
+ // --json all run this same check before serve starts.
85
+ async function checkOpenCodeCLI(onNotice, options = {}) {
79
86
  if (process.env.OPENCODE_BINARY) {
80
- const override = resolveExplicitBinary(process.env.OPENCODE_BINARY);
87
+ const override = assertOpenCode2Binary(process.env.OPENCODE_BINARY);
81
88
  if (override) {
82
89
  process.env.OPENCODE_BINARY = override;
83
90
  return override;
84
91
  }
85
- const message = `OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not an executable file. Falling back to PATH lookup.`;
92
+ const message = `OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not a usable OpenCode v2 CLI. Falling back to PATH lookup.`;
86
93
  if (typeof onNotice === 'function') {
87
94
  onNotice({ level: 'warning', code: 'OPENCODE_BINARY_INVALID', message });
88
95
  } else {
@@ -90,15 +97,60 @@ async function checkOpenCodeCLI(onNotice) {
90
97
  }
91
98
  }
92
99
 
93
- const resolvedFromPath = searchPathFor('opencode');
94
- if (resolvedFromPath) {
95
- process.env.OPENCODE_BINARY = resolvedFromPath;
96
- return resolvedFromPath;
100
+ const configured = readConfiguredOpenCodeBinary();
101
+ if (configured) {
102
+ const fromConfig = assertOpenCode2Binary(configured);
103
+ if (fromConfig) {
104
+ process.env.OPENCODE_BINARY = fromConfig;
105
+ return fromConfig;
106
+ }
107
+ }
108
+
109
+ let discovered = '';
110
+ for (const name of ['opencode']) {
111
+ const resolvedFromPath = searchPathFor(name);
112
+ if (!resolvedFromPath) continue;
113
+ try {
114
+ const verified = assertOpenCode2Binary(resolvedFromPath);
115
+ if (verified) {
116
+ discovered = verified;
117
+ break;
118
+ }
119
+ } catch (error) {
120
+ if (error?.code === 'OPENCODE_BINARY_INVALID') continue;
121
+ throw error;
122
+ }
123
+ }
124
+ const ensureCli = typeof options.ensurePinnedOpenCode2Cli === 'function'
125
+ ? options.ensurePinnedOpenCode2Cli
126
+ : ensurePinnedOpenCode2Cli;
127
+
128
+ try {
129
+ const ensured = await ensureCli({ discoveredPath: discovered });
130
+ if (ensured?.path) {
131
+ if (typeof onNotice === 'function' && ensured.installed) {
132
+ onNotice({
133
+ level: 'info',
134
+ code: 'OPENCODE_CLI_INSTALLED',
135
+ message: `Installed opencode ${ensured.version} to ${ensured.path}`,
136
+ });
137
+ }
138
+ process.env.OPENCODE_BINARY = ensured.path;
139
+ return ensured.path;
140
+ }
141
+ } catch (error) {
142
+ if (error?.code === 'OPENCODE_CLI_MISSING') {
143
+ throw new Error(
144
+ `Unable to locate the opencode CLI on PATH (${process.env.PATH || '<empty>'}). ` +
145
+ 'Ensure OpenCode v2 is installed and reachable, or set OPENCODE_BINARY to its full path.'
146
+ );
147
+ }
148
+ throw error;
97
149
  }
98
150
 
99
151
  throw new Error(
100
152
  `Unable to locate the opencode CLI on PATH (${process.env.PATH || '<empty>'}). ` +
101
- 'Ensure the CLI is installed and reachable, or set OPENCODE_BINARY to its full path.'
153
+ 'Ensure OpenCode v2 is installed and reachable, or set OPENCODE_BINARY to its full path.'
102
154
  );
103
155
  }
104
156
 
@@ -304,4 +356,5 @@ export {
304
356
  findClosestMatch,
305
357
  TunnelCliError,
306
358
  EXIT_CODE,
359
+ checkOpenCodeCLI,
307
360
  };
@@ -6,10 +6,107 @@ import { DEFAULT_PORT } from './cli-args.js';
6
6
  import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
7
7
  import { getDataDir } from './cli-paths.js';
8
8
  import { hasUiPasswordConfigured } from './cli-network.js';
9
- import { searchPathFor } from './cli-executables.js';
9
+ import { resolveExplicitBinary, searchPathFor } from './cli-executables.js';
10
+ import { isAcceptableOpenCode2HealthVersion, isOpenCode1xVersion } from '../../server/lib/opencode/opencode2-pin.js';
10
11
 
11
12
  const STARTUP_SERVICE_ID = 'dev.openchamber.web';
12
13
 
14
+ function createOpenCode1xVersionError(candidate, version) {
15
+ const error = new Error(
16
+ `OpenCode 1.x is not supported (${candidate} reports ${version}). Install OpenCode v2 (opencode).`
17
+ );
18
+ error.code = 'OPENCODE_BINARY_INVALID';
19
+ return error;
20
+ }
21
+
22
+ function extractOpenCodeVersion(output) {
23
+ if (typeof output !== 'string' || output.trim().length === 0) {
24
+ return '';
25
+ }
26
+ const match = output.match(/v?\d+\.\d+[\w.+-]*/i);
27
+ return match ? match[0] : '';
28
+ }
29
+
30
+ function readOpenCodeBinaryVersion(binaryPath) {
31
+ const result = spawnSync(binaryPath, ['--version'], {
32
+ encoding: 'utf8',
33
+ timeout: 8000,
34
+ windowsHide: true,
35
+ });
36
+ return `${result.stdout || ''}\n${result.stderr || ''}`.trim();
37
+ }
38
+
39
+ // OPENCODE_BINARY and settings.opencodeBinary share the v2 version gate.
40
+ function assertOpenCode2Binary(candidate) {
41
+ const trimmed = typeof candidate === 'string' ? candidate.trim() : '';
42
+ if (!trimmed) {
43
+ return null;
44
+ }
45
+ const resolved = resolveExplicitBinary(trimmed) || trimmed;
46
+ const version = extractOpenCodeVersion(readOpenCodeBinaryVersion(resolved));
47
+ if (isOpenCode1xVersion(version)) {
48
+ throw createOpenCode1xVersionError(resolved, version);
49
+ }
50
+ if (!isAcceptableOpenCode2HealthVersion(version)) {
51
+ return null;
52
+ }
53
+ return resolveExplicitBinary(trimmed);
54
+ }
55
+
56
+ function readConfiguredOpenCodeBinary() {
57
+ try {
58
+ const raw = fs.readFileSync(path.join(getDataDir(), 'settings.json'), 'utf8');
59
+ const parsed = JSON.parse(raw);
60
+ if (typeof parsed?.opencodeBinary !== 'string') {
61
+ return '';
62
+ }
63
+ const trimmed = parsed.opencodeBinary.trim();
64
+ if (!trimmed) {
65
+ return '';
66
+ }
67
+ try {
68
+ if (fs.statSync(trimmed).isDirectory()) {
69
+ return path.join(trimmed, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
70
+ }
71
+ } catch {
72
+ }
73
+ return trimmed;
74
+ } catch {
75
+ return '';
76
+ }
77
+ }
78
+
79
+ function resolveOpenCode2BinaryForStartup() {
80
+ const explicit = typeof process.env.OPENCODE_BINARY === 'string' ? process.env.OPENCODE_BINARY.trim() : '';
81
+ if (explicit) {
82
+ const resolved = assertOpenCode2Binary(explicit);
83
+ if (resolved) {
84
+ return resolved;
85
+ }
86
+ }
87
+
88
+ const configured = readConfiguredOpenCodeBinary();
89
+ if (configured) {
90
+ const resolved = assertOpenCode2Binary(configured);
91
+ if (resolved) {
92
+ return resolved;
93
+ }
94
+ }
95
+
96
+ for (const name of ['opencode']) {
97
+ const resolvedFromPath = searchPathFor(name);
98
+ if (!resolvedFromPath) continue;
99
+ try {
100
+ const resolved = assertOpenCode2Binary(resolvedFromPath);
101
+ if (resolved) return resolved;
102
+ } catch (error) {
103
+ if (error?.code === 'OPENCODE_BINARY_INVALID') continue;
104
+ throw error;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
13
110
  function getStartupServicePaths() {
14
111
  if (process.platform === 'darwin') {
15
112
  return {
@@ -82,9 +179,11 @@ function collectStartupEnv(options = {}) {
82
179
  );
83
180
 
84
181
  if (options.envSnapshot !== false) {
85
- const opencodeBinary = process.env.OPENCODE_BINARY || searchPathFor('opencode');
182
+ const opencodeBinary = resolveOpenCode2BinaryForStartup();
86
183
  if (typeof opencodeBinary === 'string' && opencodeBinary.trim().length > 0) {
87
184
  env.OPENCODE_BINARY = opencodeBinary.trim();
185
+ } else {
186
+ delete env.OPENCODE_BINARY;
88
187
  }
89
188
  }
90
189
  const uiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
@@ -367,4 +466,8 @@ export {
367
466
  getStartupStatus,
368
467
  enableStartupService,
369
468
  disableStartupService,
469
+ collectStartupEnv,
470
+ assertOpenCode2Binary,
471
+ readConfiguredOpenCodeBinary,
472
+ resolveOpenCode2BinaryForStartup,
370
473
  };