@openchambery/web 1.19.21-beta.1 → 2.0.0-beta.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 (195) hide show
  1. package/README.md +13 -11
  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 +165 -0
  6. package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
  7. package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
  8. package/dist/assets/{DiagramView-Bh06xinl.js → DiagramView-xuSJTnv7.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
  10. package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
  11. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
  13. package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
  14. package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
  15. package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
  16. package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
  17. package/dist/assets/{MultiRunWindow-CjCw1X4n.js → MultiRunWindow-ChZGob0v.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
  19. package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
  20. package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
  21. package/dist/assets/{SettingsWindow-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
  22. package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
  24. package/dist/assets/{appThemeRegistry-BKc-qXbQ.js → appThemeRegistry-COGfxTFV.js} +1 -1
  25. package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
  26. package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
  27. package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
  28. package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
  29. package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
  30. package/dist/assets/es-c52LTHHe.js +15 -0
  31. package/dist/assets/{fileMentionTouchSelection-BTkP7aYI.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
  32. package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
  33. package/dist/assets/gitApi-ButwzQ0N.js +9 -0
  34. package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
  35. package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
  36. package/dist/assets/ja-C8Ikun3Z.js +15 -0
  37. package/dist/assets/ko-ClFys5o2.js +15 -0
  38. package/dist/assets/main-CCE_4y5F.js +2 -0
  39. package/dist/assets/main-Dn9rpsgl.js +169 -0
  40. package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
  41. package/dist/assets/miniChat-BpjxVqgg.js +2 -0
  42. package/dist/assets/mobile-nMJCaNq1.js +2 -0
  43. package/dist/assets/multirun-BvSG94mW.js +4 -0
  44. package/dist/assets/pl-CnaKWN5F.js +15 -0
  45. package/dist/assets/platform-CWTIp02q.js +4 -0
  46. package/dist/assets/projectMeta-DOe49dM_.js +3 -0
  47. package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
  48. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
  49. package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
  50. package/dist/assets/{runtimeConfig-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
  51. package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
  52. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
  53. package/dist/assets/{sessionLookup-CbbzalcH.js → sessionLookup-gh-hVRIc.js} +1 -1
  54. package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
  55. package/dist/assets/src-BzX5UI-c.css +2 -0
  56. package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
  57. package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
  58. package/dist/assets/textarea-3XR8sUfy.js +1 -0
  59. package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
  60. package/dist/assets/uk-CaX0iqkB.js +15 -0
  61. package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
  62. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
  63. package/dist/assets/{useEffectiveDirectory-CzHynoi_.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
  64. package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
  66. package/dist/assets/{useSessionAutoCleanup-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
  67. package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
  68. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
  69. package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
  70. package/dist/assets/zh-CN-20MYEjha.js +15 -0
  71. package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
  72. package/dist/index.html +6 -6
  73. package/dist/mini-chat.html +6 -6
  74. package/dist/mobile.html +6 -6
  75. package/package.json +2 -2
  76. package/server/index.js +107 -0
  77. package/server/lib/assistants/DOCUMENTATION.md +18 -18
  78. package/server/lib/assistants/assign.js +2 -1
  79. package/server/lib/assistants/contact-store.js +37 -19
  80. package/server/lib/assistants/projection-history.test.js +207 -0
  81. package/server/lib/assistants/routes.js +5 -5
  82. package/server/lib/assistants/service.js +493 -123
  83. package/server/lib/assistants/service.test.js +203 -133
  84. package/server/lib/conversations/DOCUMENTATION.md +4 -4
  85. package/server/lib/conversations/service.js +86 -57
  86. package/server/lib/conversations/service.test.js +116 -100
  87. package/server/lib/event-stream/global-hub.js +2 -1
  88. package/server/lib/llm/DOCUMENTATION.md +71 -84
  89. package/server/lib/llm/catalog.js +111 -45
  90. package/server/lib/llm/catalog.test.js +121 -59
  91. package/server/lib/llm/completions.js +37 -12
  92. package/server/lib/llm/completions.test.js +45 -4
  93. package/server/lib/llm/generate.js +421 -450
  94. package/server/lib/llm/generate.test.js +553 -487
  95. package/server/lib/llm/routes.js +6 -4
  96. package/server/lib/message-queue/DOCUMENTATION.md +5 -5
  97. package/server/lib/message-queue/opencode-adapter.js +191 -104
  98. package/server/lib/message-queue/opencode-adapter.test.js +328 -58
  99. package/server/lib/message-queue/runtime.test.js +6 -6
  100. package/server/lib/message-queue/worker.js +7 -1
  101. package/server/lib/opencode/DOCUMENTATION.md +28 -5
  102. package/server/lib/opencode/config-entity-routes.js +84 -13
  103. package/server/lib/opencode/config-entity-routes.test.js +41 -10
  104. package/server/lib/opencode/ensure-cli.js +239 -0
  105. package/server/lib/opencode/ensure-cli.test.js +119 -0
  106. package/server/lib/opencode/env-runtime.js +193 -110
  107. package/server/lib/opencode/env-runtime.test.js +221 -17
  108. package/server/lib/opencode/feature-routes-runtime.js +37 -1
  109. package/server/lib/opencode/lifecycle.js +204 -70
  110. package/server/lib/opencode/lifecycle.test.js +376 -6
  111. package/server/lib/opencode/network-runtime.js +28 -16
  112. package/server/lib/opencode/network-runtime.test.js +68 -4
  113. package/server/lib/opencode/opencode2-pin.js +139 -0
  114. package/server/lib/opencode/opencode2-pin.test.js +87 -0
  115. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
  116. package/server/lib/opencode/plugin-compatibility.js +15 -0
  117. package/server/lib/opencode/plugin-compatibility.test.js +21 -0
  118. package/server/lib/opencode/plugins.js +4 -3
  119. package/server/lib/opencode/plugins.test.js +12 -0
  120. package/server/lib/opencode/proxy.js +153 -16
  121. package/server/lib/opencode/proxy.test.js +120 -0
  122. package/server/lib/opencode/routes.js +25 -32
  123. package/server/lib/opencode/routes.test.js +51 -52
  124. package/server/lib/opencode/server-utils-runtime.js +2 -0
  125. package/server/lib/opencode/skill-routes.js +6 -7
  126. package/server/lib/opencode/skill-routes.test.js +36 -5
  127. package/server/lib/opencode/v1-migration-gate.js +157 -0
  128. package/server/lib/opencode/v1-migration-gate.test.js +111 -0
  129. package/server/lib/opencode/v2-client.js +49 -0
  130. package/server/lib/opencode/watcher.js +2 -1
  131. package/server/lib/opencode/watcher.test.js +1 -1
  132. package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
  133. package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
  134. package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
  135. package/server/lib/scheduled-tasks/routes.js +27 -0
  136. package/server/lib/scheduled-tasks/routes.test.js +94 -0
  137. package/server/lib/scheduled-tasks/runtime.js +331 -308
  138. package/server/lib/scheduled-tasks/runtime.test.js +530 -318
  139. package/server/lib/session-goal/DOCUMENTATION.md +65 -14
  140. package/server/lib/session-goal/capability.js +57 -0
  141. package/server/lib/session-goal/capability.test.js +33 -0
  142. package/server/lib/session-goal/routes.js +231 -2
  143. package/server/lib/session-goal/routes.test.js +211 -0
  144. package/server/lib/session-goal/runtime.js +46 -4
  145. package/server/lib/session-goal/runtime.test.js +62 -1
  146. package/server/lib/session-index/sync-runtime.js +20 -17
  147. package/server/lib/session-index/sync-runtime.test.js +9 -8
  148. package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
  149. package/server/lib/session-metadata/routes.js +94 -0
  150. package/server/lib/session-metadata/routes.test.js +111 -0
  151. package/server/lib/session-metadata/session-metadata-store.js +235 -0
  152. package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
  153. package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
  154. package/server/lib/session-turn-pages/reconcile.service.js +1 -1
  155. package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
  156. package/server/lib/session-turn-pages/routes.js +204 -71
  157. package/server/lib/session-turn-pages/routes.test.js +285 -0
  158. package/server/lib/session-turn-pages/service.js +6 -2
  159. package/server/lib/session-turn-pages/service.test.js +34 -0
  160. package/server/lib/small-model/DOCUMENTATION.md +5 -4
  161. package/server/lib/small-model/catalog.js +56 -6
  162. package/server/lib/small-model/catalog.test.js +47 -0
  163. package/server/lib/small-model/index.js +3 -3
  164. package/server/lib/small-model/opencode-session.js +50 -206
  165. package/server/lib/small-model/opencode-session.test.js +58 -88
  166. package/server/opencode-proxy.test.js +61 -12
  167. package/dist/assets/AssistantView-BtGYjSW5.js +0 -3
  168. package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
  169. package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
  170. package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
  171. package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
  172. package/dist/assets/es-rqMiRP7-.js +0 -15
  173. package/dist/assets/gitApi-CcIuekQ3.js +0 -7
  174. package/dist/assets/insertionBoundaries-BocNNP9Q.js +0 -1
  175. package/dist/assets/ja-Bgr8-dOg.js +0 -15
  176. package/dist/assets/ko-B_Gxh-0l.js +0 -15
  177. package/dist/assets/main-CFLmpNC5.js +0 -169
  178. package/dist/assets/main-Ckj_IpUy.js +0 -2
  179. package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
  180. package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
  181. package/dist/assets/multirun-BGUKIJys.js +0 -4
  182. package/dist/assets/pl-DW2XJGC4.js +0 -15
  183. package/dist/assets/platform-0w_Q9_ji.js +0 -2
  184. package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
  185. package/dist/assets/pt-BR-CIsanf4T.js +0 -15
  186. package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
  187. package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
  188. package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
  189. package/dist/assets/src-rS3zCcsC.css +0 -2
  190. package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
  191. package/dist/assets/uk-BPmPEUYJ.js +0 -15
  192. package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
  193. package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
  194. package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
  195. package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
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 `opencode2` 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,7 +16,7 @@ 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:** [`opencode2`](https://opencode.ai) CLI installed (PATH or `OPENCODE_BINARY`; `opencode` / `opencode.exe` / `opencode.cmd` are rejected), Node.js 22+.
20
20
 
21
21
  ## Usage
22
22
 
@@ -33,7 +33,7 @@ openchamber connect-url --port 3000 # Add this server to OpenChamber Desktop
33
33
  openchamber connect-url --server http://host:3000 --qr
34
34
  openchamber connect-url --port 3000 --qr
35
35
  openchamber logs # Follow latest instance logs
36
- OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external OpenCode server
36
+ OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber # Connect to external opencode2 server
37
37
  OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber # Connect via custom host/HTTPS
38
38
  openchamber stop # Stop server
39
39
  openchamber update # Update to latest version
@@ -109,6 +109,8 @@ Generating a client token does not automatically password-protect the hosted bro
109
109
  <details>
110
110
  <summary>Connect to external OpenCode server</summary>
111
111
 
112
+ The external host must be **opencode2**. OpenChamber still runs v2 health (`GET /api/health`) and V1 migration (`GET /api/experimental/migration/v1`) checks; a 1.x `opencode` server will not pass. `OPENCODE_BINARY` and `settings.opencodeBinary` must resolve to `opencode2`, not `opencode` / `opencode.exe` / `opencode.cmd`.
113
+
112
114
  ```bash
113
115
  OPENCODE_PORT=4096 OPENCODE_SKIP_START=true openchamber
114
116
  OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
@@ -116,8 +118,8 @@ OPENCODE_HOST=https://myhost:4096 OPENCODE_SKIP_START=true openchamber
116
118
 
117
119
  | Variable | Description |
118
120
  |----------|-------------|
119
- | `OPENCODE_HOST` | Full base URL of external server (overrides `OPENCODE_PORT`) |
120
- | `OPENCODE_PORT` | Port of external server |
121
+ | `OPENCODE_HOST` | Full base URL of external opencode2 server (overrides `OPENCODE_PORT`) |
122
+ | `OPENCODE_PORT` | Port of external opencode2 server |
121
123
  | `OPENCODE_SKIP_START` | Skip starting embedded OpenCode server |
122
124
  | `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
125
  | `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,16 +168,16 @@ openchamber stop # Stop background server
166
168
  <details>
167
169
  <summary>systemd service (VPN / LAN access)</summary>
168
170
 
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.
171
+ 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 opencode2 instance running as a separate service.
170
172
 
171
- **`~/.config/systemd/user/opencode.service`**
173
+ **`~/.config/systemd/user/opencode2.service`**
172
174
  ```ini
173
175
  [Unit]
174
- Description=OpenCode Server
176
+ Description=OpenCode 2 Server
175
177
 
176
178
  [Service]
177
179
  Type=simple
178
- ExecStart=opencode serve --port 4095
180
+ ExecStart=opencode2 serve --port 4095
179
181
  Environment="PATH=/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/YOU/.local/bin:/home/YOU/.npm-global/bin:/usr/local/bin:/usr/bin:/bin"
180
182
  Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
181
183
  Restart=on-failure
@@ -195,7 +197,7 @@ WantedBy=default.target
195
197
  ```ini
196
198
  [Unit]
197
199
  Description=OpenChamber Web Server
198
- After=opencode.service
200
+ After=opencode2.service
199
201
 
200
202
  [Service]
201
203
  Type=simple
@@ -211,7 +213,7 @@ WantedBy=default.target
211
213
 
212
214
  ```bash
213
215
  systemctl --user daemon-reload
214
- systemctl --user enable --now opencode openchamber
216
+ systemctl --user enable --now opencode2 openchamber
215
217
  ```
216
218
 
217
219
  `--host 0.0.0.0` is required to listen on all interfaces (the default is `127.0.0.1`). Use `--host <ip>` or `OPENCHAMBER_HOST=<ip>` to bind to a specific interface instead.
@@ -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 opencode2 from PATH and ignores a sibling 1.x opencode', async () => {
122
+ await withIsolatedOpenCodeEnv(async () => {
123
+ const pathDir = createTempDir('openchamber-cli-path-opencode2-');
124
+ const legacy = path.join(pathDir, process.platform === 'win32' ? 'opencode.exe' : 'opencode');
125
+ const binary = path.join(pathDir, process.platform === 'win32' ? 'opencode2.cmd' : 'opencode2');
126
+ writeVersionBinary(legacy, '1.18.4');
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', 'opencode2']) {
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', 'opencode2']) {
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
  };