@openchambery/web 2.0.0-beta.3 → 2.0.0-beta.30

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 (292) hide show
  1. package/README.md +14 -0
  2. package/dist/assets/{AgentAvatar-B_JQoreg.js → AgentAvatar-O2x_1cFU.js} +1 -1
  3. package/dist/assets/AssistantView-pBywAsqy.js +3 -0
  4. package/dist/assets/{DiagramView-BNnda8qO.js → DiagramView-B984aE-R.js} +1 -1
  5. package/dist/assets/{FadeInOnReveal-VYQTSAkr.js → FadeInOnReveal-CMBd5BTd.js} +1 -1
  6. package/dist/assets/Icon-BN87CyS4.js +1 -0
  7. package/dist/assets/MarkdownRenderer-DHiNU8n3.js +2 -0
  8. package/dist/assets/{MarkdownRendererImpl-DwrHFGUp.js → MarkdownRendererImpl-xJHMGHju.js} +3 -3
  9. package/dist/assets/MarkstreamRendererImpl-BZflns6x.js +3 -0
  10. package/dist/assets/{MobileDetailNavigation-BXr6AGLT.js → MobileDetailNavigation-DUvxsvrB.js} +1 -1
  11. package/dist/assets/{MobileShareBridge-D7OE9OP_.js → MobileShareBridge-DWo-J3Vc.js} +3 -3
  12. package/dist/assets/MobileSurface-B-Y6U5xm.js +2 -0
  13. package/dist/assets/{MultiRunWindow-DF3iIFCd.js → MultiRunWindow-rYiVSNKK.js} +1 -1
  14. package/dist/assets/{OnboardingScreen-BjHmlwlw.js → OnboardingScreen-BLjZIBOs.js} +1 -1
  15. package/dist/assets/{OverlayScrollbar-DLtWO_Gp.js → OverlayScrollbar-CoZ4wtau.js} +1 -1
  16. package/dist/assets/SettingsView-pjD0LrHE.js +25 -0
  17. package/dist/assets/{SettingsWindow-BI5swspb.js → SettingsWindow-CFOQRwjA.js} +1 -1
  18. package/dist/assets/TerminalView-Dw5jaYkc.js +8 -0
  19. package/dist/assets/{ToolOutputDialog-BEWhBcRd.js → ToolOutputDialog-B4v-ona2.js} +1 -1
  20. package/dist/assets/appThemeRegistry-BqDCGocy.js +1 -0
  21. package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-sG8YQsVs.js} +1 -1
  22. package/dist/assets/{button-DKuz3L8l.js → button-Ck8ipzBm.js} +1 -1
  23. package/dist/assets/collapsible-nU99prkD.js +1 -0
  24. package/dist/assets/context-menu-DMzZN1oi.js +1 -0
  25. package/dist/assets/{desktopHosts-CoDkOnyS.js → desktopHosts-DuftEj8B.js} +1 -1
  26. package/dist/assets/desktopRelayRestore-g378BdZf.js +1 -0
  27. package/dist/assets/{diagram-D7T8HQgj.js → diagram-CvO4P8ds.js} +1 -1
  28. package/dist/assets/dialog-ByGUkS3c.js +1 -0
  29. package/dist/assets/dropdown-menu.styles-ayLWk2gf.js +1 -0
  30. package/dist/assets/es-DCjl24dV.js +15 -0
  31. package/dist/assets/fileMentionTouchSelection-Cgkxwzqb.js +2 -0
  32. package/dist/assets/{fileQueries-JrpcsOCs.js → fileQueries-DuF89nFO.js} +1 -1
  33. package/dist/assets/fr-DfJ5j0t5.js +13 -0
  34. package/dist/assets/{gitApi-CM42qUed.js → gitApi-Dwqkc2nZ.js} +3 -3
  35. package/dist/assets/{imageSource-CIl0FL10.js → imageSource-BpUsiwFd.js} +1 -1
  36. package/dist/assets/{input-CzQy8UhV.js → input-CXhFmHcq.js} +1 -1
  37. package/dist/assets/insertionBoundaries-BhWfAOPY.js +1 -0
  38. package/dist/assets/ja-B_9_w2Xf.js +15 -0
  39. package/dist/assets/ko-DlGbo3kV.js +15 -0
  40. package/dist/assets/main-CGMinhgQ.js +169 -0
  41. package/dist/assets/main-pMpcLJha.js +2 -0
  42. package/dist/assets/{markdownHeightCache-Cj1I5NDw.js → markdownHeightCache-g_UsWzbA.js} +1 -1
  43. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  44. package/dist/assets/{mcpQueries-D5mQymgM.js → mcpQueries-DfrW3VHF.js} +1 -1
  45. package/dist/assets/miniChat-YOt30ECc.js +2 -0
  46. package/dist/assets/mobile-BK1bBCYD.js +2 -0
  47. package/dist/assets/multirun-Dua24A69.js +3 -0
  48. package/dist/assets/persistence-Dnj1QOdX.js +3 -0
  49. package/dist/assets/pl-DjUsS8Ei.js +15 -0
  50. package/dist/assets/platform-X3Y3GyM5.js +6 -0
  51. package/dist/assets/projectMeta-DXdoPmbz.js +1 -0
  52. package/dist/assets/pt-BR-B8sV7qo1.js +15 -0
  53. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-D2XooXKZ.js} +1 -1
  54. package/dist/assets/renderElectronMiniChatApp-BOuEz_oT.js +2 -0
  55. package/dist/assets/renderMobileApp-C8Alpv-B.js +28 -0
  56. package/dist/assets/runtime-fetch-CiqQd__Y.js +1 -0
  57. package/dist/assets/runtime-switch-BJM2k_uH.js +1 -0
  58. package/dist/assets/runtimeConfig-DX_NP2Zm.js +2 -0
  59. package/dist/assets/runtimeEndpointReset-BSQU3e5U.js +148 -0
  60. package/dist/assets/runtimeEndpointReset-DsuP5FPT.css +1 -0
  61. package/dist/assets/sessionChangeSummary-8AIBkExf.js +2 -0
  62. package/dist/assets/{sessionLookup-myHe5ltu.js → sessionLookup-Bdwd1Fit.js} +1 -1
  63. package/dist/assets/{sortable-tabs-strip-BQtmHx9r.js → sortable-tabs-strip-T-dk_L69.js} +1 -1
  64. package/dist/assets/src-OzbnJsph.css +2 -0
  65. package/dist/assets/store-Cuktref-.js +15 -0
  66. package/dist/assets/{terminalApi-BxcdiduX.js → terminalApi-C_4joKUG.js} +1 -1
  67. package/dist/assets/textarea-biAqiqnJ.js +1 -0
  68. package/dist/assets/{tooltip-Cx5gTbdY.js → tooltip-BKL7J5hx.js} +1 -1
  69. package/dist/assets/uk-Bh7HXyLp.js +15 -0
  70. package/dist/assets/{url-CDHnjlsm.js → url-DNRMq_0R.js} +1 -1
  71. package/dist/assets/{useAppFontEffects-CNeo0aZs.js → useAppFontEffects-bafOEpgK.js} +57 -52
  72. package/dist/assets/{useAssistantUIStore-asNG6K7n.js → useAssistantUIStore-DBwfg9nX.js} +1 -1
  73. package/dist/assets/useDesktopWindowControlsLayout-CkCVOkxa.js +1 -0
  74. package/dist/assets/{useEffectiveDirectory-B4LvR8Gt.js → useEffectiveDirectory-B7htpJfU.js} +1 -1
  75. package/dist/assets/{useFontPreferences-CUnQ4TVm.js → useFontPreferences-CMrbTA0r.js} +1 -1
  76. package/dist/assets/useMobileNavigationStore-D2tnhKYV.js +415 -0
  77. package/dist/assets/{useRuntimeAPIs-DwLtO0c_.js → useRuntimeAPIs-DSLXJybW.js} +1 -1
  78. package/dist/assets/useSessionAutoCleanup-BC5OOH3M.js +2 -0
  79. package/dist/assets/{useThemeSystem-BlNUWH47.js → useThemeSystem-DMzWVJaW.js} +1 -1
  80. package/dist/assets/useUIStore-Dfx6TKvi.js +1 -0
  81. package/dist/assets/{useWorkerHighlightedLines-CJZbJDW3.js → useWorkerHighlightedLines-BNQUAMEB.js} +1 -1
  82. package/dist/assets/{vendor-base-ui-CDjyofeD.js → vendor-base-ui-CnHfUmDN.js} +1 -1
  83. package/dist/assets/{vendor-cmdk-BFH0UpXX.js → vendor-cmdk-CeAGfrLo.js} +1 -1
  84. package/dist/assets/{vendor-dnd-kit-accessibility-BrR3AKvM.js → vendor-dnd-kit-accessibility-BY5LhhUB.js} +1 -1
  85. package/dist/assets/{vendor-dnd-kit-core-CNfdvk6H.js → vendor-dnd-kit-core-CvWxUPdb.js} +1 -1
  86. package/dist/assets/{vendor-dnd-kit-sortable-DQUaTeGj.js → vendor-dnd-kit-sortable-hHdP9lLb.js} +1 -1
  87. package/dist/assets/{vendor-framer-motion-4dogsUk3.js → vendor-framer-motion-DFy_BSVd.js} +1 -1
  88. package/dist/assets/{vendor-legendapp-list-BU7MdPZR.js → vendor-legendapp-list-WE32lwNk.js} +1 -1
  89. package/dist/assets/{vendor-markstream-react-qHC2FykU.js → vendor-markstream-react-DX55XLGO.js} +3 -3
  90. package/dist/assets/vendor-opencode-client-MF2GBrFx.js +12 -0
  91. package/dist/assets/{vendor-pierre-diffs-CUfqC003.js → vendor-pierre-diffs-DX7XTPG3.js} +1 -1
  92. package/dist/assets/{vendor-react-C94A0AHl.js → vendor-react-DLPElpoQ.js} +1 -1
  93. package/dist/assets/{vendor-react-drawio-FVlWqKVE.js → vendor-react-drawio-ECtEwvbc.js} +1 -1
  94. package/dist/assets/{vendor-reactuses-core-C2A3EjUi.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  95. package/dist/assets/{vendor-sonner-DNqHmUQs.js → vendor-sonner-DVzioF3d.js} +1 -1
  96. package/dist/assets/{vendor-stream-diffs-CwbKsmlv.js → vendor-stream-diffs-DWy3xSmp.js} +2 -2
  97. package/dist/assets/{vendor-tanstack-react-query-D0iFK57C.js → vendor-tanstack-react-query-D7pys_c9.js} +1 -1
  98. package/dist/assets/{vendor-tanstack-react-virtual-B2kV6cnT.js → vendor-tanstack-react-virtual-tlDgH6PG.js} +1 -1
  99. package/dist/assets/{vendor-zustand-BqLKf6ZY.js → vendor-zustand-DzRqcymP.js} +2 -2
  100. package/dist/assets/zh-CN-Uzia0gk4.js +15 -0
  101. package/dist/assets/zh-TW-CiI7DLdB.js +15 -0
  102. package/dist/index.html +15 -15
  103. package/dist/mini-chat.html +15 -15
  104. package/dist/mobile.html +15 -15
  105. package/package.json +3 -2
  106. package/server/index.js +46 -27
  107. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  108. package/server/lib/browser-provider/contract.js +312 -0
  109. package/server/lib/browser-provider/host.js +421 -0
  110. package/server/lib/browser-provider/host.test.js +304 -0
  111. package/server/lib/browser-provider/routes.js +122 -0
  112. package/server/lib/browser-provider/routes.test.js +120 -0
  113. package/server/lib/browser-provider/selection-store.js +43 -0
  114. package/server/lib/browser-provider/selection-store.test.js +35 -0
  115. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  116. package/server/lib/event-stream/reasoning-projection.js +16 -1
  117. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  118. package/server/lib/git/DOCUMENTATION.md +1 -0
  119. package/server/lib/git/routes.js +7 -4
  120. package/server/lib/git/routes.test.js +34 -0
  121. package/server/lib/git/service.js +3 -1
  122. package/server/lib/llm/DOCUMENTATION.md +6 -4
  123. package/server/lib/llm/generate.js +8 -9
  124. package/server/lib/llm/generate.test.js +33 -6
  125. package/server/lib/message-queue/service.js +1 -1
  126. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  127. package/server/lib/notifications/runtime.js +128 -31
  128. package/server/lib/notifications/runtime.test.js +103 -0
  129. package/server/lib/notifications/template-runtime.js +130 -38
  130. package/server/lib/notifications/template-runtime.test.js +96 -0
  131. package/server/lib/opencode/DOCUMENTATION.md +36 -19
  132. package/server/lib/opencode/agents.js +13 -22
  133. package/server/lib/opencode/auth-state-runtime.js +3 -3
  134. package/server/lib/opencode/auth-state-runtime.test.js +59 -0
  135. package/server/lib/opencode/auth.d.ts +13 -0
  136. package/server/lib/opencode/auth.js +74 -64
  137. package/server/lib/opencode/auth.test.js +57 -0
  138. package/server/lib/opencode/commands.js +1 -1
  139. package/server/lib/opencode/config-entities-layering.test.js +72 -0
  140. package/server/lib/opencode/config-entity-routes.js +16 -124
  141. package/server/lib/opencode/config-entity-routes.test.js +48 -11
  142. package/server/lib/opencode/config-files.d.ts +17 -0
  143. package/server/lib/opencode/config-files.js +187 -0
  144. package/server/lib/opencode/config-files.test.js +201 -0
  145. package/server/lib/opencode/core-routes.js +0 -1
  146. package/server/lib/opencode/core-routes.test.js +0 -15
  147. package/server/lib/opencode/credential-boundary.d.ts +1 -0
  148. package/server/lib/opencode/credential-boundary.js +11 -0
  149. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  150. package/server/lib/opencode/ensure-cli.js +4 -1
  151. package/server/lib/opencode/env-config.js +4 -1
  152. package/server/lib/opencode/env-config.test.js +28 -0
  153. package/server/lib/opencode/env-runtime.test.js +18 -1
  154. package/server/lib/opencode/feature-routes-runtime.js +19 -7
  155. package/server/lib/opencode/feature-routes-runtime.test.js +7 -0
  156. package/server/lib/opencode/hmr-state-runtime.js +4 -4
  157. package/server/lib/opencode/lifecycle.js +1 -49
  158. package/server/lib/opencode/lifecycle.test.js +30 -4
  159. package/server/lib/opencode/location-release.d.ts +8 -0
  160. package/server/lib/opencode/location-release.js +32 -0
  161. package/server/lib/opencode/location-release.test.js +43 -0
  162. package/server/lib/opencode/mcp.js +1 -1
  163. package/server/lib/opencode/opencode2-pin.js +2 -1
  164. package/server/lib/opencode/owned-runtime-upgrade.test.js +5 -5
  165. package/server/lib/opencode/plugin-routes.test.js +4 -2
  166. package/server/lib/opencode/plugins.js +12 -60
  167. package/server/lib/opencode/plugins.test.js +5 -3
  168. package/server/lib/opencode/providers.js +6 -3
  169. package/server/lib/opencode/proxy.js +13 -1
  170. package/server/lib/opencode/proxy.test.js +31 -3
  171. package/server/lib/opencode/remote-upgrade.integration.test.js +66 -0
  172. package/server/lib/opencode/routes.js +40 -8
  173. package/server/lib/opencode/routes.test.js +51 -10
  174. package/server/lib/opencode/runtime-contract.js +3 -2
  175. package/server/lib/opencode/runtime-contract.test.js +12 -12
  176. package/server/lib/opencode/shared.js +6 -175
  177. package/server/lib/opencode/update-discovery.js +39 -0
  178. package/server/lib/opencode/update-discovery.test.js +39 -0
  179. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  180. package/server/lib/opencode/upgrade-screen.js +292 -0
  181. package/server/lib/opencode/upgrade-screen.test.js +196 -0
  182. package/server/lib/opencode/version-alignment.test.js +40 -0
  183. package/server/lib/question-auto-delegate/DOCUMENTATION.md +21 -6
  184. package/server/lib/question-auto-delegate/core.d.ts +5 -1
  185. package/server/lib/question-auto-delegate/core.js +21 -13
  186. package/server/lib/question-auto-delegate/form-io.d.ts +6 -0
  187. package/server/lib/question-auto-delegate/form-io.js +39 -0
  188. package/server/lib/question-auto-delegate/runtime.js +4 -24
  189. package/server/lib/question-auto-delegate/runtime.test.js +138 -0
  190. package/server/lib/quota/DOCUMENTATION.md +9 -0
  191. package/server/lib/quota/provider-credentials.test.js +29 -0
  192. package/server/lib/quota/providers/claude.js +4 -4
  193. package/server/lib/quota/providers/codex.js +4 -4
  194. package/server/lib/quota/providers/codex.test.js +1 -1
  195. package/server/lib/quota/providers/copilot.js +5 -5
  196. package/server/lib/quota/providers/google/auth.js +3 -3
  197. package/server/lib/quota/providers/google/index.js +2 -2
  198. package/server/lib/quota/providers/index.js +3 -7
  199. package/server/lib/quota/providers/index.test.js +2 -2
  200. package/server/lib/quota/providers/kimi.js +4 -4
  201. package/server/lib/quota/providers/minimax-shared.js +4 -4
  202. package/server/lib/quota/providers/nanogpt.js +4 -4
  203. package/server/lib/quota/providers/openai.js +4 -4
  204. package/server/lib/quota/providers/openrouter.js +4 -4
  205. package/server/lib/quota/providers/wafer.js +4 -4
  206. package/server/lib/quota/providers/zai.js +4 -4
  207. package/server/lib/quota/providers/zhipuai-coding-plan.js +6 -20
  208. package/server/lib/quota/routes.js +1 -1
  209. package/server/lib/relay/DOCUMENTATION.md +3 -1
  210. package/server/lib/relay/service.js +1 -0
  211. package/server/lib/relay/service.test.js +13 -0
  212. package/server/lib/scheduled-tasks/DOCUMENTATION.md +8 -1
  213. package/server/lib/scheduled-tasks/runtime.js +12 -4
  214. package/server/lib/scheduled-tasks/runtime.test.js +81 -6
  215. package/server/lib/session-goal/DOCUMENTATION.md +11 -0
  216. package/server/lib/session-goal/runtime.js +4 -4
  217. package/server/lib/session-goal/runtime.test.js +32 -3
  218. package/server/lib/session-index/DOCUMENTATION.md +7 -0
  219. package/server/lib/session-index/event-ingest.js +13 -0
  220. package/server/lib/session-index/event-ingest.test.js +7 -0
  221. package/server/lib/session-index/service.js +45 -0
  222. package/server/lib/session-index/service.test.js +47 -0
  223. package/server/lib/session-metadata/DOCUMENTATION.md +33 -17
  224. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  225. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  226. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  227. package/server/lib/session-metadata/routes.js +5 -1
  228. package/server/lib/session-metadata/session-archive.js +1 -1
  229. package/server/lib/session-metadata/session-metadata-store.d.ts +2 -0
  230. package/server/lib/session-metadata/session-metadata-store.js +63 -16
  231. package/server/lib/session-metadata/session-metadata-store.test.js +23 -0
  232. package/server/lib/session-metadata/session-projection.d.ts +1 -0
  233. package/server/lib/session-metadata/session-projection.js +49 -2
  234. package/server/lib/session-metadata/session-projection.test.js +35 -0
  235. package/server/lib/session-title/DOCUMENTATION.md +52 -14
  236. package/server/lib/session-title/native-runtime.test.js +59 -5
  237. package/server/lib/session-title/runtime.js +65 -19
  238. package/server/lib/session-title/session-access.js +108 -3
  239. package/server/lib/session-title/session-access.test.js +125 -7
  240. package/server/lib/tts/DOCUMENTATION.md +7 -5
  241. package/server/lib/tts/credential-source.test.js +23 -0
  242. package/server/lib/tts/routes.js +2 -2
  243. package/server/lib/tts/service.js +16 -37
  244. package/dist/assets/AssistantView-ClQAfGOT.js +0 -3
  245. package/dist/assets/Icon-BiEQEcih.js +0 -1
  246. package/dist/assets/MarkdownRenderer-CJHSsWin.js +0 -2
  247. package/dist/assets/MarkstreamRendererImpl-CSnbQMu3.js +0 -3
  248. package/dist/assets/MobileSurface-D2jUkX9l.js +0 -2
  249. package/dist/assets/SettingsView-BRxELjow.js +0 -25
  250. package/dist/assets/TerminalView-BU3x8-fr.js +0 -8
  251. package/dist/assets/appThemeRegistry-4MR0F7Jz.js +0 -1
  252. package/dist/assets/collapsible-DXbDynN4.js +0 -3
  253. package/dist/assets/context-menu-CqolsLCm.js +0 -1
  254. package/dist/assets/desktopRelayRestore-DDlVKyOE.js +0 -1
  255. package/dist/assets/dialog-DQYl3W_o.js +0 -1
  256. package/dist/assets/dropdown-menu.styles-Bzo6L3FH.js +0 -1
  257. package/dist/assets/es-CN3E_zXj.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BZ6ovBYH.js +0 -4
  259. package/dist/assets/fr-9zZvQZD2.js +0 -13
  260. package/dist/assets/insertionBoundaries-CtkZ9720.js +0 -1
  261. package/dist/assets/ja-Dnjk_82N.js +0 -15
  262. package/dist/assets/ko-BxCEE99C.js +0 -15
  263. package/dist/assets/main-BILgBI3B.js +0 -169
  264. package/dist/assets/main-BeveiW6F.js +0 -2
  265. package/dist/assets/markdownTheme-CKac3wKC.js +0 -1
  266. package/dist/assets/miniChat-BgrzM3u7.js +0 -2
  267. package/dist/assets/mobile-DtYoQE8C.js +0 -2
  268. package/dist/assets/multirun-jy8dEI96.js +0 -4
  269. package/dist/assets/persistence-C9LQpiOS.js +0 -3
  270. package/dist/assets/pl-CNNsWnPM.js +0 -15
  271. package/dist/assets/platform-BQghHRfp.js +0 -6
  272. package/dist/assets/pt-BR-BZB2DJMp.js +0 -15
  273. package/dist/assets/renderElectronMiniChatApp-BGrrmeME.js +0 -2
  274. package/dist/assets/renderMobileApp-u3c4GtFC.js +0 -28
  275. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  276. package/dist/assets/runtime-switch-LvWDU-SL.js +0 -1
  277. package/dist/assets/runtimeConfig-BIGBRGQI.js +0 -2
  278. package/dist/assets/runtimeEndpointReset-D2savkuw.js +0 -147
  279. package/dist/assets/sessionChangeSummary-eIbpSfrO.js +0 -2
  280. package/dist/assets/src-CLN7Z-zx.css +0 -2
  281. package/dist/assets/store-DLCiZaA6.js +0 -15
  282. package/dist/assets/textarea-CakKvqFw.js +0 -1
  283. package/dist/assets/uk-rwQrxeYM.js +0 -15
  284. package/dist/assets/useDesktopWindowControlsLayout-DBJ56qIo.js +0 -1
  285. package/dist/assets/useMobileNavigationStore-ZHJPxlHt.js +0 -414
  286. package/dist/assets/useSessionAutoCleanup-BIVmrnYU.js +0 -2
  287. package/dist/assets/useUIStore-CQYKJnrg.js +0 -1
  288. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  289. package/dist/assets/zh-CN-BvmBDnkD.js +0 -15
  290. package/dist/assets/zh-TW-9wcHTX0y.js +0 -15
  291. package/server/lib/opencode/snippets.js +0 -242
  292. package/server/lib/opencode/snippets.test.js +0 -97
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Explicit worktree deletion only. An eviction acknowledgement detaches cached
3
+ * services; it does not prove that borrowers have released every OS handle.
4
+ * Never use this operation for automatic navigation cleanup on a shared serve.
5
+ *
6
+ * @param {{ getClient: () => import('@opencode/client').OpenCodeClient | null, directory: string, timeoutMs?: number }} input
7
+ * @returns {Promise<{ state: 'released' | 'timeout' | 'failed' | 'unavailable' }>}
8
+ */
9
+ export async function releaseDeletedWorktreeLocation({ getClient, directory, timeoutMs = 2000 }) {
10
+ const controller = new AbortController();
11
+ let timer;
12
+ try {
13
+ if (!directory || !directory.trim()) return { state: 'unavailable' };
14
+ const client = getClient();
15
+ if (!client) return { state: 'unavailable' };
16
+ const deadline = new Promise((resolve) => {
17
+ timer = setTimeout(() => {
18
+ controller.abort();
19
+ resolve({ state: 'timeout' });
20
+ }, timeoutMs);
21
+ });
22
+ const request = Promise.resolve().then(() => client.debug.location.evict(
23
+ { location: { directory } },
24
+ { signal: controller.signal },
25
+ )).then(() => ({ state: 'released' }), () => ({ state: controller.signal.aborted ? 'timeout' : 'failed' }));
26
+ return await Promise.race([request, deadline]);
27
+ } catch {
28
+ return { state: 'failed' };
29
+ } finally {
30
+ clearTimeout(timer);
31
+ }
32
+ }
@@ -0,0 +1,43 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { OpenCode } from '@opencode/client';
3
+ import { releaseDeletedWorktreeLocation } from './location-release.js';
4
+
5
+ afterEach(() => vi.useRealTimers());
6
+
7
+ describe('explicit worktree location release', () => {
8
+ it('uses the official SDK exact target, current origin and auth', async () => {
9
+ const requests = [];
10
+ let origin = 'https://old.invalid';
11
+ const getClient = () => OpenCode.make({ baseUrl: origin, headers: { Authorization: 'test-auth' }, fetch: async (input, init) => {
12
+ requests.push(new Request(input, init));
13
+ return new Response(null, { status: 204 });
14
+ } });
15
+ origin = 'https://current.invalid';
16
+ expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo/work tree' })).toEqual({ state: 'released' });
17
+ expect(requests).toHaveLength(1);
18
+ const url = new URL(requests[0].url);
19
+ expect(url.origin + url.pathname).toBe('https://current.invalid/api/debug/location');
20
+ expect(url.searchParams.get('location[directory]')).toBe('/repo/work tree');
21
+ expect(requests[0].method).toBe('DELETE');
22
+ expect(requests[0].headers.get('Authorization')).toBe('test-auth');
23
+ });
24
+
25
+ it('reports failure without claiming successful disposal', async () => {
26
+ const getClient = () => ({ debug: { location: { evict: async () => { throw new Error('unavailable'); } } } });
27
+ expect(await releaseDeletedWorktreeLocation({ getClient, directory: '/repo' })).toEqual({ state: 'failed' });
28
+ expect(await releaseDeletedWorktreeLocation({ getClient: () => null, directory: '/repo' })).toEqual({ state: 'unavailable' });
29
+ });
30
+
31
+ it('bounds even a transport that ignores abort and never settles', async () => {
32
+ vi.useFakeTimers();
33
+ let signal;
34
+ const getClient = () => ({ debug: { location: { evict: (_input, options) => {
35
+ signal = options.signal;
36
+ return new Promise(() => {});
37
+ } } } });
38
+ const result = releaseDeletedWorktreeLocation({ getClient, directory: '/repo', timeoutMs: 50 });
39
+ await vi.advanceTimersByTimeAsync(50);
40
+ expect(await result).toEqual({ state: 'timeout' });
41
+ expect(signal.aborted).toBe(true);
42
+ });
43
+ });
@@ -29,7 +29,7 @@ function validateMcpName(name) {
29
29
  */
30
30
  function resolveMcpScopeFromPath(layers, sourcePath) {
31
31
  if (!sourcePath) return null;
32
- return sourcePath === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
32
+ return layers.projectLayers.some((layer) => layer.path === sourcePath) ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
33
33
  }
34
34
 
35
35
  function ensureProjectMcpConfigPath(workingDirectory) {
@@ -1,5 +1,6 @@
1
1
  // Pinned desktop/runtime opencode2. Upgrade and prepare must not fall back to 1.18.x.
2
- export const PINNED_OPENCODE2_VERSION = '2.0.12';
2
+ export const PINNED_OPENCODE2_VERSION = '2.0.23';
3
+ export const MINIMUM_OPENCODE2_VERSION = '2.0.20';
3
4
 
4
5
  // Global npm/bun package that installs the opencode v2 binary (not 1.x opencode-ai).
5
6
  export const OPENCODE2_NPM_PACKAGE = '@opencode/cli';
@@ -86,15 +86,15 @@ describe('owned runtime upgrade (ticket 12)', () => {
86
86
 
87
87
  it('verifies running serve identity before upgrade success', () => {
88
88
  const contractOk = evaluateRuntimeContract({
89
- serveVersion: '2.0.14',
89
+ serveVersion: '2.0.23',
90
90
  reachable: true,
91
91
  authenticated: true,
92
92
  healthOk: true,
93
93
  migrationAdmitTranscript: true,
94
94
  });
95
95
  expect(evaluateOwnedUpgradeResult({
96
- targetVersion: '2.0.14',
97
- serveVersion: '2.0.14',
96
+ targetVersion: '2.0.23',
97
+ serveVersion: '2.0.23',
98
98
  binaryPath: ownedBinary,
99
99
  expectedBinaryPath: ownedBinary,
100
100
  contract: contractOk,
@@ -116,8 +116,8 @@ describe('owned runtime upgrade (ticket 12)', () => {
116
116
  });
117
117
 
118
118
  expect(evaluateOwnedUpgradeResult({
119
- targetVersion: '2.0.14',
120
- serveVersion: '2.0.14',
119
+ targetVersion: '2.0.23',
120
+ serveVersion: '2.0.23',
121
121
  binaryPath: '/usr/bin/opencode',
122
122
  expectedBinaryPath: ownedBinary,
123
123
  contract: contractOk,
@@ -1,4 +1,5 @@
1
1
  import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test';
2
+ import { vi } from 'vitest';
2
3
  import express from 'express';
3
4
  import fs from 'fs';
4
5
  import os from 'os';
@@ -67,7 +68,8 @@ describe('opencode plugin routes', () => {
67
68
  beforeAll(async () => {
68
69
  rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugin-routes-'));
69
70
  userConfigPath = path.join(rootDir, 'user-opencode.json');
70
- process.env.OPENCODE_CONFIG = userConfigPath;
71
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
72
+ vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
71
73
  plugins = await import('./plugins.js');
72
74
  });
73
75
 
@@ -91,7 +93,7 @@ describe('opencode plugin routes', () => {
91
93
 
92
94
  afterAll(() => {
93
95
  fs.rmSync(rootDir, { recursive: true, force: true });
94
- delete process.env.OPENCODE_CONFIG;
96
+ vi.unstubAllEnvs();
95
97
  });
96
98
 
97
99
  test('GET /api/config/plugins empty returns entries and files arrays', async () => {
@@ -4,6 +4,8 @@ import path from 'path';
4
4
  import {
5
5
  AGENT_SCOPE,
6
6
  readConfigFile,
7
+ readConfigLayers,
8
+ getConfigForPath,
7
9
  writeConfig,
8
10
  } from './shared.js';
9
11
  import { isPathSpec } from './plugin-spec.js';
@@ -74,54 +76,8 @@ function getActiveOpencodeConfigDir() {
74
76
  return path.join(os.homedir(), '.config', 'opencode');
75
77
  }
76
78
 
77
- function getActiveUserConfigPaths() {
78
- const configDir = getActiveOpencodeConfigDir();
79
- return [
80
- path.join(configDir, 'config.json'),
81
- path.join(configDir, 'opencode.json'),
82
- path.join(configDir, 'opencode.jsonc'),
83
- ];
84
- }
85
-
86
- function getActiveCustomConfigPath() {
87
- return process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : null;
88
- }
89
-
90
- function getPrimaryUserConfigPath() {
91
- const [defaultPath, ...fallbackPaths] = getActiveUserConfigPaths();
92
- for (const userPath of [defaultPath, ...fallbackPaths]) {
93
- if (fs.existsSync(userPath)) {
94
- return userPath;
95
- }
96
- }
97
- return defaultPath;
98
- }
99
-
100
- function getProjectConfigPath(workingDirectory) {
101
- if (!workingDirectory) return null;
102
- const candidates = [
103
- path.join(workingDirectory, 'opencode.json'),
104
- path.join(workingDirectory, 'opencode.jsonc'),
105
- path.join(workingDirectory, '.opencode', 'opencode.json'),
106
- path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
107
- ];
108
- return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
109
- }
110
-
111
79
  function readPluginConfigLayers(workingDirectory) {
112
- const customPath = getActiveCustomConfigPath();
113
- const userPath = getPrimaryUserConfigPath();
114
- const projectPath = getProjectConfigPath(workingDirectory);
115
- return {
116
- userConfig: readConfigFile(userPath),
117
- projectConfig: readConfigFile(projectPath),
118
- customConfig: readConfigFile(customPath),
119
- paths: {
120
- userPath,
121
- projectPath,
122
- customPath,
123
- },
124
- };
80
+ return readConfigLayers(workingDirectory);
125
81
  }
126
82
 
127
83
  function validateFileName(fileName) {
@@ -144,16 +100,10 @@ function ensureProjectConfigPath(workingDirectory) {
144
100
  }
145
101
 
146
102
  function configSources(layers) {
147
- const sources = [];
148
- if (layers.paths.customPath) {
149
- sources.push({ config: layers.customConfig, filePath: layers.paths.customPath, scope: AGENT_SCOPE.USER });
150
- } else {
151
- sources.push({ config: layers.userConfig, filePath: layers.paths.userPath, scope: AGENT_SCOPE.USER });
152
- }
153
- if (layers.paths.projectPath) {
154
- sources.push({ config: layers.projectConfig, filePath: layers.paths.projectPath, scope: AGENT_SCOPE.PROJECT });
155
- }
156
- return sources;
103
+ return [...layers.documents.filter((layer) => layer.scope !== 'project').reverse(), ...layers.projectLayers.toReversed()].map((layer) => ({
104
+ config: layer.config, filePath: layer.path,
105
+ scope: layer.scope === 'project' ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
106
+ }));
157
107
  }
158
108
 
159
109
  function splitScopedValue(value) {
@@ -175,7 +125,9 @@ function getPluginTarget(id, workingDirectory) {
175
125
  const { scope, value: spec } = splitScopedValue(decoded.value);
176
126
  validateScope(scope);
177
127
  const layers = readPluginConfigLayers(workingDirectory);
178
- const source = configSources(layers).find((candidate) => candidate.scope === scope);
128
+ const source = configSources(layers).find((candidate) => candidate.scope === scope
129
+ && Array.isArray(candidate.config?.plugin)
130
+ && candidate.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec));
179
131
  const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
180
132
  const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
181
133
  if (!source || index === -1) {
@@ -281,14 +233,14 @@ function createPluginEntry(entry, workingDirectory) {
281
233
  throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
282
234
  }
283
235
 
284
- let targetPath = getPrimaryUserConfigPath();
236
+ let targetPath = layers.paths.userPath;
285
237
  let config = {};
286
238
  if (scope === AGENT_SCOPE.PROJECT) {
287
239
  targetPath = ensureProjectConfigPath(workingDirectory);
288
240
  config = fs.existsSync(targetPath) ? readConfigFile(targetPath) : {};
289
241
  } else {
290
242
  targetPath = layers.paths.customPath || layers.paths.userPath;
291
- config = layers.paths.customPath ? layers.customConfig : layers.userConfig;
243
+ config = getConfigForPath(layers, targetPath);
292
244
  }
293
245
 
294
246
  if (!Array.isArray(config.plugin)) {
@@ -1,4 +1,5 @@
1
1
  import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { vi } from 'vitest';
2
3
  import fs from 'fs';
3
4
  import os from 'os';
4
5
  import path from 'path';
@@ -30,19 +31,20 @@ describe('opencode plugins data layer', () => {
30
31
  beforeAll(async () => {
31
32
  rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-plugins-'));
32
33
  userConfigPath = path.join(rootDir, 'user-opencode.json');
33
- process.env.OPENCODE_CONFIG = userConfigPath;
34
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
35
+ vi.stubEnv('OPENCODE_CONFIG_DIR', path.join(rootDir, 'global-config'));
34
36
  plugins = await import('./plugins.js');
35
37
  });
36
38
 
37
39
  beforeEach(() => {
38
- process.env.OPENCODE_CONFIG = userConfigPath;
40
+ vi.stubEnv('OPENCODE_CONFIG', userConfigPath);
39
41
  projectDir = fs.mkdtempSync(path.join(rootDir, 'project-'));
40
42
  fs.rmSync(userConfigPath, { force: true });
41
43
  });
42
44
 
43
45
  afterAll(() => {
44
46
  fs.rmSync(rootDir, { recursive: true, force: true });
45
- delete process.env.OPENCODE_CONFIG;
47
+ vi.unstubAllEnvs();
46
48
  });
47
49
 
48
50
  test('parses raw plugin entries', () => {
@@ -30,8 +30,8 @@ function getProviderSources(providerId, workingDirectory) {
30
30
  return {
31
31
  sources: {
32
32
  auth: { exists: false },
33
- user: { exists: userExists, path: paths.userPath },
34
- project: { exists: projectExists, path: paths.projectPath || null },
33
+ user: { exists: userExists, path: layers.userLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.userPath },
34
+ project: { exists: projectExists, path: layers.projectLayers.findLast((layer) => layer.config?.provider?.[providerId] !== undefined || layer.config?.providers?.[providerId] !== undefined)?.path || paths.projectPath || null },
35
35
  custom: { exists: customExists, path: paths.customPath }
36
36
  }
37
37
  };
@@ -44,17 +44,20 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
44
44
 
45
45
  const layers = readConfigLayers(workingDirectory);
46
46
  let targetPath = layers.paths.userPath;
47
+ const sources = getProviderSources(providerId, workingDirectory).sources;
47
48
 
48
49
  if (scope === 'project') {
49
50
  if (!workingDirectory) {
50
51
  throw new Error('Working directory is required for project scope');
51
52
  }
52
- targetPath = layers.paths.projectPath || targetPath;
53
+ targetPath = sources.project.path || targetPath;
53
54
  } else if (scope === 'custom') {
54
55
  if (!layers.paths.customPath) {
55
56
  return false;
56
57
  }
57
58
  targetPath = layers.paths.customPath;
59
+ } else {
60
+ targetPath = sources.user.path;
58
61
  }
59
62
 
60
63
  const targetConfig = getConfigForPath(layers, targetPath);
@@ -1,4 +1,5 @@
1
1
  import { createProxyMiddleware } from 'http-proxy-middleware';
2
+ import { isSensitiveCredentialResponse } from './credential-boundary.js';
2
3
 
3
4
  import {
4
5
  applyForwardProxyResponseHeaders,
@@ -244,6 +245,12 @@ export const resolveSessionTurnAdmissionRequest = (req) => {
244
245
  };
245
246
 
246
247
  export const registerOpenCodeProxy = (app, deps) => {
248
+ app.use((req, res, next) => {
249
+ if (isSensitiveCredentialResponse(req.method, req.originalUrl || req.url)) {
250
+ return res.status(403).json({ code: 'credential_response_refused', error: 'Provider credential values are host-only.' });
251
+ }
252
+ return next();
253
+ });
247
254
  const {
248
255
  fs,
249
256
  os,
@@ -957,7 +964,9 @@ export const registerOpenCodeProxy = (app, deps) => {
957
964
  req.path === '/opencode/version' ||
958
965
  req.path === '/opencode/contract' ||
959
966
  req.path === '/opencode/upgrade-status' ||
960
- req.path === '/opencode/upgrade'
967
+ req.path === '/opencode/upgrade' ||
968
+ req.path === '/opencode/compatibility' ||
969
+ req.path === '/opencode/install-required'
961
970
  ) {
962
971
  return next();
963
972
  }
@@ -1207,6 +1216,9 @@ export const registerOpenCodeProxy = (app, deps) => {
1207
1216
  app.get('/api/session/:sessionID/message', (req, res, next) => {
1208
1217
  return forwardSessionMessagesListRequest(req, res, next);
1209
1218
  });
1219
+ app.get('/api/session/:sessionID/context', (req, res, next) => {
1220
+ return forwardSessionMessagesListRequest(req, res, next);
1221
+ });
1210
1222
 
1211
1223
  app.get('/api/global/event', forwardSseRequest);
1212
1224
  app.get('/api/event', forwardSseRequest);
@@ -57,6 +57,19 @@ const mountProxy = () => {
57
57
  return app;
58
58
  };
59
59
 
60
+ describe('credential response boundary', () => {
61
+ it.each(['/api/credential', '/api/credential/', '/api/%63redential?directory=/repo'])('refuses raw credential list %s before upstream', async (path) => {
62
+ const fetchSpy = vi.fn();
63
+ globalThis.fetch = fetchSpy;
64
+ const response = await request(mountProxy()).get(path).expect(403);
65
+ expect(response.body.code).toBe('credential_response_refused');
66
+ expect(fetchSpy).not.toHaveBeenCalled();
67
+ });
68
+ it('refuses credential create replies which echo values', async () => {
69
+ await request(mountProxy()).post('/api/credential').send({}).expect(403);
70
+ });
71
+ });
72
+
60
73
  const sseBody = (blocks) => {
61
74
  const encoder = new TextEncoder();
62
75
  const text = blocks.join('');
@@ -460,6 +473,21 @@ describe('registerOpenCodeProxy reasoning projection routes', () => {
460
473
  expect(JSON.stringify(res.body)).not.toContain('hidden');
461
474
  });
462
475
 
476
+ it.each(['message', 'context'])('filters native v2 %s before outbound transport', async (surface) => {
477
+ const upstreamUrls = [];
478
+ globalThis.fetch = vi.fn(async (url) => {
479
+ upstreamUrls.push(String(url));
480
+ return new Response(JSON.stringify({ data: [{ id: 'm', type: 'assistant', content: [
481
+ { type: 'reasoning', text: 'hidden' }, { type: 'text', text: 'visible' },
482
+ ] }], cursor: { next: 'older' } }), { headers: { 'content-type': 'application/json' } });
483
+ });
484
+ const res = await request(mountProxy()).get(`/api/session/ses_1/${surface}?includeReasoning=false`);
485
+ expect(res.status).toBe(200);
486
+ expect(res.body.data[0].content).toEqual([{ type: 'text', text: 'visible' }]);
487
+ expect(res.body.cursor.next).toBe('older');
488
+ expect(upstreamUrls[0]).not.toContain('includeReasoning');
489
+ });
490
+
463
491
  it('keeps reasoning parts on session.messages list when includeReasoning is omitted', async () => {
464
492
  globalThis.fetch = vi.fn(async () => ({
465
493
  ok: true,
@@ -503,7 +531,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
503
531
  return app;
504
532
  };
505
533
 
506
- it('merges Host store metadata onto session list and detail (store keys win)', async () => {
534
+ it('merges side-store gaps onto the OpenCode record without letting the side row win conflicts', async () => {
507
535
  globalThis.fetch = vi.fn(async (url) => {
508
536
  const path = String(url).split('?')[0];
509
537
  if (path.endsWith('/api/session') || path.endsWith('/session')) {
@@ -551,7 +579,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
551
579
  id: 'ses_1',
552
580
  metadata: {
553
581
  fromOpenCode: true,
554
- shared: 'ours',
582
+ shared: 'theirs',
555
583
  openchamber: { goal: { status: 'active' } },
556
584
  },
557
585
  });
@@ -563,7 +591,7 @@ describe('registerOpenCodeProxy session metadata overlay', () => {
563
591
  id: 'ses_1',
564
592
  metadata: {
565
593
  fromOpenCode: true,
566
- shared: 'ours',
594
+ shared: 'theirs',
567
595
  openchamber: { goal: { status: 'active' } },
568
596
  },
569
597
  });
@@ -0,0 +1,66 @@
1
+ import { expect, test } from 'vitest';
2
+ import express from 'express';
3
+ import request from 'supertest';
4
+ import { execFile, spawn } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import net from 'node:net';
10
+ import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
11
+ import { installPinnedOpenCode2Cli } from './ensure-cli.js';
12
+ import { createSharedOpenCodeService } from './shared-service.js';
13
+ import { registerOpenCodeRoutes } from './routes.js';
14
+
15
+ // Downloads real official binaries; never touches the user's shared service.
16
+ test.skipIf(process.env.OPENCHAMBER_TEST_REMOTE_UPGRADE !== '1')('remote discovery upgrades an isolated shared service and verifies its running version', async () => {
17
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-upgrade-'));
18
+ const env = {
19
+ PATH: process.env.PATH, HOME: root, TMPDIR: os.tmpdir(),
20
+ XDG_CONFIG_HOME: path.join(root, 'config'), XDG_DATA_HOME: path.join(root, 'data'),
21
+ XDG_STATE_HOME: path.join(root, 'state'), XDG_CACHE_HOME: path.join(root, 'cache'),
22
+ OPENCODE_CONFIG_DIR: path.join(root, 'config', 'opencode'), OPENCODE_DISABLE_MODELS_FETCH: '1',
23
+ };
24
+ await fs.mkdir(env.OPENCODE_CONFIG_DIR, { recursive: true });
25
+ const listener = net.createServer();
26
+ await new Promise((resolve) => listener.listen(0, '127.0.0.1', resolve));
27
+ const port = listener.address().port;
28
+ await new Promise((resolve) => listener.close(resolve));
29
+ await fs.writeFile(path.join(env.OPENCODE_CONFIG_DIR, 'service.json'), JSON.stringify({ hostname: '127.0.0.1', port }));
30
+ const service = createSharedOpenCodeService({ envLike: env, spawnImpl: (command, args, options) => spawn(command, args, { ...options, cwd: root }) });
31
+ let binary;
32
+ let registration;
33
+ try {
34
+ const target = await createOpenCodeUpdateDiscovery()();
35
+ expect(target).not.toBe('2.0.16');
36
+ binary = await installPinnedOpenCode2Cli({ version: '2.0.16', dataDir: root });
37
+ await service.start({ binary, env });
38
+ registration = await service.readRegistration();
39
+ expect(registration.version).toBe('2.0.16');
40
+ const app = express();
41
+ app.use(express.json());
42
+ registerOpenCodeRoutes(app, {
43
+ getIsExternalOpenCode: () => true, getIsSharedOpenCodeService: () => true,
44
+ getResolvedOpenCodeBinary: () => binary, getResolvedOpenCodeBinarySource: () => 'installed',
45
+ readSettingsFromDiskMigrated: async () => ({}),
46
+ getOpenCodeResolutionSnapshot: async () => ({ source: 'installed', resolved: binary }),
47
+ buildOpenCodeUrl: (pathname) => `${registration.origin}${pathname}`,
48
+ getOpenCodeAuthHeaders: () => ({ Authorization: `Basic ${Buffer.from(`opencode:${registration.password}`).toString('base64')}` }),
49
+ forceResolvedOpenCodeBinary: (value) => { binary = value; },
50
+ restartOpenCode: async ({ binaryPath }) => {
51
+ await service.start({ binary: binaryPath, env, replace: true });
52
+ registration = await service.readRegistration();
53
+ },
54
+ openchamberDataDir: root,
55
+ });
56
+ const before = await request(app).get('/api/opencode/upgrade-status').expect(200);
57
+ expect(before.body).toMatchObject({ available: true, targetVersion: target });
58
+ const upgrade = await request(app).post('/api/opencode/upgrade').send({ target }).expect(200);
59
+ expect(upgrade.body).toMatchObject({ success: true, restarted: true, version: target });
60
+ const after = await request(app).get('/api/opencode/upgrade-status').expect(200);
61
+ expect(after.body).toMatchObject({ available: false, serveVersion: target });
62
+ } finally {
63
+ if (binary) 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
+ }, 180_000);
@@ -1,7 +1,6 @@
1
1
  import { createProjectIdFromPath } from '../projects/project-id.js';
2
2
  import { projectBootstrapSettingsResponse } from './settings-helpers.js';
3
3
  import {
4
- PINNED_OPENCODE2_VERSION,
5
4
  evaluateOpenCodeHealthBody,
6
5
  isOpenCode1xVersion,
7
6
  resolveOpenCode2UpgradeTarget,
@@ -11,7 +10,9 @@ import {
11
10
  readOpenCode2BinaryVersion,
12
11
  resolveOpenChamberDataDir,
13
12
  } from './ensure-cli.js';
13
+ import { registerUpgradeScreenRoutes } from './upgrade-screen.js';
14
14
  import { evaluateRuntimeContract } from './runtime-contract.js';
15
+ import { createOpenCodeUpdateDiscovery } from './update-discovery.js';
15
16
  import {
16
17
  buildUpgradeStatusSnapshot,
17
18
  classifyRuntimeOwnership,
@@ -53,6 +54,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
53
54
  getResolvedOpenCodeBinarySource = () => null,
54
55
  getActiveSessionCount = () => 0,
55
56
  openchamberDataDir = null,
57
+ discoverOpenCodeUpdate = createOpenCodeUpdateDiscovery(),
56
58
  } = dependencies;
57
59
 
58
60
  const upgradeOperation = createUpgradeOperationState();
@@ -155,12 +157,13 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
155
157
  const buildUpgradeStatus = async () => {
156
158
  const ownership = await resolveOwnershipContext();
157
159
  const contract = await buildLiveContract();
160
+ const targetVersion = await discoverOpenCodeUpdate();
158
161
  const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
159
162
  return buildUpgradeStatusSnapshot({
160
163
  ownership,
161
164
  serveVersion: contract.serveVersion,
162
165
  cliVersion: contract.cliVersion,
163
- targetVersion: PINNED_OPENCODE2_VERSION,
166
+ targetVersion,
164
167
  contract,
165
168
  operation: upgradeOperation.getState(),
166
169
  hasActiveTasks: activeCount > 0,
@@ -253,6 +256,35 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
253
256
  return fallback;
254
257
  };
255
258
 
259
+ registerUpgradeScreenRoutes(app, {
260
+ resolveOwnership: resolveOwnershipContext,
261
+ readServeVersion: async () => {
262
+ const probe = await readOpenCodeCurrentVersion().catch(() => ({ currentVersion: null }));
263
+ return probe.currentVersion || null;
264
+ },
265
+ readCliVersion: () => {
266
+ const fromRuntime = typeof getOpenCodeCliVersion === 'function' ? getOpenCodeCliVersion() : null;
267
+ if (fromRuntime) return fromRuntime;
268
+ const binaryPath = typeof getResolvedOpenCodeBinary === 'function' ? getResolvedOpenCodeBinary() : null;
269
+ return binaryPath ? readOpenCode2BinaryVersion(binaryPath) : null;
270
+ },
271
+ readConfiguredBinary: async () => {
272
+ const settings = await readSettingsFromDiskMigrated();
273
+ return typeof settings?.opencodeBinary === 'string' ? settings.opencodeBinary : null;
274
+ },
275
+ persistBinary: async (binaryPath) => {
276
+ await persistSettings({ opencodeBinary: binaryPath });
277
+ },
278
+ getResolvedBinary: getResolvedOpenCodeBinary,
279
+ getResolvedBinarySource: getResolvedOpenCodeBinarySource,
280
+ forceBinary: typeof forceResolvedOpenCodeBinary === 'function' ? forceResolvedOpenCodeBinary : null,
281
+ restart: typeof restartOpenCode === 'function' ? restartOpenCode : null,
282
+ waitReady: typeof waitForOpenCodeReady === 'function' ? waitForOpenCodeReady : null,
283
+ isSharedService: () => getIsSharedOpenCodeService() === true,
284
+ install: (options) => installPinnedOpenCode2Cli({ ...options, dataDir: resolveDataDir() }),
285
+ readBinaryVersion: readOpenCode2BinaryVersion,
286
+ });
287
+
256
288
  app.get('/api/config/settings', async (req, res) => {
257
289
  try {
258
290
  const settings = await readSettingsFromDiskMigrated();
@@ -312,7 +344,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
312
344
  errorCode: 'OPENCODE_UPGRADE_1X_REFUSED',
313
345
  });
314
346
  }
315
- const target = resolveOpenCode2UpgradeTarget(rawTarget);
347
+ const target = resolveOpenCode2UpgradeTarget(rawTarget ?? (await buildUpgradeStatus()).targetVersion);
316
348
  const confirmActive = req.body?.confirmActiveTasks === true || req.body?.force === true;
317
349
  const activeCount = typeof getActiveSessionCount === 'function' ? Number(getActiveSessionCount()) || 0 : 0;
318
350
  if (activeCount > 0 && !confirmActive) {
@@ -677,7 +709,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
677
709
 
678
710
  const sources = getProviderSources(providerId, directory);
679
711
  const { getProviderAuth } = await getAuthLibrary();
680
- const auth = getProviderAuth(providerId);
712
+ const auth = await getProviderAuth(providerId);
681
713
  sources.sources.auth.exists = Boolean(auth);
682
714
 
683
715
  return res.json({
@@ -721,12 +753,12 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
721
753
  let removed = false;
722
754
  if (scope === 'auth') {
723
755
  const { removeProviderAuth } = await getAuthLibrary();
724
- removed = removeProviderAuth(providerId);
756
+ removed = await removeProviderAuth(providerId);
725
757
  } else if (scope === 'user' || scope === 'project' || scope === 'custom') {
726
758
  removed = removeProviderConfig(providerId, directory, scope);
727
759
  } else if (scope === 'all') {
728
760
  const { removeProviderAuth } = await getAuthLibrary();
729
- const authRemoved = removeProviderAuth(providerId);
761
+ const authRemoved = await removeProviderAuth(providerId);
730
762
  const userRemoved = removeProviderConfig(providerId, directory, 'user');
731
763
  const projectRemoved = directory ? removeProviderConfig(providerId, directory, 'project') : false;
732
764
  const customRemoved = removeProviderConfig(providerId, directory, 'custom');
@@ -735,14 +767,14 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
735
767
  return res.status(400).json({ error: 'Invalid scope' });
736
768
  }
737
769
 
738
- if (removed) {
770
+ if (removed && scope !== 'auth') {
739
771
  await refreshOpenCodeAfterConfigChange(`provider ${providerId} disconnected (${scope})`);
740
772
  }
741
773
 
742
774
  return res.json({
743
775
  success: true,
744
776
  removed,
745
- requiresReload: removed,
777
+ requiresReload: removed && scope !== 'auth',
746
778
  message: removed ? 'Provider disconnected successfully' : 'Provider was not connected',
747
779
  reloadDelayMs: removed ? clientReloadDelayMs : undefined,
748
780
  });