@openchambery/web 2.0.0-beta.1 → 2.0.0-beta.4

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 (287) hide show
  1. package/README.md +15 -11
  2. package/bin/cli-opencode.test.js +4 -4
  3. package/bin/cli.js +1 -1
  4. package/bin/lib/cli-startup.js +1 -1
  5. package/bin/lib/cli-startup.test.js +18 -4
  6. package/dist/assets/{AgentAvatar-CLWWNu_Q.js → AgentAvatar-Q1SmFRx2.js} +1 -1
  7. package/dist/assets/AssistantView-CwXkiCAd.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-BW92schO.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-BZmwl-zE.js} +1 -1
  10. package/dist/assets/{Icon-DD-BuMfZ.js → Icon-AQPvxMiU.js} +1 -1
  11. package/dist/assets/{MarkdownRenderer-bc_p1H7l.js → MarkdownRenderer-Bv05iDZZ.js} +2 -2
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-C81xqYib.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-CXUCQuKJ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-BMgIu7zc.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-BOXGNHmC.js} +2 -2
  16. package/dist/assets/MobileSurface-xuc_z0_G.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-B5At2Z95.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-bsPLgz4r.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-4wp4migh.js +2 -0
  20. package/dist/assets/SettingsView-CJ6tcH5_.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-CMtX2bMl.js} +1 -1
  22. package/dist/assets/{TerminalView-Dh9MWCpS.js → TerminalView-Dvr-ysTS.js} +1 -1
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-uH1ZZDSS.js} +2 -2
  24. package/dist/assets/appThemeRegistry-DK7q2juD.js +1 -0
  25. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
  26. package/dist/assets/{button-D-rATN7w.js → button-CcNJUxUd.js} +1 -1
  27. package/dist/assets/collapsible-ZmZ6rLWs.js +3 -0
  28. package/dist/assets/context-menu-B2LXJgXL.js +1 -0
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-C6HrP0jq.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-DdVBp45U.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-CxTs9hCQ.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-KMVL9QKF.js} +1 -1
  33. package/dist/assets/es-CN3E_zXj.js +15 -0
  34. package/dist/assets/{fileMentionTouchSelection-BvrNCroG.js → fileMentionTouchSelection-7RIwlpkB.js} +1 -1
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-CjdBRy8y.js} +1 -1
  36. package/dist/assets/fr-9zZvQZD2.js +13 -0
  37. package/dist/assets/gitApi-yuWg9BN4.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-9vQBq3zp.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-CrIvk3HZ.js} +1 -1
  40. package/dist/assets/insertionBoundaries-BiUCSkx-.js +1 -0
  41. package/dist/assets/ja-Dnjk_82N.js +15 -0
  42. package/dist/assets/ko-BxCEE99C.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-C9mIBo77.js} +2 -2
  44. package/dist/assets/main-DiL6w6KF.js +169 -0
  45. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  46. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-BmXVY_gc.js} +1 -1
  47. package/dist/assets/{markdownTheme-DOfGg-A7.js → markdownTheme-CKac3wKC.js} +1 -1
  48. package/dist/assets/mcpQueries-C7jknHV8.js +1 -0
  49. package/dist/assets/miniChat-CwtUaElU.js +2 -0
  50. package/dist/assets/mobile-DB-6--ws.js +2 -0
  51. package/dist/assets/multirun-TAsxFI8e.js +4 -0
  52. package/dist/assets/{persistence-Ce3AnpEr.js → persistence-56kq8Z4V.js} +2 -2
  53. package/dist/assets/pl-CNNsWnPM.js +15 -0
  54. package/dist/assets/platform-C13fMVEs.js +6 -0
  55. package/dist/assets/pt-BR-BZB2DJMp.js +15 -0
  56. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  57. package/dist/assets/renderElectronMiniChatApp-CGj0MNRU.js +2 -0
  58. package/dist/assets/renderMobileApp-CGMMqbsO.js +28 -0
  59. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  60. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-BEANkvOd.js} +1 -1
  61. package/dist/assets/runtimeEndpointReset-A_ZBUJFS.js +147 -0
  62. package/dist/assets/sessionChangeSummary-qOLrP9fw.js +2 -0
  63. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-34XFmP9C.js} +1 -1
  64. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-DyEV0Heg.js} +1 -1
  65. package/dist/assets/src-ubCWXZhR.css +2 -0
  66. package/dist/assets/store-DLCiZaA6.js +15 -0
  67. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-CXaBEoa_.js} +1 -1
  68. package/dist/assets/textarea-BBOlRI9O.js +1 -0
  69. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-CPI4fr1a.js} +1 -1
  70. package/dist/assets/uk-rwQrxeYM.js +15 -0
  71. package/dist/assets/{url-_z7evzdK.js → url-DRDQXjyX.js} +1 -1
  72. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-CiDe42I5.js} +57 -43
  73. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-asNG6K7n.js} +1 -1
  74. package/dist/assets/useDesktopWindowControlsLayout-BlizAlOt.js +1 -0
  75. package/dist/assets/useEffectiveDirectory-C_CouL5v.js +1 -0
  76. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-B0oUZAiB.js} +1 -1
  77. package/dist/assets/useMobileNavigationStore-DfPClaW-.js +414 -0
  78. package/dist/assets/{useRuntimeAPIs-DSLXJybW.js → useRuntimeAPIs-DwLtO0c_.js} +1 -1
  79. package/dist/assets/useSessionAutoCleanup-DtrfbxAp.js +2 -0
  80. package/dist/assets/{useThemeSystem-DMzWVJaW.js → useThemeSystem-BlNUWH47.js} +1 -1
  81. package/dist/assets/useUIStore-CQYKJnrg.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-CPNYz6Wf.js} +2 -2
  83. package/dist/assets/{vendor-base-ui-CnHfUmDN.js → vendor-base-ui-CDjyofeD.js} +1 -1
  84. package/dist/assets/{vendor-cmdk-CeAGfrLo.js → vendor-cmdk-BFH0UpXX.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-accessibility-BY5LhhUB.js → vendor-dnd-kit-accessibility-BrR3AKvM.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-core-CvWxUPdb.js → vendor-dnd-kit-core-CNfdvk6H.js} +1 -1
  87. package/dist/assets/{vendor-dnd-kit-sortable-hHdP9lLb.js → vendor-dnd-kit-sortable-DQUaTeGj.js} +1 -1
  88. package/dist/assets/{vendor-framer-motion-DFy_BSVd.js → vendor-framer-motion-4dogsUk3.js} +1 -1
  89. package/dist/assets/{vendor-legendapp-list-WE32lwNk.js → vendor-legendapp-list-BU7MdPZR.js} +1 -1
  90. package/dist/assets/{vendor-markstream-react-DX55XLGO.js → vendor-markstream-react-qHC2FykU.js} +3 -3
  91. package/dist/assets/{vendor-pierre-diffs-DX7XTPG3.js → vendor-pierre-diffs-CUfqC003.js} +1 -1
  92. package/dist/assets/{vendor-react-DLPElpoQ.js → vendor-react-C94A0AHl.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-ECtEwvbc.js → vendor-react-drawio-FVlWqKVE.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-C2A3EjUi.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DVzioF3d.js → vendor-sonner-DNqHmUQs.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-DWy3xSmp.js → vendor-stream-diffs-CwbKsmlv.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D7pys_c9.js → vendor-tanstack-react-query-D0iFK57C.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-tlDgH6PG.js → vendor-tanstack-react-virtual-B2kV6cnT.js} +1 -1
  99. package/dist/assets/{vendor-zustand-B4dlGEM2.js → vendor-zustand-BqLKf6ZY.js} +1 -1
  100. package/dist/assets/zh-CN-BvmBDnkD.js +15 -0
  101. package/dist/assets/zh-TW-9wcHTX0y.js +15 -0
  102. package/dist/index.html +13 -13
  103. package/dist/mini-chat.html +13 -13
  104. package/dist/mobile.html +13 -13
  105. package/package.json +1 -1
  106. package/server/index.js +327 -62
  107. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  108. package/server/lib/assistants/contracts.js +11 -1
  109. package/server/lib/assistants/projection-history.test.js +265 -18
  110. package/server/lib/assistants/routes.js +6 -0
  111. package/server/lib/assistants/service.js +417 -231
  112. package/server/lib/assistants/service.test.js +392 -148
  113. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  114. package/server/lib/conversations/service.js +1 -0
  115. package/server/lib/conversations/service.test.js +16 -0
  116. package/server/lib/conversations/validation.js +11 -1
  117. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  118. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  119. package/server/lib/event-stream/global-hub.js +49 -4
  120. package/server/lib/event-stream/reasoning-projection.js +16 -1
  121. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  122. package/server/lib/event-stream/runtime.js +57 -20
  123. package/server/lib/event-stream/runtime.test.js +112 -0
  124. package/server/lib/llm/DOCUMENTATION.md +33 -9
  125. package/server/lib/llm/catalog.js +1 -1
  126. package/server/lib/llm/completions.js +5 -1
  127. package/server/lib/llm/completions.test.js +3 -3
  128. package/server/lib/llm/generate.js +237 -145
  129. package/server/lib/llm/generate.test.js +201 -46
  130. package/server/lib/llm/routes.js +2 -0
  131. package/server/lib/message-queue/runtime.js +15 -2
  132. package/server/lib/notifications/runtime.js +23 -5
  133. package/server/lib/notifications/runtime.test.js +11 -0
  134. package/server/lib/opencode/DOCUMENTATION.md +85 -17
  135. package/server/lib/opencode/agent-document.d.ts +28 -0
  136. package/server/lib/opencode/agent-document.js +280 -0
  137. package/server/lib/opencode/agent-document.test.js +137 -0
  138. package/server/lib/opencode/agents.js +96 -0
  139. package/server/lib/opencode/auth-state-runtime.js +5 -0
  140. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  141. package/server/lib/opencode/config-entity-routes.js +42 -99
  142. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  143. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  144. package/server/lib/opencode/ensure-cli.js +1 -2
  145. package/server/lib/opencode/env-runtime.js +40 -26
  146. package/server/lib/opencode/env-runtime.test.js +10 -17
  147. package/server/lib/opencode/feature-routes-runtime.js +43 -4
  148. package/server/lib/opencode/feature-routes-runtime.test.js +18 -0
  149. package/server/lib/opencode/http-server-connections.js +98 -0
  150. package/server/lib/opencode/http-server-connections.test.js +321 -0
  151. package/server/lib/opencode/lifecycle.js +349 -39
  152. package/server/lib/opencode/lifecycle.test.js +322 -0
  153. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  154. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  155. package/server/lib/opencode/openchamber-routes.js +7 -1
  156. package/server/lib/opencode/opencode2-pin.js +4 -4
  157. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  158. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  159. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  160. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  161. package/server/lib/opencode/plugin-routes.js +3 -18
  162. package/server/lib/opencode/plugin-routes.test.js +9 -9
  163. package/server/lib/opencode/provider-catalog.js +58 -12
  164. package/server/lib/opencode/provider-catalog.test.js +48 -0
  165. package/server/lib/opencode/proxy.js +252 -46
  166. package/server/lib/opencode/proxy.test.js +235 -2
  167. package/server/lib/opencode/routes.js +296 -62
  168. package/server/lib/opencode/routes.test.js +278 -12
  169. package/server/lib/opencode/runtime-contract.js +401 -0
  170. package/server/lib/opencode/runtime-contract.test.js +214 -0
  171. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  172. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  173. package/server/lib/opencode/server-utils-runtime.js +33 -0
  174. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  175. package/server/lib/opencode/shared-service.js +147 -0
  176. package/server/lib/opencode/shared-service.test.js +93 -0
  177. package/server/lib/opencode/shared.js +3 -3
  178. package/server/lib/opencode/shutdown-runtime.js +15 -6
  179. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  180. package/server/lib/opencode/skill-routes.js +20 -33
  181. package/server/lib/opencode/skill-routes.test.js +35 -2
  182. package/server/lib/opencode/skills.test.js +15 -0
  183. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  184. package/server/lib/opencode/static-routes-runtime.js +3 -1
  185. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  186. package/server/lib/opencode/v2-client.js +14 -3
  187. package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
  188. package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
  189. package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
  190. package/server/lib/permission-auto-accept/runtime.js +14 -7
  191. package/server/lib/permission-auto-accept/runtime.test.js +27 -12
  192. package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
  193. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -0
  194. package/server/lib/scheduled-tasks/runtime.js +52 -4
  195. package/server/lib/scheduled-tasks/runtime.test.js +80 -1
  196. package/server/lib/session-goal/DOCUMENTATION.md +113 -13
  197. package/server/lib/session-goal/routes.js +9 -0
  198. package/server/lib/session-goal/runtime.js +1579 -117
  199. package/server/lib/session-goal/runtime.test.js +1453 -84
  200. package/server/lib/session-index/DOCUMENTATION.md +18 -15
  201. package/server/lib/session-index/event-ingest.js +89 -16
  202. package/server/lib/session-index/event-ingest.test.js +73 -0
  203. package/server/lib/session-index/host-authority-snapshot.js +92 -0
  204. package/server/lib/session-index/routes.host-archive.test.js +201 -0
  205. package/server/lib/session-index/routes.js +63 -10
  206. package/server/lib/session-index/routes.test.js +8 -8
  207. package/server/lib/session-index/service.js +7 -1
  208. package/server/lib/session-index/sync-runtime.js +161 -34
  209. package/server/lib/session-index/sync-runtime.test.js +155 -1
  210. package/server/lib/session-metadata/DOCUMENTATION.md +61 -16
  211. package/server/lib/session-metadata/routes.js +108 -9
  212. package/server/lib/session-metadata/routes.test.js +47 -2
  213. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  214. package/server/lib/session-metadata/session-archive.js +654 -0
  215. package/server/lib/session-metadata/session-archive.test.js +359 -0
  216. package/server/lib/session-metadata/session-metadata-store.d.ts +36 -0
  217. package/server/lib/session-metadata/session-metadata-store.js +212 -72
  218. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  219. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  220. package/server/lib/session-metadata/session-projection.js +255 -0
  221. package/server/lib/session-metadata/session-projection.test.js +64 -0
  222. package/server/lib/session-metadata/system-session.js +55 -0
  223. package/server/lib/session-metadata/system-session.test.js +55 -0
  224. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  225. package/server/lib/session-title/native-runtime.test.js +167 -0
  226. package/server/lib/session-title/runtime.js +67 -42
  227. package/server/lib/session-title/session-access.js +46 -0
  228. package/server/lib/session-title/session-access.test.js +91 -0
  229. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  230. package/server/lib/session-turn-pages/routes.js +17 -102
  231. package/server/lib/session-turn-pages/routes.test.js +281 -0
  232. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  233. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  234. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  235. package/server/lib/small-model/catalog.js +31 -51
  236. package/server/lib/small-model/catalog.test.js +65 -10
  237. package/server/lib/small-model/custom-api.js +68 -0
  238. package/server/lib/small-model/custom-api.test.js +64 -0
  239. package/server/lib/small-model/index.js +124 -240
  240. package/server/lib/small-model/resolve.js +35 -120
  241. package/server/lib/small-model/resolve.test.js +20 -110
  242. package/server/lib/small-model/routes.js +1 -3
  243. package/server/lib/small-model/summary-settings.test.js +157 -253
  244. package/server/opencode-proxy.test.js +2 -0
  245. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  246. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  247. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  248. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  249. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  250. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  251. package/dist/assets/collapsible-BXRd5_Hr.js +0 -1
  252. package/dist/assets/context-menu-jGtH6fR9.js +0 -1
  253. package/dist/assets/es-c52LTHHe.js +0 -15
  254. package/dist/assets/fr-PisFbUE8.js +0 -13
  255. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  256. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  257. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  258. package/dist/assets/ko-ClFys5o2.js +0 -15
  259. package/dist/assets/main-Dn9rpsgl.js +0 -169
  260. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  261. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  262. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  263. package/dist/assets/multirun-BvSG94mW.js +0 -4
  264. package/dist/assets/pl-CnaKWN5F.js +0 -15
  265. package/dist/assets/platform-CWTIp02q.js +0 -4
  266. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  267. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  268. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  269. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  270. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  271. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  272. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  273. package/dist/assets/src-BzX5UI-c.css +0 -2
  274. package/dist/assets/store-DJczmczv.js +0 -15
  275. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  276. package/dist/assets/uk-CaX0iqkB.js +0 -15
  277. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  278. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  279. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  280. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  281. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  282. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  283. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  284. package/server/lib/small-model/call.js +0 -427
  285. package/server/lib/small-model/call.test.js +0 -382
  286. package/server/lib/small-model/opencode-session.js +0 -183
  287. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Host-side OpenCode transport boundary (ticket 11).
3
+ *
4
+ * Proxy gates browser → OpenChamber → OpenCode. Background Host work
5
+ * (queue / goal / scheduled / assistants / session-title) often calls OpenCode
6
+ * directly and must share the same execution admission: verified contract only
7
+ * for writes; diagnostics, reads, and stop-task stay available.
8
+ */
9
+
10
+ import {
11
+ runtimeContractExecutionBlockedBody,
12
+ shouldBlockRuntimeContractExecution,
13
+ } from './runtime-contract.js';
14
+
15
+ /** @type {(() => (object | null | undefined)) | null} */
16
+ let registeredGetRuntimeContract = null;
17
+
18
+ /** @type {{ allowMissingContract?: boolean }} */
19
+ let registeredGateOptions = {};
20
+
21
+ /**
22
+ * Wire once from the composition root so makeOpenCodeV2Client + serverOpenCodeFetch
23
+ * share the live lifecycle contract without each caller threading deps.
24
+ *
25
+ * Production must not pass `allowMissingContract: true` — missing contract blocks
26
+ * writes. Unit/DI factories may opt in when no lifecycle is configured.
27
+ * @param {(() => (object | null | undefined)) | null} getter
28
+ * @param {{ allowMissingContract?: boolean }} [options]
29
+ */
30
+ export function configureServerOpenCodeFetchGate(getter, options = {}) {
31
+ registeredGetRuntimeContract = typeof getter === 'function' ? getter : null;
32
+ registeredGateOptions = {
33
+ allowMissingContract: options?.allowMissingContract === true,
34
+ };
35
+ }
36
+
37
+ export function getRegisteredRuntimeContract() {
38
+ try {
39
+ return registeredGetRuntimeContract ? registeredGetRuntimeContract() : null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export function getRegisteredRuntimeContractGateOptions() {
46
+ return { ...registeredGateOptions };
47
+ }
48
+
49
+ /**
50
+ * @param {string | URL | Request} input
51
+ * @param {RequestInit | undefined} init
52
+ * @returns {{ method: string, pathWithQuery: string }}
53
+ */
54
+ export function resolveFetchMethodAndPath(input, init) {
55
+ const method = String(
56
+ (init && typeof init.method === 'string' && init.method)
57
+ || (typeof Request !== 'undefined' && input instanceof Request && input.method)
58
+ || 'GET',
59
+ ).toUpperCase();
60
+
61
+ let urlString = '';
62
+ if (typeof input === 'string') {
63
+ urlString = input;
64
+ } else if (typeof URL !== 'undefined' && input instanceof URL) {
65
+ urlString = input.toString();
66
+ } else if (typeof Request !== 'undefined' && input instanceof Request) {
67
+ urlString = input.url;
68
+ } else {
69
+ urlString = String(input || '');
70
+ }
71
+
72
+ let pathWithQuery = urlString;
73
+ try {
74
+ const parsed = new URL(urlString, 'http://opencode.local');
75
+ pathWithQuery = `${parsed.pathname}${parsed.search || ''}`;
76
+ } catch {
77
+ const stripped = urlString.replace(/^https?:\/\/[^/]+/i, '');
78
+ pathWithQuery = stripped.startsWith('/') ? stripped : `/${stripped}`;
79
+ }
80
+
81
+ return { method, pathWithQuery };
82
+ }
83
+
84
+ /**
85
+ * @param {{
86
+ * getRuntimeContract?: (() => (object | null | undefined)) | null,
87
+ * method: string,
88
+ * pathWithQuery: string,
89
+ * allowMissingContract?: boolean,
90
+ * }} input
91
+ */
92
+ export function assertServerOpenCodeExecutionAllowed(input) {
93
+ const getter = input.getRuntimeContract || registeredGetRuntimeContract;
94
+ let contract = null;
95
+ try {
96
+ contract = typeof getter === 'function' ? getter() : null;
97
+ } catch {
98
+ contract = null;
99
+ }
100
+ const allowMissingContract = typeof input.allowMissingContract === 'boolean'
101
+ ? input.allowMissingContract
102
+ : registeredGateOptions.allowMissingContract === true;
103
+ if (!shouldBlockRuntimeContractExecution(input.method, input.pathWithQuery, contract, {
104
+ allowMissingContract,
105
+ })) {
106
+ return null;
107
+ }
108
+ return runtimeContractExecutionBlockedBody(contract);
109
+ }
110
+
111
+ /**
112
+ * Wrap fetch so host OpenCode SDK clients share proxy write admission.
113
+ * @param {typeof fetch} [fetchImpl]
114
+ * @param {{
115
+ * getRuntimeContract?: () => (object | null | undefined),
116
+ * allowMissingContract?: boolean,
117
+ * }} [options]
118
+ * @returns {typeof fetch}
119
+ */
120
+ export function wrapFetchWithRuntimeContractGate(fetchImpl = fetch, options = {}) {
121
+ const getRuntimeContract = options.getRuntimeContract || (() => getRegisteredRuntimeContract());
122
+ const allowMissingContract = typeof options.allowMissingContract === 'boolean'
123
+ ? options.allowMissingContract
124
+ : registeredGateOptions.allowMissingContract === true;
125
+
126
+ return async (input, init) => {
127
+ const { method, pathWithQuery } = resolveFetchMethodAndPath(input, init);
128
+ const blocked = assertServerOpenCodeExecutionAllowed({
129
+ getRuntimeContract,
130
+ method,
131
+ pathWithQuery,
132
+ allowMissingContract,
133
+ });
134
+ if (blocked) {
135
+ return new Response(JSON.stringify(blocked), {
136
+ status: 409,
137
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
138
+ });
139
+ }
140
+ return fetchImpl(input, init);
141
+ };
142
+ }
143
+
144
+ /**
145
+ * High-level Host OpenCode JSON helper used by goal / title and tests.
146
+ * @param {{
147
+ * buildOpenCodeUrl: (pathname: string, prefix?: string) => string,
148
+ * getOpenCodeAuthHeaders: () => Record<string, string>,
149
+ * getRuntimeContract?: () => (object | null | undefined),
150
+ * allowMissingContract?: boolean,
151
+ * fetchImpl?: typeof fetch,
152
+ * }} deps
153
+ */
154
+ export function createServerOpenCodeFetch(deps) {
155
+ const {
156
+ buildOpenCodeUrl,
157
+ getOpenCodeAuthHeaders,
158
+ getRuntimeContract = () => getRegisteredRuntimeContract(),
159
+ allowMissingContract,
160
+ fetchImpl = fetch,
161
+ } = deps;
162
+
163
+ const gatedFetch = wrapFetchWithRuntimeContractGate(fetchImpl, {
164
+ getRuntimeContract,
165
+ allowMissingContract,
166
+ });
167
+
168
+ /**
169
+ * @param {string} fetchPath
170
+ * @param {{
171
+ * directory?: string | null,
172
+ * method?: string,
173
+ * body?: unknown,
174
+ * query?: Record<string, string> | URLSearchParams | null,
175
+ * signal?: AbortSignal,
176
+ * timeoutMs?: number,
177
+ * }} [options]
178
+ */
179
+ const serverOpenCodeFetch = async (fetchPath, options = {}) => {
180
+ const method = String(options.method || 'GET').toUpperCase();
181
+ const base = buildOpenCodeUrl(fetchPath, '');
182
+ const params = new URLSearchParams(options.query || {});
183
+ if (options.directory) params.set('directory', options.directory);
184
+ const search = params.toString();
185
+ const url = search ? `${base}${base.includes('?') ? '&' : '?'}${search}` : base;
186
+
187
+ const pathOnly = String(fetchPath || '').split('?')[0] || '';
188
+ const blocked = assertServerOpenCodeExecutionAllowed({
189
+ getRuntimeContract,
190
+ method,
191
+ pathWithQuery: pathOnly,
192
+ allowMissingContract,
193
+ });
194
+ if (blocked) {
195
+ const error = new Error(blocked.error);
196
+ error.code = blocked.errorCode;
197
+ error.status = 409;
198
+ error.body = blocked;
199
+ throw error;
200
+ }
201
+
202
+ const signal = options.signal
203
+ || (typeof options.timeoutMs === 'number' && options.timeoutMs > 0
204
+ ? AbortSignal.timeout(options.timeoutMs)
205
+ : undefined);
206
+
207
+ const response = await gatedFetch(url, {
208
+ method,
209
+ headers: {
210
+ Accept: 'application/json',
211
+ ...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
212
+ ...getOpenCodeAuthHeaders(),
213
+ },
214
+ ...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
215
+ ...(signal ? { signal } : {}),
216
+ });
217
+
218
+ if (!response.ok) {
219
+ let body = null;
220
+ try {
221
+ body = await response.json();
222
+ } catch {
223
+ body = null;
224
+ }
225
+ if (body && body.errorCode === 'RUNTIME_CONTRACT_EXECUTION_BLOCKED') {
226
+ const error = new Error(body.error || 'OpenCode runtime contract blocked execution');
227
+ error.code = body.errorCode;
228
+ error.status = response.status;
229
+ error.body = body;
230
+ throw error;
231
+ }
232
+ const error = new Error(`OpenCode ${method} ${fetchPath} failed with ${response.status}`);
233
+ error.status = response.status;
234
+ error.body = body;
235
+ throw error;
236
+ }
237
+
238
+ return response.json().catch(() => null);
239
+ };
240
+
241
+ return serverOpenCodeFetch;
242
+ }
@@ -0,0 +1,130 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ assertServerOpenCodeExecutionAllowed,
4
+ configureServerOpenCodeFetchGate,
5
+ createServerOpenCodeFetch,
6
+ resolveFetchMethodAndPath,
7
+ wrapFetchWithRuntimeContractGate,
8
+ } from './server-opencode-fetch.js';
9
+ import { makeOpenCodeV2Client } from './v2-client.js';
10
+
11
+ describe('serverOpenCodeFetch transport gate (ticket 11)', () => {
12
+ afterEach(() => {
13
+ configureServerOpenCodeFetchGate(null);
14
+ });
15
+
16
+ it('resolves method and path from URL strings', () => {
17
+ expect(resolveFetchMethodAndPath('http://127.0.0.1:4096/session/ses_1/prompt', { method: 'POST' }))
18
+ .toEqual({ method: 'POST', pathWithQuery: '/session/ses_1/prompt' });
19
+ });
20
+
21
+ it('blocks write execution when contract denies, keeps reads and stop-task', () => {
22
+ const contract = {
23
+ executionAllowed: false,
24
+ phase: 'ready-unverified',
25
+ reasons: ['unverified-newer'],
26
+ serveVersion: '2.1.0',
27
+ minVerifiedVersion: '2.0.12',
28
+ maxVerifiedVersion: '2.0.14',
29
+ };
30
+
31
+ expect(assertServerOpenCodeExecutionAllowed({
32
+ getRuntimeContract: () => contract,
33
+ method: 'POST',
34
+ pathWithQuery: '/session/ses_1/prompt_async',
35
+ })?.errorCode).toBe('RUNTIME_CONTRACT_EXECUTION_BLOCKED');
36
+
37
+ expect(assertServerOpenCodeExecutionAllowed({
38
+ getRuntimeContract: () => contract,
39
+ method: 'GET',
40
+ pathWithQuery: '/session/ses_1/message',
41
+ })).toBeNull();
42
+
43
+ expect(assertServerOpenCodeExecutionAllowed({
44
+ getRuntimeContract: () => contract,
45
+ method: 'POST',
46
+ pathWithQuery: '/session/ses_1/interrupt',
47
+ })).toBeNull();
48
+ });
49
+
50
+ it('production default blocks Host writes when contract is missing', () => {
51
+ expect(assertServerOpenCodeExecutionAllowed({
52
+ getRuntimeContract: () => null,
53
+ method: 'POST',
54
+ pathWithQuery: '/session/ses_1/prompt',
55
+ })?.errorCode).toBe('RUNTIME_CONTRACT_EXECUTION_BLOCKED');
56
+
57
+ expect(assertServerOpenCodeExecutionAllowed({
58
+ getRuntimeContract: () => null,
59
+ method: 'POST',
60
+ pathWithQuery: '/session/ses_1/prompt',
61
+ allowMissingContract: true,
62
+ })).toBeNull();
63
+ });
64
+
65
+ it('wrapFetch returns 409 JSON without calling upstream on blocked writes', async () => {
66
+ const upstream = vi.fn(async () => new Response('ok'));
67
+ configureServerOpenCodeFetchGate(() => ({
68
+ executionAllowed: false,
69
+ phase: 'incompatible',
70
+ reasons: ['below-min-verified'],
71
+ serveVersion: '2.0.5',
72
+ minVerifiedVersion: '2.0.12',
73
+ }));
74
+ const gated = wrapFetchWithRuntimeContractGate(upstream);
75
+ const response = await gated('http://opencode.test/session/ses_1/prompt', { method: 'POST', body: '{}' });
76
+ expect(response.status).toBe(409);
77
+ const body = await response.json();
78
+ expect(body.errorCode).toBe('RUNTIME_CONTRACT_EXECUTION_BLOCKED');
79
+ expect(upstream).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('createServerOpenCodeFetch throws structured error on blocked prompt', async () => {
83
+ const fetchImpl = vi.fn();
84
+ const serverOpenCodeFetch = createServerOpenCodeFetch({
85
+ buildOpenCodeUrl: (pathname) => `http://opencode.test${pathname}`,
86
+ getOpenCodeAuthHeaders: () => ({}),
87
+ getRuntimeContract: () => ({
88
+ executionAllowed: false,
89
+ phase: 'ready-unverified',
90
+ reasons: ['unverified-newer'],
91
+ serveVersion: '2.1.0',
92
+ }),
93
+ fetchImpl,
94
+ });
95
+
96
+ await expect(serverOpenCodeFetch('/session/ses_1/prompt_async', {
97
+ method: 'POST',
98
+ body: { parts: [] },
99
+ })).rejects.toMatchObject({
100
+ code: 'RUNTIME_CONTRACT_EXECUTION_BLOCKED',
101
+ status: 409,
102
+ });
103
+ expect(fetchImpl).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it('makeOpenCodeV2Client uses gated fetch so host SDK cannot bypass admission', async () => {
107
+ const upstream = vi.fn(async () => new Response(JSON.stringify({ ok: true }), {
108
+ status: 200,
109
+ headers: { 'content-type': 'application/json' },
110
+ }));
111
+ configureServerOpenCodeFetchGate(() => ({
112
+ executionAllowed: false,
113
+ phase: 'incompatible',
114
+ reasons: ['below-min-verified'],
115
+ serveVersion: '2.0.1',
116
+ }));
117
+
118
+ // Client construction must not throw; gate applies on network.
119
+ const client = makeOpenCodeV2Client({
120
+ baseUrl: 'http://opencode.test',
121
+ fetchImpl: upstream,
122
+ });
123
+ expect(client).toBeTruthy();
124
+
125
+ const gated = wrapFetchWithRuntimeContractGate(upstream);
126
+ const blocked = await gated('http://opencode.test/session', { method: 'POST', body: '{}' });
127
+ expect(blocked.status).toBe(409);
128
+ expect(upstream).not.toHaveBeenCalled();
129
+ });
130
+ });
@@ -1,5 +1,10 @@
1
1
  import { registerOpenCodeProxy } from './proxy.js';
2
2
  import { pathLooksUserConfigured, mergePathValues } from './path-utils.js';
3
+ import {
4
+ configureServerOpenCodeFetchGate,
5
+ createServerOpenCodeFetch,
6
+ wrapFetchWithRuntimeContractGate,
7
+ } from './server-opencode-fetch.js';
3
8
 
4
9
  export const createServerUtilsRuntime = (dependencies) => {
5
10
  const {
@@ -22,8 +27,30 @@ export const createServerUtilsRuntime = (dependencies) => {
22
27
  clearLastOpenCodeError,
23
28
  getLoginShellPath,
24
29
  getStoredSessionMetadata = null,
30
+ getStoredSessionMetadataSync = null,
31
+ ensureSessionMetadataReady = null,
32
+ onSessionDeleted = null,
25
33
  } = dependencies;
26
34
 
35
+ const getRuntimeContract = () => {
36
+ try {
37
+ return getRuntime()?.runtimeContract ?? null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ };
42
+
43
+ // Shared Host transport gate for SDK clients + raw serverOpenCodeFetch.
44
+ configureServerOpenCodeFetchGate(getRuntimeContract);
45
+
46
+ const serverOpenCodeFetch = createServerOpenCodeFetch({
47
+ buildOpenCodeUrl,
48
+ getOpenCodeAuthHeaders,
49
+ getRuntimeContract,
50
+ });
51
+
52
+ const gatedOpenCodeFetchImpl = wrapFetchWithRuntimeContractGate(fetch, { getRuntimeContract });
53
+
27
54
  const setOpenCodePort = (port) => {
28
55
  if (!Number.isFinite(port) || port <= 0) {
29
56
  return;
@@ -217,6 +244,9 @@ export const createServerUtilsRuntime = (dependencies) => {
217
244
  onInteractiveSessionRequest: options.onInteractiveSessionRequest,
218
245
  onSessionTurnAdmission: options.onSessionTurnAdmission,
219
246
  getStoredSessionMetadata,
247
+ getStoredSessionMetadataSync,
248
+ ensureSessionMetadataReady,
249
+ onSessionDeleted,
220
250
  });
221
251
  };
222
252
 
@@ -230,5 +260,8 @@ export const createServerUtilsRuntime = (dependencies) => {
230
260
  fetchProvidersSnapshot,
231
261
  fetchModelsSnapshot,
232
262
  setupProxy,
263
+ serverOpenCodeFetch,
264
+ gatedOpenCodeFetchImpl,
265
+ getRuntimeContract,
233
266
  };
234
267
  };
@@ -0,0 +1,66 @@
1
+ import { expect, test } from 'vitest';
2
+ import { execFile, spawn } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import fs from 'node:fs/promises';
5
+ import net from 'node:net';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { createSharedOpenCodeService, isProcessAlive } from './shared-service.js';
9
+
10
+ // Opt-in real service, with isolated registration, config and database. No model calls.
11
+ test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('shared service survives its launcher, is reused, and can be replaced explicitly', async () => {
12
+ const temporary = path.join(os.tmpdir(), 'opencode');
13
+ await fs.mkdir(temporary, { recursive: true });
14
+ const root = await fs.mkdtemp(path.join(temporary, 'shared-service-'));
15
+ const config = path.join(root, 'config', 'opencode');
16
+ await fs.mkdir(config, { recursive: true });
17
+ const listener = net.createServer();
18
+ await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve));
19
+ const port = listener.address().port;
20
+ await new Promise((resolve) => listener.close(resolve));
21
+ await fs.writeFile(path.join(config, 'service.json'), JSON.stringify({ hostname: '127.0.0.1', port }));
22
+ const env = {
23
+ PATH: process.env.PATH,
24
+ HOME: root,
25
+ TMPDIR: os.tmpdir(),
26
+ XDG_CONFIG_HOME: path.join(root, 'config'),
27
+ XDG_DATA_HOME: path.join(root, 'data'),
28
+ XDG_STATE_HOME: path.join(root, 'state'),
29
+ XDG_CACHE_HOME: path.join(root, 'cache'),
30
+ OPENCODE_CONFIG_DIR: config,
31
+ OPENCODE_DISABLE_MODELS_FETCH: '1',
32
+ };
33
+ const binary = process.env.OPENCHAMBER_TEST_OPENCODE_BINARY;
34
+ const service = createSharedOpenCodeService({
35
+ envLike: env,
36
+ spawnImpl: (command, args, options) => spawn(command, args, { ...options, cwd: root }),
37
+ });
38
+ const probe = async (registration) => {
39
+ const response = await fetch(`${registration.origin}/api/info`, {
40
+ headers: { Authorization: `Basic ${Buffer.from(`opencode:${registration.password}`).toString('base64')}` },
41
+ signal: AbortSignal.timeout(5000),
42
+ });
43
+ expect(response.ok).toBe(true);
44
+ return response.json();
45
+ };
46
+ try {
47
+ await service.start({ binary, env });
48
+ const first = await service.readRegistration();
49
+ expect(first?.pid).toBeGreaterThan(0);
50
+ expect(isProcessAlive(first.pid)).toBe(true);
51
+ expect((await probe(first)).version).toBe(first.version);
52
+
53
+ await service.start({ binary, env });
54
+ const reused = await service.readRegistration();
55
+ expect(reused.pid).toBe(first.pid);
56
+
57
+ await service.start({ binary, env, replace: true });
58
+ const replacement = await service.readRegistration();
59
+ expect(replacement.pid).not.toBe(first.pid);
60
+ expect(isProcessAlive(first.pid)).toBe(false);
61
+ expect((await probe(replacement)).version).toBe(replacement.version);
62
+ } finally {
63
+ await promisify(execFile)(binary, ['service', 'stop'], { env, cwd: root, timeout: 20_000, windowsHide: true });
64
+ await fs.rm(root, { recursive: true, force: true });
65
+ }
66
+ }, 120_000);
@@ -0,0 +1,147 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ // Official OpenCode background service (`opencode serve --service`). Every
7
+ // official client on this machine — TUI, CLI, Desktop — finds it through one
8
+ // registration file and shares its process, database, and config watchers, so
9
+ // providers connected or plugins changed elsewhere are live here too.
10
+
11
+ const SERVICE_START_TIMEOUT_MS = 45_000;
12
+
13
+ /** Stable-channel registration file (`latest`/`beta`/`next`/`dev` share it). */
14
+ export const resolveSharedServiceRegistrationPath = (envLike = process.env, homeDir = os.homedir()) => {
15
+ const stateHome = typeof envLike.XDG_STATE_HOME === 'string' && envLike.XDG_STATE_HOME.trim()
16
+ ? envLike.XDG_STATE_HOME.trim()
17
+ : path.join(homeDir, '.local', 'state');
18
+ return path.join(stateHome, 'opencode', 'service.json');
19
+ };
20
+
21
+ /** `OPENCHAMBER_OPENCODE_SHARED_SERVICE=0` opts back into a private managed `opencode serve`. */
22
+ export const isSharedServiceEnabled = (envLike = process.env) => {
23
+ const raw = String(envLike.OPENCHAMBER_OPENCODE_SHARED_SERVICE ?? '').trim().toLowerCase();
24
+ return raw !== '0' && raw !== 'false' && raw !== 'off';
25
+ };
26
+
27
+ /** @returns {{ origin: string, port: number, pid: number | null, password: string | null, version: string | null } | null} */
28
+ export const parseSharedServiceRegistration = (text) => {
29
+ let value;
30
+ try {
31
+ value = JSON.parse(text);
32
+ } catch {
33
+ return null;
34
+ }
35
+ if (!value || typeof value !== 'object' || typeof value.url !== 'string') return null;
36
+ let url;
37
+ try {
38
+ url = new URL(value.url);
39
+ } catch {
40
+ return null;
41
+ }
42
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
43
+ const port = Number.parseInt(url.port, 10);
44
+ if (!Number.isInteger(port) || port <= 0) return null;
45
+ return {
46
+ origin: url.origin,
47
+ port,
48
+ pid: Number.isInteger(value.pid) && value.pid > 0 ? value.pid : null,
49
+ password: typeof value.password === 'string' && value.password.trim() ? value.password.trim() : null,
50
+ version: typeof value.version === 'string' ? value.version : null,
51
+ };
52
+ };
53
+
54
+ export const readSharedServiceRegistration = async ({ file = resolveSharedServiceRegistrationPath(), fsLike = fs } = {}) => {
55
+ const text = await fsLike.readFile(file, 'utf8').catch(() => null);
56
+ return text == null ? null : parseSharedServiceRegistration(text);
57
+ };
58
+
59
+ /**
60
+ * Environment inherited by a service OpenChamber starts. The service outlives
61
+ * OpenChamber and passes its env to every tool, so OpenChamber credentials
62
+ * (`OPENCHAMBER_*`, the managed-serve password) never travel with it; the
63
+ * service owns its own persistent password.
64
+ */
65
+ export const buildSharedServiceStartEnv = (baseEnv = {}) => {
66
+ const env = {};
67
+ for (const [key, value] of Object.entries(baseEnv)) {
68
+ if (typeof value !== 'string') continue;
69
+ if (key.startsWith('OPENCHAMBER_')) continue;
70
+ if (key === 'OPENCODE_SERVER_PASSWORD' || key === 'OPENCODE_PASSWORD') continue;
71
+ env[key] = value;
72
+ }
73
+ return env;
74
+ };
75
+
76
+ /**
77
+ * `opencode service start` reuses a healthy compatible service or spawns a
78
+ * detached one, then exits once it is ready. Normal recovery does not replace
79
+ * a healthy service; explicit upgrades use `service restart` with the target.
80
+ */
81
+ export const startSharedOpenCodeService = ({
82
+ binary,
83
+ args = [],
84
+ env,
85
+ spawnImpl = spawn,
86
+ timeoutMs = SERVICE_START_TIMEOUT_MS,
87
+ replace = false,
88
+ }) => new Promise((resolve, reject) => {
89
+ const action = replace ? 'restart' : 'start';
90
+ const child = spawnImpl(binary, [...args, 'service', action], {
91
+ env,
92
+ stdio: ['ignore', 'ignore', 'pipe'],
93
+ windowsHide: true,
94
+ });
95
+ let stderr = '';
96
+ let settled = false;
97
+ const finish = (error) => {
98
+ if (settled) return;
99
+ settled = true;
100
+ clearTimeout(timer);
101
+ if (error) reject(error);
102
+ else resolve();
103
+ };
104
+ const timer = setTimeout(() => {
105
+ try {
106
+ child.kill('SIGTERM');
107
+ } catch {
108
+ }
109
+ finish(new Error(`Timed out waiting for \`opencode service ${action}\``));
110
+ }, timeoutMs);
111
+ child.stderr?.on('data', (chunk) => {
112
+ stderr = (stderr + chunk.toString()).slice(-2000);
113
+ });
114
+ child.once('error', (error) => finish(error));
115
+ child.once('exit', (code) => {
116
+ if (code === 0) {
117
+ finish(null);
118
+ return;
119
+ }
120
+ const detail = stderr.trim();
121
+ finish(new Error(`\`opencode service ${action}\` exited with code ${code}${detail ? `: ${detail}` : ''}`));
122
+ });
123
+ });
124
+
125
+ export const isProcessAlive = (pid) => {
126
+ if (!Number.isInteger(pid) || pid <= 0) return false;
127
+ try {
128
+ process.kill(pid, 0);
129
+ return true;
130
+ } catch (error) {
131
+ return error?.code === 'EPERM';
132
+ }
133
+ };
134
+
135
+ export const createSharedOpenCodeService = ({
136
+ envLike = process.env,
137
+ fsLike = fs,
138
+ spawnImpl = spawn,
139
+ } = {}) => {
140
+ const file = resolveSharedServiceRegistrationPath(envLike);
141
+ return {
142
+ registrationFile: file,
143
+ buildStartEnv: buildSharedServiceStartEnv,
144
+ start: ({ binary, args, env, replace }) => startSharedOpenCodeService({ binary, args, env, replace, spawnImpl }),
145
+ readRegistration: () => readSharedServiceRegistration({ file, fsLike }),
146
+ };
147
+ };