@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,93 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import path from 'node:path';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import {
6
+ buildSharedServiceStartEnv,
7
+ isSharedServiceEnabled,
8
+ parseSharedServiceRegistration,
9
+ readSharedServiceRegistration,
10
+ resolveSharedServiceRegistrationPath,
11
+ startSharedOpenCodeService,
12
+ } from './shared-service.js';
13
+
14
+ const fakeChild = () => {
15
+ const child = new EventEmitter();
16
+ child.stderr = new EventEmitter();
17
+ child.kill = vi.fn();
18
+ return child;
19
+ };
20
+
21
+ describe('shared OpenCode service registration', () => {
22
+ it('resolves the stable-channel registration under the XDG state root', () => {
23
+ expect(resolveSharedServiceRegistrationPath({}, '/home/u')).toBe(path.join('/home/u', '.local', 'state', 'opencode', 'service.json'));
24
+ expect(resolveSharedServiceRegistrationPath({ XDG_STATE_HOME: '/xdg' }, '/home/u')).toBe(path.join('/xdg', 'opencode', 'service.json'));
25
+ });
26
+
27
+ it('parses a registration and rejects malformed ones', () => {
28
+ expect(parseSharedServiceRegistration(JSON.stringify({
29
+ id: 'a', version: '2.0.12', url: 'http://127.0.0.1:49374', pid: 42, password: 'pw',
30
+ }))).toEqual({ origin: 'http://127.0.0.1:49374', port: 49374, pid: 42, password: 'pw', version: '2.0.12' });
31
+ expect(parseSharedServiceRegistration(JSON.stringify({ url: 'http://127.0.0.1:49374' }))?.password).toBeNull();
32
+ expect(parseSharedServiceRegistration('not json')).toBeNull();
33
+ expect(parseSharedServiceRegistration(JSON.stringify({ url: 'ftp://x:1' }))).toBeNull();
34
+ expect(parseSharedServiceRegistration(JSON.stringify({ url: 'http://127.0.0.1' }))).toBeNull();
35
+ });
36
+
37
+ it('treats a missing registration file as absent, not as a service', async () => {
38
+ const fsLike = { readFile: vi.fn(async () => { throw Object.assign(new Error('nope'), { code: 'ENOENT' }); }) };
39
+ await expect(readSharedServiceRegistration({ file: '/x/service.json', fsLike })).resolves.toBeNull();
40
+ });
41
+
42
+ it('is enabled by default and can be opted out', () => {
43
+ expect(isSharedServiceEnabled({})).toBe(true);
44
+ expect(isSharedServiceEnabled({ OPENCHAMBER_OPENCODE_SHARED_SERVICE: '0' })).toBe(false);
45
+ expect(isSharedServiceEnabled({ OPENCHAMBER_OPENCODE_SHARED_SERVICE: 'false' })).toBe(false);
46
+ });
47
+ });
48
+
49
+ describe('shared OpenCode service start', () => {
50
+ it('never hands OpenChamber credentials to the long-lived service', () => {
51
+ expect(buildSharedServiceStartEnv({
52
+ PATH: '/bin',
53
+ HOME: '/home/u',
54
+ OPENCODE_SERVER_PASSWORD: 'managed',
55
+ OPENCODE_PASSWORD: 'managed',
56
+ OPENCHAMBER_UI_PASSWORD: 'ui',
57
+ OPENCHAMBER_DATA_DIR: '/data',
58
+ })).toEqual({ PATH: '/bin', HOME: '/home/u' });
59
+ });
60
+
61
+ it('runs `service start` and resolves on a clean exit', async () => {
62
+ const child = fakeChild();
63
+ const spawnImpl = vi.fn(() => {
64
+ queueMicrotask(() => child.emit('exit', 0));
65
+ return child;
66
+ });
67
+ await startSharedOpenCodeService({ binary: '/bin/opencode', args: ['--x'], env: { PATH: '/bin' }, spawnImpl });
68
+ expect(spawnImpl).toHaveBeenCalledWith('/bin/opencode', ['--x', 'service', 'start'], expect.objectContaining({ env: { PATH: '/bin' } }));
69
+ });
70
+
71
+ it('surfaces the CLI error when `service start` fails', async () => {
72
+ const child = fakeChild();
73
+ const spawnImpl = vi.fn(() => {
74
+ queueMicrotask(() => {
75
+ child.stderr.emit('data', 'Background service failed to start');
76
+ child.emit('exit', 1);
77
+ });
78
+ return child;
79
+ });
80
+ await expect(startSharedOpenCodeService({ binary: 'opencode', env: {}, spawnImpl }))
81
+ .rejects.toThrow(/exited with code 1: Background service failed to start/);
82
+ });
83
+
84
+ it('replaces the registered service only for an explicit upgrade', async () => {
85
+ const child = fakeChild();
86
+ const spawnImpl = vi.fn(() => {
87
+ queueMicrotask(() => child.emit('exit', 0));
88
+ return child;
89
+ });
90
+ await startSharedOpenCodeService({ binary: '/cache/opencode', env: {}, replace: true, spawnImpl });
91
+ expect(spawnImpl).toHaveBeenCalledWith('/cache/opencode', ['service', 'restart'], expect.objectContaining({ windowsHide: true }));
92
+ });
93
+ });
@@ -371,9 +371,9 @@ function addSkillFromMdFile(skillsMap, skillMdPath, scope, source) {
371
371
  return;
372
372
  }
373
373
 
374
- const name = typeof parsed.frontmatter?.name === 'string'
375
- ? parsed.frontmatter.name.trim()
376
- : '';
374
+ const name = path.basename(skillMdPath) === 'SKILL.md'
375
+ ? path.basename(path.dirname(skillMdPath))
376
+ : path.basename(skillMdPath, '.md');
377
377
  const description = typeof parsed.frontmatter?.description === 'string'
378
378
  ? parsed.frontmatter.description
379
379
  : '';
@@ -1,3 +1,5 @@
1
+ import { forceCloseHttpServerConnections } from './http-server-connections.js';
2
+
1
3
  export const createGracefulShutdownRuntime = (dependencies) => {
2
4
  const {
3
5
  process,
@@ -22,7 +24,6 @@ export const createGracefulShutdownRuntime = (dependencies) => {
22
24
  getOpenCodePort,
23
25
  getOpenCodeProcess,
24
26
  setOpenCodeProcess,
25
- killProcessOnPort,
26
27
  waitForPortRelease,
27
28
  getServer,
28
29
  getUiAuthController,
@@ -78,12 +79,14 @@ export const createGracefulShutdownRuntime = (dependencies) => {
78
79
  }
79
80
 
80
81
  if (!shouldSkipOpenCodeStop()) {
81
- const portToKill = getOpenCodePort();
82
+ const portToRelease = getOpenCodePort();
82
83
  const openCodeProcess = getOpenCodeProcess();
83
84
 
84
85
  if (openCodeProcess) {
85
86
  console.log('Stopping OpenCode process...');
86
87
  try {
88
+ // close() owns SIGTERM→SIGKILL on this child handle. Never re-kill a
89
+ // numeric pid captured before close — it may already be recycled.
87
90
  await openCodeProcess.close();
88
91
  } catch (error) {
89
92
  console.warn('Error closing OpenCode process:', error);
@@ -91,9 +94,9 @@ export const createGracefulShutdownRuntime = (dependencies) => {
91
94
  setOpenCodeProcess(null);
92
95
  }
93
96
 
94
- killProcessOnPort(portToKill);
95
- if (!(await waitForPortRelease(portToKill, 5000))) {
96
- console.warn(`Timed out waiting for OpenCode port ${portToKill} to be released during shutdown`);
97
+ // Probe only after owned close. Do not mass-kill by port (clients / external).
98
+ if (!(await waitForPortRelease(portToRelease, 5000))) {
99
+ console.warn(`Timed out waiting for OpenCode port ${portToRelease} to be released during shutdown`);
97
100
  }
98
101
  } else {
99
102
  console.log('Skipping OpenCode shutdown (external server)');
@@ -105,12 +108,18 @@ export const createGracefulShutdownRuntime = (dependencies) => {
105
108
  try {
106
109
  await Promise.race([
107
110
  new Promise((resolve) => {
111
+ // Stop accepting first. closeAllConnections drops plain HTTP sockets
112
+ // but leaves upgraded WS/SSE; destroy tracked server-owned leftovers
113
+ // so the close callback can fire without waiting SHUTDOWN_TIMEOUT.
108
114
  server.close(() => {
109
115
  console.log('HTTP server closed');
110
116
  resolve();
111
117
  });
112
118
  if (options.forceCloseConnections === true) {
113
- server.closeAllConnections?.();
119
+ const { destroyedTracked } = forceCloseHttpServerConnections(server);
120
+ if (destroyedTracked > 0) {
121
+ console.log(`Forced close of ${destroyedTracked} remaining server socket(s)`);
122
+ }
114
123
  }
115
124
  }),
116
125
  new Promise((resolve) => {
@@ -22,7 +22,6 @@ const createRuntime = (server, shutdownTimeoutMs = 1000, overrides = {}) => crea
22
22
  getOpenCodePort: () => null,
23
23
  getOpenCodeProcess: () => null,
24
24
  setOpenCodeProcess: vi.fn(),
25
- killProcessOnPort: vi.fn(),
26
25
  waitForPortRelease: vi.fn(async () => true),
27
26
  getServer: () => server,
28
27
  getUiAuthController: () => null,
@@ -96,4 +95,53 @@ describe('graceful shutdown runtime', () => {
96
95
  expect(scheduledTasksRuntime.stop).toHaveBeenCalledOnce();
97
96
  expect(runHistoryStore.close).not.toHaveBeenCalled();
98
97
  });
98
+
99
+ it('awaits managed OpenCode close with zero extra kill, then probes port release for 5s', async () => {
100
+ const close = vi.fn(async () => undefined);
101
+ const waitForPortRelease = vi.fn(async (_port, timeoutMs) => {
102
+ expect(timeoutMs).toBe(5000);
103
+ return true;
104
+ });
105
+ const setOpenCodeProcess = vi.fn();
106
+ const server = {
107
+ close: vi.fn((callback) => {
108
+ callback();
109
+ }),
110
+ };
111
+
112
+ const runtime = createRuntime(server, 1000, {
113
+ shouldSkipOpenCodeStop: () => false,
114
+ getOpenCodePort: () => 45678,
115
+ getOpenCodeProcess: () => ({ close, pid: 99 }),
116
+ setOpenCodeProcess,
117
+ waitForPortRelease,
118
+ });
119
+ await runtime.gracefulShutdown({ exitProcess: false });
120
+
121
+ expect(close).toHaveBeenCalledOnce();
122
+ expect(setOpenCodeProcess).toHaveBeenCalledWith(null);
123
+ expect(waitForPortRelease).toHaveBeenCalledWith(45678, 5000);
124
+ });
125
+
126
+ it('does not kill anything when OpenCode close already finished and a later server stage fails', async () => {
127
+ const close = vi.fn(async () => undefined);
128
+ const waitForPortRelease = vi.fn(async () => true);
129
+ const server = {
130
+ close: vi.fn(() => {
131
+ throw new Error('server close blew up after opencode stop');
132
+ }),
133
+ };
134
+
135
+ const runtime = createRuntime(server, 1000, {
136
+ shouldSkipOpenCodeStop: () => false,
137
+ getOpenCodePort: () => 45678,
138
+ getOpenCodeProcess: () => ({ close, pid: 99 }),
139
+ setOpenCodeProcess: vi.fn(),
140
+ waitForPortRelease,
141
+ });
142
+
143
+ await expect(runtime.gracefulShutdown({ exitProcess: false })).rejects.toThrow(/server close blew up/);
144
+ expect(close).toHaveBeenCalledOnce();
145
+ expect(waitForPortRelease).toHaveBeenCalledWith(45678, 5000);
146
+ });
99
147
  });
@@ -16,8 +16,6 @@ export const registerSkillRoutes = (app, dependencies) => {
16
16
  readSettingsFromDisk,
17
17
  sanitizeSkillCatalogs,
18
18
  isUnsafeSkillRelativePath,
19
- refreshOpenCodeAfterConfigChange,
20
- clientReloadDelayMs,
21
19
  buildOpenCodeUrl,
22
20
  getOpenCodeAuthHeaders,
23
21
  getOpenCodePort,
@@ -130,11 +128,12 @@ export const registerSkillRoutes = (app, dependencies) => {
130
128
  const client = OpenCode.make({
131
129
  baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
132
130
  headers: getOpenCodeAuthHeaders(),
133
- fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
131
+ fetch: globalThis.fetch,
134
132
  });
135
133
 
136
134
  const response = await client.skill.list(
137
135
  workingDirectory ? { location: { directory: workingDirectory } } : undefined,
136
+ { signal: AbortSignal.timeout(8_000) },
138
137
  );
139
138
  const payload = response?.data;
140
139
  if (!Array.isArray(payload)) {
@@ -143,8 +142,9 @@ export const registerSkillRoutes = (app, dependencies) => {
143
142
 
144
143
  return payload
145
144
  .map((item) => {
146
- const name = typeof item?.name === 'string' ? item.name.trim() : '';
147
- const location = typeof item?.location === 'string' ? item.location : '';
145
+ // OpenChamber's name is the invocation key, not the V2 display label.
146
+ const name = typeof item?.id === 'string' ? item.id : '';
147
+ const location = typeof item?.path === 'string' ? item.path : '';
148
148
  const description = typeof item?.description === 'string' ? item.description : '';
149
149
  const content = typeof item?.content === 'string' ? item.content : '';
150
150
  if (!name || !location) {
@@ -461,19 +461,14 @@ export const registerSkillRoutes = (app, dependencies) => {
461
461
 
462
462
  const installed = result.installed || [];
463
463
  const skipped = result.skipped || [];
464
- const requiresReload = installed.length > 0;
465
-
466
- if (requiresReload) {
467
- await refreshOpenCodeAfterConfigChange('skills install');
468
- }
469
464
 
470
465
  return res.json({
471
466
  ok: true,
472
467
  installed,
473
468
  skipped,
474
- requiresReload,
475
- message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
476
- reloadDelayMs: requiresReload ? clientReloadDelayMs : undefined,
469
+ requiresReload: false,
470
+ application: 'watch',
471
+ message: installed.length > 0 ? 'Skills installed successfully.' : 'No skills were installed',
477
472
  });
478
473
  }
479
474
 
@@ -512,19 +507,14 @@ export const registerSkillRoutes = (app, dependencies) => {
512
507
 
513
508
  const installed = result.installed || [];
514
509
  const skipped = result.skipped || [];
515
- const requiresReload = installed.length > 0;
516
-
517
- if (requiresReload) {
518
- await refreshOpenCodeAfterConfigChange('skills install');
519
- }
520
510
 
521
511
  res.json({
522
512
  ok: true,
523
513
  installed,
524
514
  skipped,
525
- requiresReload,
526
- message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
527
- reloadDelayMs: requiresReload ? clientReloadDelayMs : undefined,
515
+ requiresReload: false,
516
+ application: 'watch',
517
+ message: installed.length > 0 ? 'Skills installed successfully.' : 'No skills were installed',
528
518
  });
529
519
  } catch (error) {
530
520
  console.error('Failed to install skills:', error);
@@ -605,13 +595,12 @@ export const registerSkillRoutes = (app, dependencies) => {
605
595
  console.log('[Server] Scope:', scope, 'Working directory:', directory);
606
596
 
607
597
  createSkill(skillName, { ...config, source: skillSource }, directory, scope);
608
- await refreshOpenCodeAfterConfigChange('skill creation');
609
598
 
610
599
  res.json({
611
600
  success: true,
612
- requiresReload: true,
613
- message: `Skill ${skillName} created successfully. Reloading interface…`,
614
- reloadDelayMs: clientReloadDelayMs,
601
+ requiresReload: false,
602
+ application: 'watch',
603
+ message: `Skill ${skillName} created successfully.`,
615
604
  });
616
605
  } catch (error) {
617
606
  console.error('Failed to create skill:', error);
@@ -632,13 +621,12 @@ export const registerSkillRoutes = (app, dependencies) => {
632
621
  console.log('[Server] Working directory:', directory);
633
622
 
634
623
  updateSkill(skillName, updates, directory, updates?.targetPath);
635
- await refreshOpenCodeAfterConfigChange('skill update');
636
624
 
637
625
  res.json({
638
626
  success: true,
639
- requiresReload: true,
640
- message: `Skill ${skillName} updated successfully. Reloading interface…`,
641
- reloadDelayMs: clientReloadDelayMs,
627
+ requiresReload: false,
628
+ application: 'watch',
629
+ message: `Skill ${skillName} updated successfully.`,
642
630
  });
643
631
  } catch (error) {
644
632
  console.error('[Server] Failed to update skill:', error);
@@ -724,13 +712,12 @@ export const registerSkillRoutes = (app, dependencies) => {
724
712
  }
725
713
 
726
714
  deleteSkill(skillName, directory);
727
- await refreshOpenCodeAfterConfigChange('skill deletion');
728
715
 
729
716
  res.json({
730
717
  success: true,
731
- requiresReload: true,
732
- message: `Skill ${skillName} deleted successfully. Reloading interface…`,
733
- reloadDelayMs: clientReloadDelayMs,
718
+ requiresReload: false,
719
+ application: 'watch',
720
+ message: `Skill ${skillName} deleted successfully.`,
734
721
  });
735
722
  } catch (error) {
736
723
  console.error('Failed to delete skill:', error);
@@ -8,12 +8,45 @@ const { OpenCode } = await import('@opencode/client');
8
8
  const { registerSkillRoutes } = await import('./skill-routes.js');
9
9
 
10
10
  describe('skill summary route', () => {
11
+ it('preserves authentication and directory through the real V2 SDK transport', async () => {
12
+ const { OpenCode: RealOpenCode } = await vi.importActual('@opencode/client');
13
+ const upstreamRequests = [];
14
+ const originalFetch = globalThis.fetch;
15
+ globalThis.fetch = async (url, init) => {
16
+ upstreamRequests.push(new Request(url, init));
17
+ return Response.json({ location: { directory: '/repo' }, data: [{
18
+ id: 'agent-tracker-api', name: 'Runmark', path: '/skills/agent-tracker-api/SKILL.md',
19
+ description: 'Query Runmark API', content: 'Instructions',
20
+ }] });
21
+ };
22
+ OpenCode.make.mockImplementation(RealOpenCode.make);
23
+ try {
24
+ const app = express();
25
+ registerSkillRoutes(app, {
26
+ fs: { existsSync: () => false }, path: await import('node:path'), os: await import('node:os'),
27
+ resolveOptionalProjectDirectory: async () => ({ directory: '/repo' }),
28
+ buildOpenCodeUrl: () => 'http://opencode-upstream:4096/',
29
+ getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic example' }), getOpenCodePort: () => 4096,
30
+ SKILL_SCOPE: { PROJECT: 'project', USER: 'user' }, discoverSkills: () => [], mergeDiscoveredSkills: (skills) => skills,
31
+ });
32
+ const response = await request(app).get('/api/config/skills?summary=true').expect(200);
33
+ expect(response.body.skills[0].name).toBe('agent-tracker-api');
34
+ expect(upstreamRequests).toHaveLength(1);
35
+ expect(upstreamRequests[0].headers.get('authorization')).toBe('Basic example');
36
+ expect(upstreamRequests[0].url).toContain(encodeURIComponent('/repo'));
37
+ } finally {
38
+ globalThis.fetch = originalFetch;
39
+ OpenCode.make.mockReset();
40
+ }
41
+ });
42
+
11
43
  it('returns compact normalized skill data without content or sources', async () => {
12
44
  OpenCode.make.mockReturnValue({
13
45
  skill: {
14
46
  list: vi.fn(async () => ({ data: [{
15
- name: 'skill',
16
- location: '/repo/.opencode/skills/skill/SKILL.md',
47
+ id: 'skill',
48
+ name: 'Skill Display Name',
49
+ path: '/repo/.opencode/skills/skill/SKILL.md',
17
50
  description: ` ${'😀'.repeat(161)}\nnext `,
18
51
  content: 'secret skill content',
19
52
  }] })),
@@ -3,8 +3,23 @@ import fsPromises from 'fs/promises';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
5
  import { getSkillSources, mergeDiscoveredSkills } from './skills.js';
6
+ import { addSkillFromMdFile } from './shared.js';
6
7
 
7
8
  describe('skills', () => {
9
+ it('uses the path-derived id even when frontmatter has a different display name', async () => {
10
+ const root = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skill-id-'));
11
+ try {
12
+ const dir = path.join(root, 'release');
13
+ await fsPromises.mkdir(dir);
14
+ const file = path.join(dir, 'SKILL.md');
15
+ await fsPromises.writeFile(file, '---\nname: Git Release\ndescription: Release notes\n---\nInstructions');
16
+ const skills = new Map();
17
+ addSkillFromMdFile(skills, file, 'project', 'opencode');
18
+ expect([...skills.keys()]).toEqual(['release']);
19
+ } finally {
20
+ await fsPromises.rm(root, { recursive: true, force: true });
21
+ }
22
+ });
8
23
  it('merges locally discovered skills missing from OpenCode live discovery', () => {
9
24
  const merged = mergeDiscoveredSkills(
10
25
  [
@@ -23,6 +23,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
23
23
  getOpenCodeAuthHeaders,
24
24
  globalEventHub,
25
25
  processForwardedEventPayload,
26
+ projectOutboundSessionPayload = null,
27
+ ensureSessionMetadataReady = null,
26
28
  messageStreamWsClients,
27
29
  triggerHealthCheck,
28
30
  upstreamStallTimeoutMs,
@@ -83,6 +85,8 @@ export const createStartupPipelineRuntime = (dependencies) => {
83
85
  getOpenCodeAuthHeaders,
84
86
  globalEventHub,
85
87
  processForwardedEventPayload,
88
+ projectOutboundSessionPayload,
89
+ ensureSessionMetadataReady,
86
90
  wsClients: messageStreamWsClients,
87
91
  triggerHealthCheck,
88
92
  upstreamStallTimeoutMs,
@@ -47,8 +47,10 @@ export const createStaticRoutesRuntime = (dependencies) => {
47
47
  normalizePwaOrientation,
48
48
  });
49
49
 
50
+ // `root` limits send's dotfile check to `index.html`; an absolute path
51
+ // would 404 whenever the install lives under a dot directory (~/.bun, ~/.local).
50
52
  app.get(/^(?!\/api|.*\.(js|css|svg|png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot|map)).*$/, (_req, res) => {
51
- res.sendFile(path.join(distPath, 'index.html'));
53
+ res.sendFile('index.html', { root: distPath });
52
54
  });
53
55
  return;
54
56
  }
@@ -1,4 +1,7 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import express from 'express';
3
6
  import request from 'supertest';
4
7
  import { createStaticRoutesRuntime } from './static-routes-runtime.js';
@@ -57,4 +60,34 @@ describe('static routes runtime', () => {
57
60
  expect(auth.body).not.toEqual({ ok: true, mode: 'api-only', message: 'OpenChamber is running in API-only mode' });
58
61
  expect(health.body).not.toEqual({ ok: true, mode: 'api-only', message: 'OpenChamber is running in API-only mode' });
59
62
  });
63
+
64
+ it('serves index.html for deep links when dist lives under a dot directory', async () => {
65
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-static-'));
66
+ const distPath = path.join(root, '.bun', 'install', 'dist');
67
+ fs.mkdirSync(distPath, { recursive: true });
68
+ fs.writeFileSync(path.join(distPath, 'index.html'), '<!doctype html><title>OpenChamber</title>');
69
+ try {
70
+ const app = express();
71
+ createStaticRoutesRuntime({
72
+ fs,
73
+ path,
74
+ process: { env: { OPENCHAMBER_DIST_DIR: distPath } },
75
+ __dirname: '/server',
76
+ express,
77
+ resolveProjectDirectory: () => '',
78
+ buildOpenCodeUrl: () => '',
79
+ getOpenCodeAuthHeaders: () => ({}),
80
+ readSettingsFromDiskMigrated: async () => ({}),
81
+ normalizePwaAppName: (value) => value,
82
+ normalizePwaOrientation: (value) => value,
83
+ }).registerStaticRoutes(app);
84
+
85
+ const response = await request(app).get('/session/ses_abc');
86
+
87
+ expect(response.status).toBe(200);
88
+ expect(response.text).toContain('<title>OpenChamber</title>');
89
+ } finally {
90
+ fs.rmSync(root, { recursive: true, force: true });
91
+ }
92
+ });
60
93
  });
@@ -1,4 +1,5 @@
1
1
  import { OpenCode } from '@opencode/client';
2
+ import { wrapFetchWithRuntimeContractGate } from './server-opencode-fetch.js';
2
3
 
3
4
  /**
4
5
  * @typedef {import('@opencode/client').OpenCodeClient} OpenCodeV2ClientRaw
@@ -19,15 +20,25 @@ import { OpenCode } from '@opencode/client';
19
20
 
20
21
  /**
21
22
  * Create the single server-side client for official OpenCode v2 APIs.
23
+ * Fetch is always wrapped with runtime-contract write admission so Host
24
+ * background clients (queue / scheduled / assistants) cannot bypass the proxy gate.
22
25
  *
23
- * @param {{ baseUrl: string, authHeaders?: Record<string, string>, fetchImpl?: typeof fetch }} input
26
+ * @param {{
27
+ * baseUrl: string,
28
+ * authHeaders?: Record<string, string>,
29
+ * fetchImpl?: typeof fetch,
30
+ * getRuntimeContract?: () => (object | null | undefined),
31
+ * }} input
24
32
  * @returns {OpenCodeV2Client}
25
33
  */
26
- export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl }) {
34
+ export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl, getRuntimeContract }) {
35
+ const gatedFetch = wrapFetchWithRuntimeContractGate(fetchImpl || fetch, {
36
+ ...(typeof getRuntimeContract === 'function' ? { getRuntimeContract } : {}),
37
+ });
27
38
  const client = OpenCode.make({
28
39
  baseUrl: baseUrl.replace(/\/$/, ''),
29
40
  ...(authHeaders ? { headers: authHeaders } : {}),
30
- ...(fetchImpl ? { fetch: fetchImpl } : {}),
41
+ fetch: gatedFetch,
31
42
  });
32
43
 
33
44
  const messageGet = client.session.message?.get?.bind(client.session.message);