@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
@@ -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 {
@@ -21,8 +26,31 @@ export const createServerUtilsRuntime = (dependencies) => {
21
26
  setOpenCodeNotReadySince,
22
27
  clearLastOpenCodeError,
23
28
  getLoginShellPath,
29
+ getStoredSessionMetadata = null,
30
+ getStoredSessionMetadataSync = null,
31
+ ensureSessionMetadataReady = null,
32
+ onSessionDeleted = null,
24
33
  } = dependencies;
25
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
+
26
54
  const setOpenCodePort = (port) => {
27
55
  if (!Number.isFinite(port) || port <= 0) {
28
56
  return;
@@ -215,6 +243,10 @@ export const createServerUtilsRuntime = (dependencies) => {
215
243
  getUiNotificationClients,
216
244
  onInteractiveSessionRequest: options.onInteractiveSessionRequest,
217
245
  onSessionTurnAdmission: options.onSessionTurnAdmission,
246
+ getStoredSessionMetadata,
247
+ getStoredSessionMetadataSync,
248
+ ensureSessionMetadataReady,
249
+ onSessionDeleted,
218
250
  });
219
251
  };
220
252
 
@@ -228,5 +260,8 @@ export const createServerUtilsRuntime = (dependencies) => {
228
260
  fetchProvidersSnapshot,
229
261
  fetchModelsSnapshot,
230
262
  setupProxy,
263
+ serverOpenCodeFetch,
264
+ gatedOpenCodeFetchImpl,
265
+ getRuntimeContract,
231
266
  };
232
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
+ };