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

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 (296) 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-CzTtVMnI.js} +1 -1
  7. package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
  8. package/dist/assets/{DiagramView-xuSJTnv7.js → DiagramView-hOi72Wdx.js} +1 -1
  9. package/dist/assets/{FadeInOnReveal-Ov0D96d0.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
  10. package/dist/assets/Icon-B8fSTLdP.js +1 -0
  11. package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
  12. package/dist/assets/{MarkdownRendererImpl-SKi4oG0c.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
  13. package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
  14. package/dist/assets/{MobileDetailNavigation-Bnx59U_J.js → MobileDetailNavigation-iX_hb_Xt.js} +1 -1
  15. package/dist/assets/{MobileShareBridge-BQlP4J-F.js → MobileShareBridge-CAMUupwR.js} +2 -2
  16. package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
  17. package/dist/assets/{MultiRunWindow-ChZGob0v.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
  18. package/dist/assets/{OnboardingScreen-DtQJxZG-.js → OnboardingScreen-O4cGdz5p.js} +1 -1
  19. package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
  20. package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
  21. package/dist/assets/{SettingsWindow-DfnsFa8y.js → SettingsWindow-B8d8qIQ7.js} +1 -1
  22. package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
  23. package/dist/assets/{ToolOutputDialog-BejmjrRk.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
  24. package/dist/assets/appThemeRegistry-BGGHio8T.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-DIIrsMjW.js} +1 -1
  27. package/dist/assets/{collapsible-BXRd5_Hr.js → collapsible-CnvUeYst.js} +1 -1
  28. package/dist/assets/{context-menu-jGtH6fR9.js → context-menu-BGBUpZEQ.js} +1 -1
  29. package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
  30. package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
  31. package/dist/assets/{diagram-DQf7Y_uR.js → diagram-rvn7_qBM.js} +1 -1
  32. package/dist/assets/{dialog-bfbvR4VH.js → dialog-CQ63sc9E.js} +1 -1
  33. package/dist/assets/es-Dsmc0A_3.js +15 -0
  34. package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
  35. package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
  36. package/dist/assets/fr-C_ZlZDI2.js +13 -0
  37. package/dist/assets/gitApi-BF8vuoZc.js +10 -0
  38. package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
  39. package/dist/assets/{input-8r6F_Fza.js → input-BVu-iykl.js} +1 -1
  40. package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
  41. package/dist/assets/ja-BqqTaWTi.js +15 -0
  42. package/dist/assets/ko-DP2XPM37.js +15 -0
  43. package/dist/assets/{main-CCE_4y5F.js → main-B7t9lavz.js} +2 -2
  44. package/dist/assets/main-D2BzpnUt.css +1 -0
  45. package/dist/assets/main-DWP-prNB.js +169 -0
  46. package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
  47. package/dist/assets/{markdownHeightCache-1Qo6y6IJ.js → markdownHeightCache-D9-B1wdY.js} +1 -1
  48. package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
  49. package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
  50. package/dist/assets/miniChat-DgOrEQbN.js +2 -0
  51. package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
  52. package/dist/assets/multirun-DelyUO6k.js +4 -0
  53. package/dist/assets/persistence-D0y7226-.js +3 -0
  54. package/dist/assets/pl-BCf5uLlK.js +15 -0
  55. package/dist/assets/platform-CurqO28D.js +6 -0
  56. package/dist/assets/projectMeta-CPElzJ0n.js +3 -0
  57. package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
  58. package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
  59. package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
  60. package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
  61. package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
  62. package/dist/assets/{runtimeConfig-BMvqa5we.js → runtimeConfig-DSS0KxG3.js} +1 -1
  63. package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
  64. package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
  65. package/dist/assets/{sessionLookup-gh-hVRIc.js → sessionLookup-Bss8M20i.js} +1 -1
  66. package/dist/assets/{sortable-tabs-strip-CudoSgV4.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
  67. package/dist/assets/src-lS1keDVV.css +2 -0
  68. package/dist/assets/store-jaP79mqI.js +15 -0
  69. package/dist/assets/{terminalApi-DiF6JmBP.js → terminalApi-mDR2jXTo.js} +1 -1
  70. package/dist/assets/textarea-XD2oX_Ee.js +1 -0
  71. package/dist/assets/{tooltip-CQvpK-UV.js → tooltip-DYeQ_Hvy.js} +1 -1
  72. package/dist/assets/uk-BgFzz8m2.js +15 -0
  73. package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
  74. package/dist/assets/{useAppFontEffects-Cpmpo7Wb.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
  75. package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
  76. package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
  77. package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
  78. package/dist/assets/{useFontPreferences-DcbXQOkc.js → useFontPreferences-Bjx8zkGM.js} +1 -1
  79. package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
  80. package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
  81. package/dist/assets/useUIStore-syVmbuDA.js +1 -0
  82. package/dist/assets/{useWorkerHighlightedLines-Bqn4AiBL.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
  83. package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
  84. package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
  85. package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
  86. package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
  87. package/dist/index.html +12 -12
  88. package/dist/mini-chat.html +12 -12
  89. package/dist/mobile.html +12 -12
  90. package/package.json +3 -2
  91. package/server/index.js +359 -63
  92. package/server/lib/assistants/DOCUMENTATION.md +14 -9
  93. package/server/lib/assistants/contracts.js +11 -1
  94. package/server/lib/assistants/projection-history.test.js +265 -18
  95. package/server/lib/assistants/routes.js +6 -0
  96. package/server/lib/assistants/service.js +417 -231
  97. package/server/lib/assistants/service.test.js +392 -148
  98. package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
  99. package/server/lib/browser-provider/contract.js +312 -0
  100. package/server/lib/browser-provider/host.js +421 -0
  101. package/server/lib/browser-provider/host.test.js +304 -0
  102. package/server/lib/browser-provider/routes.js +122 -0
  103. package/server/lib/browser-provider/routes.test.js +120 -0
  104. package/server/lib/browser-provider/selection-store.js +43 -0
  105. package/server/lib/browser-provider/selection-store.test.js +35 -0
  106. package/server/lib/conversations/DOCUMENTATION.md +1 -1
  107. package/server/lib/conversations/service.js +1 -0
  108. package/server/lib/conversations/service.test.js +16 -0
  109. package/server/lib/conversations/validation.js +11 -1
  110. package/server/lib/event-stream/DOCUMENTATION.md +3 -1
  111. package/server/lib/event-stream/directory-ws-bridge.js +13 -2
  112. package/server/lib/event-stream/global-hub.js +49 -4
  113. package/server/lib/event-stream/reasoning-projection.js +16 -1
  114. package/server/lib/event-stream/reasoning-projection.test.js +15 -0
  115. package/server/lib/event-stream/runtime.js +57 -20
  116. package/server/lib/event-stream/runtime.test.js +112 -0
  117. package/server/lib/llm/DOCUMENTATION.md +33 -9
  118. package/server/lib/llm/catalog.js +1 -1
  119. package/server/lib/llm/completions.js +5 -1
  120. package/server/lib/llm/completions.test.js +3 -3
  121. package/server/lib/llm/generate.js +237 -145
  122. package/server/lib/llm/generate.test.js +201 -46
  123. package/server/lib/llm/routes.js +2 -0
  124. package/server/lib/message-queue/runtime.js +15 -2
  125. package/server/lib/notifications/DOCUMENTATION.md +3 -1
  126. package/server/lib/notifications/runtime.js +133 -31
  127. package/server/lib/notifications/runtime.test.js +103 -0
  128. package/server/lib/notifications/template-runtime.js +130 -38
  129. package/server/lib/notifications/template-runtime.test.js +96 -0
  130. package/server/lib/opencode/DOCUMENTATION.md +89 -17
  131. package/server/lib/opencode/agent-document.d.ts +28 -0
  132. package/server/lib/opencode/agent-document.js +280 -0
  133. package/server/lib/opencode/agent-document.test.js +137 -0
  134. package/server/lib/opencode/agents.js +96 -0
  135. package/server/lib/opencode/auth-state-runtime.js +5 -0
  136. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  137. package/server/lib/opencode/config-entity-routes.js +42 -99
  138. package/server/lib/opencode/config-entity-routes.test.js +107 -0
  139. package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
  140. package/server/lib/opencode/ensure-cli.d.ts +8 -0
  141. package/server/lib/opencode/ensure-cli.js +1 -2
  142. package/server/lib/opencode/env-runtime.js +40 -26
  143. package/server/lib/opencode/env-runtime.test.js +10 -17
  144. package/server/lib/opencode/feature-routes-runtime.js +53 -4
  145. package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
  146. package/server/lib/opencode/http-server-connections.js +98 -0
  147. package/server/lib/opencode/http-server-connections.test.js +321 -0
  148. package/server/lib/opencode/lifecycle.js +349 -39
  149. package/server/lib/opencode/lifecycle.test.js +322 -0
  150. package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
  151. package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
  152. package/server/lib/opencode/openchamber-routes.js +7 -1
  153. package/server/lib/opencode/opencode2-pin.js +4 -4
  154. package/server/lib/opencode/opencode2-pin.test.js +1 -0
  155. package/server/lib/opencode/opencode2-sidecar.e2e.test.js +127 -4
  156. package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
  157. package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
  158. package/server/lib/opencode/plugin-routes.js +3 -18
  159. package/server/lib/opencode/plugin-routes.test.js +9 -9
  160. package/server/lib/opencode/provider-catalog.js +58 -12
  161. package/server/lib/opencode/provider-catalog.test.js +48 -0
  162. package/server/lib/opencode/proxy.js +254 -46
  163. package/server/lib/opencode/proxy.test.js +238 -5
  164. package/server/lib/opencode/routes.js +326 -62
  165. package/server/lib/opencode/routes.test.js +278 -12
  166. package/server/lib/opencode/runtime-contract.js +401 -0
  167. package/server/lib/opencode/runtime-contract.test.js +214 -0
  168. package/server/lib/opencode/server-opencode-fetch.js +242 -0
  169. package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
  170. package/server/lib/opencode/server-utils-runtime.js +33 -0
  171. package/server/lib/opencode/shared-service.integration.test.js +66 -0
  172. package/server/lib/opencode/shared-service.js +147 -0
  173. package/server/lib/opencode/shared-service.test.js +93 -0
  174. package/server/lib/opencode/shared.js +3 -3
  175. package/server/lib/opencode/shutdown-runtime.js +15 -6
  176. package/server/lib/opencode/shutdown-runtime.test.js +49 -1
  177. package/server/lib/opencode/skill-routes.js +20 -33
  178. package/server/lib/opencode/skill-routes.test.js +35 -2
  179. package/server/lib/opencode/skills.test.js +15 -0
  180. package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
  181. package/server/lib/opencode/static-routes-runtime.js +3 -1
  182. package/server/lib/opencode/static-routes-runtime.test.js +33 -0
  183. package/server/lib/opencode/upgrade-screen.d.ts +30 -0
  184. package/server/lib/opencode/upgrade-screen.js +299 -0
  185. package/server/lib/opencode/upgrade-screen.test.js +196 -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 +73 -18
  211. package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
  212. package/server/lib/session-metadata/opencode-session-record.js +316 -0
  213. package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
  214. package/server/lib/session-metadata/routes.js +112 -9
  215. package/server/lib/session-metadata/routes.test.js +47 -2
  216. package/server/lib/session-metadata/session-archive.d.ts +73 -0
  217. package/server/lib/session-metadata/session-archive.js +654 -0
  218. package/server/lib/session-metadata/session-archive.test.js +359 -0
  219. package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
  220. package/server/lib/session-metadata/session-metadata-store.js +270 -83
  221. package/server/lib/session-metadata/session-metadata-store.test.js +205 -10
  222. package/server/lib/session-metadata/session-projection.d.ts +41 -0
  223. package/server/lib/session-metadata/session-projection.js +279 -0
  224. package/server/lib/session-metadata/session-projection.test.js +64 -0
  225. package/server/lib/session-metadata/system-session.js +55 -0
  226. package/server/lib/session-metadata/system-session.test.js +55 -0
  227. package/server/lib/session-title/DOCUMENTATION.md +40 -7
  228. package/server/lib/session-title/native-runtime.test.js +167 -0
  229. package/server/lib/session-title/runtime.js +69 -44
  230. package/server/lib/session-title/session-access.js +46 -0
  231. package/server/lib/session-title/session-access.test.js +91 -0
  232. package/server/lib/session-turn-pages/DOCUMENTATION.md +9 -8
  233. package/server/lib/session-turn-pages/routes.js +17 -102
  234. package/server/lib/session-turn-pages/routes.test.js +281 -0
  235. package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
  236. package/server/lib/session-turn-pages/session-message-projection.js +281 -0
  237. package/server/lib/small-model/DOCUMENTATION.md +101 -147
  238. package/server/lib/small-model/catalog.js +31 -51
  239. package/server/lib/small-model/catalog.test.js +65 -10
  240. package/server/lib/small-model/custom-api.js +68 -0
  241. package/server/lib/small-model/custom-api.test.js +64 -0
  242. package/server/lib/small-model/index.js +124 -240
  243. package/server/lib/small-model/resolve.js +35 -120
  244. package/server/lib/small-model/resolve.test.js +20 -110
  245. package/server/lib/small-model/routes.js +1 -3
  246. package/server/lib/small-model/summary-settings.test.js +157 -253
  247. package/server/opencode-proxy.test.js +2 -0
  248. package/dist/assets/AssistantView-BTCkm8yk.js +0 -3
  249. package/dist/assets/Icon-DD-BuMfZ.js +0 -1
  250. package/dist/assets/MarkdownRenderer-bc_p1H7l.js +0 -2
  251. package/dist/assets/MarkstreamRendererImpl-CnkodGJW.js +0 -3
  252. package/dist/assets/MobileSurface-BXLeiZ4d.js +0 -2
  253. package/dist/assets/OverlayScrollbar-pMwxaCca.js +0 -2
  254. package/dist/assets/SettingsView-B5I0KDTZ.js +0 -25
  255. package/dist/assets/TerminalView-Dh9MWCpS.js +0 -8
  256. package/dist/assets/appThemeRegistry-COGfxTFV.js +0 -1
  257. package/dist/assets/es-c52LTHHe.js +0 -15
  258. package/dist/assets/fileMentionTouchSelection-BvrNCroG.js +0 -4
  259. package/dist/assets/fr-PisFbUE8.js +0 -13
  260. package/dist/assets/gitApi-ButwzQ0N.js +0 -9
  261. package/dist/assets/insertionBoundaries-DGs7S5zv.js +0 -1
  262. package/dist/assets/ja-C8Ikun3Z.js +0 -15
  263. package/dist/assets/ko-ClFys5o2.js +0 -15
  264. package/dist/assets/main-Dn9rpsgl.js +0 -169
  265. package/dist/assets/main-qSeeJRW1.css +0 -1
  266. package/dist/assets/markdown-worker-CptoICmq.js +0 -2
  267. package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
  268. package/dist/assets/miniChat-BpjxVqgg.js +0 -2
  269. package/dist/assets/mobile-nMJCaNq1.js +0 -2
  270. package/dist/assets/multirun-BvSG94mW.js +0 -4
  271. package/dist/assets/persistence-Ce3AnpEr.js +0 -3
  272. package/dist/assets/pl-CnaKWN5F.js +0 -15
  273. package/dist/assets/platform-CWTIp02q.js +0 -4
  274. package/dist/assets/projectMeta-DOe49dM_.js +0 -3
  275. package/dist/assets/pt-BR-s8OpfRI6.js +0 -15
  276. package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +0 -2
  277. package/dist/assets/renderMobileApp-y-S8y8v_.js +0 -28
  278. package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
  279. package/dist/assets/runtimeEndpointReset-B6BxhvR0.js +0 -147
  280. package/dist/assets/sessionChangeSummary-VVBlEXuh.js +0 -2
  281. package/dist/assets/src-BzX5UI-c.css +0 -2
  282. package/dist/assets/store-DJczmczv.js +0 -15
  283. package/dist/assets/textarea-3XR8sUfy.js +0 -1
  284. package/dist/assets/uk-CaX0iqkB.js +0 -15
  285. package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +0 -1
  286. package/dist/assets/useEffectiveDirectory-CN6JoCFw.js +0 -1
  287. package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +0 -414
  288. package/dist/assets/useSessionAutoCleanup-BLjymaFt.js +0 -2
  289. package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
  290. package/dist/assets/vendor-opencode-client-BOe2qpQb.js +0 -12
  291. package/dist/assets/zh-CN-20MYEjha.js +0 -15
  292. package/dist/assets/zh-TW-DiN04qTw.js +0 -15
  293. package/server/lib/small-model/call.js +0 -427
  294. package/server/lib/small-model/call.test.js +0 -382
  295. package/server/lib/small-model/opencode-session.js +0 -183
  296. package/server/lib/small-model/opencode-session.test.js +0 -147
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Startup upgrade screen for OpenCode below 2.0.15.
3
+ *
4
+ * This module does not own process lifecycle. It classifies the version the
5
+ * existing startup path already detected, and installs through the owned-cache
6
+ * installer (`installPinnedOpenCode2Cli`) plus the caller's restart hook.
7
+ * Success is a verified running version, never a download that has not started.
8
+ */
9
+
10
+ import {
11
+ PINNED_OPENCODE2_VERSION,
12
+ isAcceptableOpenCode2HealthVersion,
13
+ isOpenCode1xVersion,
14
+ isOpenCode2VersionAtLeast,
15
+ npmPackageForOpenCode2,
16
+ } from './opencode2-pin.js';
17
+
18
+ /** Oldest OpenCode this OpenChamber build will enter a session against. */
19
+ export const REQUIRED_OPENCODE_VERSION = '2.0.15';
20
+
21
+ const normalizeVersion = (value) => {
22
+ if (typeof value !== 'string') return null;
23
+ const trimmed = value.trim().replace(/^v/i, '');
24
+ return trimmed || null;
25
+ };
26
+
27
+ /**
28
+ * Install target is the pin when the pin already satisfies the screen, otherwise
29
+ * the screen minimum. A pin still at 2.0.12 must not be what this screen installs.
30
+ * @param {string} [pin]
31
+ */
32
+ export function resolveUpgradeScreenTarget(pin = PINNED_OPENCODE2_VERSION) {
33
+ if (isOpenCode2VersionAtLeast(pin, REQUIRED_OPENCODE_VERSION)) return pin;
34
+ return REQUIRED_OPENCODE_VERSION;
35
+ }
36
+
37
+ /** 2.x at or above the screen minimum. A future major is not this contract. */
38
+ function isUpgradeScreenSatisfied(version) {
39
+ const normalized = normalizeVersion(version);
40
+ if (!normalized || !isAcceptableOpenCode2HealthVersion(normalized)) return false;
41
+ return isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
42
+ }
43
+
44
+ function isOlderThanUpgradeScreenMinimum(version) {
45
+ const normalized = normalizeVersion(version);
46
+ if (!normalized) return false;
47
+ if (isOpenCode1xVersion(normalized)) return true;
48
+ if (!isAcceptableOpenCode2HealthVersion(normalized)) return false;
49
+ return !isOpenCode2VersionAtLeast(normalized, REQUIRED_OPENCODE_VERSION);
50
+ }
51
+
52
+ /**
53
+ * @param {string | null | undefined} ownership
54
+ * @returns {'managed' | 'external' | 'bundled'}
55
+ */
56
+ function classifyUpgradeScreenInstallation(ownership) {
57
+ if (ownership === 'external-serve') return 'external';
58
+ if (ownership === 'bundled') return 'bundled';
59
+ return 'managed';
60
+ }
61
+
62
+ export function platformSupportsUpgradeScreenInstall(platform = process.platform, arch = process.arch) {
63
+ try {
64
+ npmPackageForOpenCode2(platform, arch);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ const pickDetectedVersion = (serveVersion, cliVersion) => {
72
+ const serve = normalizeVersion(serveVersion);
73
+ const cli = normalizeVersion(cliVersion);
74
+ if (serve && !isUpgradeScreenSatisfied(serve)) return serve;
75
+ if (serve) return serve;
76
+ return cli;
77
+ };
78
+
79
+ /**
80
+ * @param {{
81
+ * version?: string | null,
82
+ * installation?: 'managed' | 'external' | 'bundled',
83
+ * platformCanInstall?: boolean,
84
+ * guidance?: string | null,
85
+ * pin?: string,
86
+ * }} input
87
+ */
88
+ export function describeUpgradeScreen(input = {}) {
89
+ const version = normalizeVersion(input.version);
90
+ const installation = input.installation === 'external' || input.installation === 'bundled'
91
+ ? input.installation
92
+ : 'managed';
93
+ const state = version == null
94
+ ? 'unavailable'
95
+ : isUpgradeScreenSatisfied(version)
96
+ ? 'compatible'
97
+ : 'incompatible';
98
+ const older = version != null && isOlderThanUpgradeScreenMinimum(version);
99
+ // External processes are not replaced. Bundled and managed locals can receive
100
+ // an owned-cache install; that does not overwrite the user's other binaries.
101
+ const canInstall = state === 'incompatible'
102
+ && older
103
+ && installation !== 'external'
104
+ && input.platformCanInstall === true;
105
+ const guidance = canInstall
106
+ ? null
107
+ : (typeof input.guidance === 'string' && input.guidance.trim() ? input.guidance.trim() : null);
108
+ return {
109
+ state,
110
+ version,
111
+ installation,
112
+ minimumVersion: REQUIRED_OPENCODE_VERSION,
113
+ targetVersion: resolveUpgradeScreenTarget(input.pin),
114
+ canInstall,
115
+ guidance,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * A running serve below the minimum, or a missing version, is not an upgrade.
121
+ * @param {{ targetVersion?: string | null, serveVersion?: string | null }} input
122
+ */
123
+ export function evaluateUpgradeScreenInstallResult(input = {}) {
124
+ const targetVersion = resolveUpgradeScreenTarget(input.targetVersion || REQUIRED_OPENCODE_VERSION);
125
+ const version = normalizeVersion(input.serveVersion);
126
+ if (!version || !isUpgradeScreenSatisfied(version) || !isOpenCode2VersionAtLeast(version, targetVersion)) {
127
+ const found = version || 'unknown';
128
+ return {
129
+ ok: false,
130
+ upgraded: false,
131
+ version,
132
+ targetVersion,
133
+ errorCode: 'UPGRADE_SCREEN_NOT_VERIFIED',
134
+ error: `OpenCode is still ${found}. OpenChamber requires ${REQUIRED_OPENCODE_VERSION} or newer.`,
135
+ };
136
+ }
137
+ return {
138
+ ok: true,
139
+ upgraded: true,
140
+ version,
141
+ targetVersion,
142
+ error: null,
143
+ errorCode: null,
144
+ };
145
+ }
146
+
147
+ async function readUpgradeScreenStatus(deps) {
148
+ const ownership = typeof deps.resolveOwnership === 'function'
149
+ ? await deps.resolveOwnership()
150
+ : { ownership: 'unknown', guidance: null };
151
+ const installation = classifyUpgradeScreenInstallation(ownership?.ownership);
152
+ const serveVersion = typeof deps.readServeVersion === 'function'
153
+ ? await Promise.resolve(deps.readServeVersion()).catch(() => null)
154
+ : null;
155
+ const cliVersion = typeof deps.readCliVersion === 'function'
156
+ ? await Promise.resolve(deps.readCliVersion()).catch(() => null)
157
+ : null;
158
+ return describeUpgradeScreen({
159
+ version: pickDetectedVersion(serveVersion, cliVersion),
160
+ installation,
161
+ platformCanInstall: typeof deps.platformCanInstall === 'boolean'
162
+ ? deps.platformCanInstall
163
+ : platformSupportsUpgradeScreenInstall(deps.platform, deps.arch),
164
+ guidance: ownership?.guidance ?? null,
165
+ pin: deps.pin,
166
+ });
167
+ }
168
+
169
+ const failure = (message, code, status = 500) => {
170
+ const error = new Error(message);
171
+ error.code = code;
172
+ error.status = status;
173
+ return error;
174
+ };
175
+
176
+ /**
177
+ * Install the screen target into the owned cache and restart through the
178
+ * caller's existing lifecycle hooks. Restores the previous binary selection
179
+ * when verification fails. Never returns upgraded:true for a failed install.
180
+ * @param {object} deps
181
+ */
182
+ export async function installRequiredOpenCode(deps) {
183
+ const status = await readUpgradeScreenStatus(deps);
184
+ if (status.state !== 'incompatible' || status.canInstall !== true) {
185
+ throw failure(
186
+ status.guidance || 'Automatic OpenCode installation is unavailable for this runtime.',
187
+ 'UPGRADE_SCREEN_UNAVAILABLE',
188
+ 409,
189
+ );
190
+ }
191
+ const target = status.targetVersion;
192
+ const previousBinary = typeof deps.getResolvedBinary === 'function' ? deps.getResolvedBinary() : null;
193
+ const previousSource = typeof deps.getResolvedBinarySource === 'function' ? deps.getResolvedBinarySource() : null;
194
+ const previousSetting = typeof deps.readConfiguredBinary === 'function'
195
+ ? await deps.readConfiguredBinary()
196
+ : undefined;
197
+ let persisted = false;
198
+ try {
199
+ if (typeof deps.install !== 'function') {
200
+ throw failure('OpenCode install is not configured.', 'UPGRADE_SCREEN_INSTALL_UNAVAILABLE', 500);
201
+ }
202
+ const installedPath = await deps.install({ version: target });
203
+ const diskVersion = typeof deps.readBinaryVersion === 'function'
204
+ ? await Promise.resolve(deps.readBinaryVersion(installedPath))
205
+ : null;
206
+ if (diskVersion !== target) {
207
+ throw failure(
208
+ `Installed OpenCode version mismatch: expected ${target}, got ${diskVersion || 'unknown'}`,
209
+ 'UPGRADE_SCREEN_BINARY_MISMATCH',
210
+ 500,
211
+ );
212
+ }
213
+ if (typeof deps.persistBinary === 'function') {
214
+ await deps.persistBinary(installedPath);
215
+ persisted = true;
216
+ }
217
+ if (typeof deps.forceBinary === 'function') {
218
+ deps.forceBinary(installedPath, 'installed');
219
+ }
220
+ if (typeof deps.restart === 'function') {
221
+ const shared = typeof deps.isSharedService === 'function' && deps.isSharedService() === true;
222
+ await deps.restart(shared ? { binaryPath: installedPath } : undefined);
223
+ }
224
+ if (typeof deps.waitReady === 'function') {
225
+ await deps.waitReady();
226
+ }
227
+ const serveVersion = typeof deps.readServeVersion === 'function'
228
+ ? await deps.readServeVersion()
229
+ : null;
230
+ const verification = evaluateUpgradeScreenInstallResult({ targetVersion: target, serveVersion });
231
+ if (!verification.ok) {
232
+ throw failure(verification.error, verification.errorCode, 500);
233
+ }
234
+ return verification;
235
+ } catch (error) {
236
+ if (persisted && typeof deps.persistBinary === 'function') {
237
+ try {
238
+ await deps.persistBinary(typeof previousSetting === 'string' ? previousSetting : '');
239
+ } catch {
240
+ // Keep the install error. A failed restore must not become success.
241
+ }
242
+ }
243
+ if (previousBinary && typeof deps.forceBinary === 'function') {
244
+ try {
245
+ deps.forceBinary(previousBinary, previousSource || 'discovered');
246
+ } catch {
247
+ // Same: report the install failure, not a restored-success.
248
+ }
249
+ }
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ export function registerUpgradeScreenRoutes(app, deps) {
255
+ app.get('/api/opencode/compatibility', async (_req, res) => {
256
+ try {
257
+ res.json(await readUpgradeScreenStatus(deps));
258
+ } catch (error) {
259
+ res.status(503).json({
260
+ error: error instanceof Error ? error.message : 'Could not check OpenCode compatibility',
261
+ });
262
+ }
263
+ });
264
+
265
+ let inFlight = null;
266
+ app.post('/api/opencode/install-required', async (_req, res) => {
267
+ try {
268
+ if (!inFlight) {
269
+ inFlight = installRequiredOpenCode(deps).finally(() => {
270
+ inFlight = null;
271
+ });
272
+ }
273
+ const result = await inFlight;
274
+ if (!result || result.ok !== true || result.upgraded !== true || !result.version) {
275
+ return res.status(500).json({
276
+ success: false,
277
+ upgraded: false,
278
+ error: result?.error || 'OpenCode installation did not verify a new version',
279
+ errorCode: result?.errorCode || 'UPGRADE_SCREEN_NOT_VERIFIED',
280
+ version: result?.version ?? null,
281
+ });
282
+ }
283
+ return res.json({
284
+ success: true,
285
+ upgraded: true,
286
+ version: result.version,
287
+ targetVersion: result.targetVersion,
288
+ });
289
+ } catch (error) {
290
+ const status = Number.isInteger(error?.status) ? error.status : 500;
291
+ return res.status(status).json({
292
+ success: false,
293
+ upgraded: false,
294
+ error: error instanceof Error ? error.message : 'OpenCode installation failed',
295
+ errorCode: error?.code || 'UPGRADE_SCREEN_FAILED',
296
+ });
297
+ }
298
+ });
299
+ }
@@ -0,0 +1,196 @@
1
+ import express from 'express';
2
+ import request from 'supertest';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import {
5
+ REQUIRED_OPENCODE_VERSION,
6
+ describeUpgradeScreen,
7
+ evaluateUpgradeScreenInstallResult,
8
+ installRequiredOpenCode,
9
+ registerUpgradeScreenRoutes,
10
+ resolveUpgradeScreenTarget,
11
+ } from './upgrade-screen.js';
12
+
13
+ describe('upgrade screen version gate', () => {
14
+ it('uses 2.0.15 even when the runtime pin is older', () => {
15
+ expect(REQUIRED_OPENCODE_VERSION).toBe('2.0.15');
16
+ expect(resolveUpgradeScreenTarget('2.0.12')).toBe('2.0.15');
17
+ expect(resolveUpgradeScreenTarget('2.0.16')).toBe('2.0.16');
18
+ });
19
+
20
+ it('does not open the gate at or above 2.0.15', () => {
21
+ expect(describeUpgradeScreen({
22
+ version: '2.0.15',
23
+ installation: 'managed',
24
+ platformCanInstall: true,
25
+ })).toMatchObject({ state: 'compatible', canInstall: false, minimumVersion: '2.0.15' });
26
+ expect(describeUpgradeScreen({
27
+ version: 'v2.0.16',
28
+ installation: 'managed',
29
+ platformCanInstall: true,
30
+ }).state).toBe('compatible');
31
+ });
32
+
33
+ it('opens the gate below 2.0.15 and offers install only when this runtime can switch', () => {
34
+ expect(describeUpgradeScreen({
35
+ version: '2.0.14',
36
+ installation: 'managed',
37
+ platformCanInstall: true,
38
+ })).toMatchObject({ state: 'incompatible', canInstall: true, version: '2.0.14' });
39
+ expect(describeUpgradeScreen({
40
+ version: '1.18.30',
41
+ installation: 'managed',
42
+ platformCanInstall: true,
43
+ })).toMatchObject({ state: 'incompatible', canInstall: true });
44
+ expect(describeUpgradeScreen({
45
+ version: '2.0.14',
46
+ installation: 'external',
47
+ platformCanInstall: true,
48
+ }).canInstall).toBe(false);
49
+ expect(describeUpgradeScreen({
50
+ version: '2.0.14',
51
+ installation: 'bundled',
52
+ platformCanInstall: true,
53
+ })).toMatchObject({ state: 'incompatible', canInstall: true, installation: 'bundled' });
54
+ expect(describeUpgradeScreen({
55
+ version: '2.0.14',
56
+ installation: 'managed',
57
+ platformCanInstall: false,
58
+ }).canInstall).toBe(false);
59
+ });
60
+
61
+ it('does not treat a missing version as an old install', () => {
62
+ expect(describeUpgradeScreen({
63
+ version: null,
64
+ installation: 'managed',
65
+ platformCanInstall: true,
66
+ }).state).toBe('unavailable');
67
+ });
68
+
69
+ it('does not report an unverified install as upgraded', () => {
70
+ expect(evaluateUpgradeScreenInstallResult({
71
+ targetVersion: '2.0.15',
72
+ serveVersion: '2.0.14',
73
+ })).toMatchObject({ ok: false, upgraded: false, version: '2.0.14' });
74
+ expect(evaluateUpgradeScreenInstallResult({
75
+ targetVersion: '2.0.15',
76
+ serveVersion: null,
77
+ }).upgraded).toBe(false);
78
+ expect(evaluateUpgradeScreenInstallResult({
79
+ targetVersion: '2.0.15',
80
+ serveVersion: '2.0.15',
81
+ })).toMatchObject({ ok: true, upgraded: true, version: '2.0.15' });
82
+ });
83
+ });
84
+
85
+ describe('installRequiredOpenCode', () => {
86
+ const createDeps = (overrides = {}) => {
87
+ const calls = [];
88
+ const deps = {
89
+ resolveOwnership: async () => ({ ownership: 'global-cli', guidance: null }),
90
+ readServeVersion: async () => '2.0.14',
91
+ readCliVersion: () => '2.0.14',
92
+ platformCanInstall: true,
93
+ install: vi.fn(async () => {
94
+ calls.push('install');
95
+ return '/cache/opencode';
96
+ }),
97
+ readBinaryVersion: vi.fn(() => '2.0.15'),
98
+ persistBinary: vi.fn(async () => {
99
+ calls.push('persist');
100
+ }),
101
+ forceBinary: vi.fn(() => {
102
+ calls.push('force');
103
+ }),
104
+ restart: vi.fn(async () => {
105
+ calls.push('restart');
106
+ }),
107
+ waitReady: vi.fn(async () => {
108
+ calls.push('ready');
109
+ }),
110
+ readConfiguredBinary: async () => null,
111
+ isSharedService: () => false,
112
+ ...overrides,
113
+ };
114
+ return { deps, calls };
115
+ };
116
+
117
+ it('installs 2.0.15 and only succeeds after the running serve is verified', async () => {
118
+ const { deps, calls } = createDeps({
119
+ readServeVersion: vi.fn()
120
+ .mockResolvedValueOnce('2.0.14')
121
+ .mockResolvedValueOnce('2.0.15'),
122
+ });
123
+
124
+ await expect(installRequiredOpenCode(deps)).resolves.toMatchObject({
125
+ ok: true,
126
+ upgraded: true,
127
+ version: '2.0.15',
128
+ targetVersion: '2.0.15',
129
+ });
130
+ expect(deps.install).toHaveBeenCalledWith({ version: '2.0.15' });
131
+ expect(calls).toEqual(['install', 'persist', 'force', 'restart', 'ready']);
132
+ });
133
+
134
+ it('returns the failure reason and does not claim an upgrade when verification fails', async () => {
135
+ const { deps } = createDeps({
136
+ readServeVersion: vi.fn()
137
+ .mockResolvedValueOnce('1.18.30')
138
+ .mockResolvedValueOnce('1.18.30'),
139
+ readCliVersion: () => '1.18.30',
140
+ readBinaryVersion: () => '2.0.15',
141
+ });
142
+
143
+ await expect(installRequiredOpenCode(deps)).rejects.toThrow(/still 1\.18\.30/);
144
+ expect(deps.persistBinary).toHaveBeenCalledWith('');
145
+ });
146
+
147
+ it('does not install when the runtime cannot switch', async () => {
148
+ const { deps } = createDeps({
149
+ resolveOwnership: async () => ({
150
+ ownership: 'external-serve',
151
+ guidance: 'Restart that server yourself.',
152
+ }),
153
+ });
154
+
155
+ await expect(installRequiredOpenCode(deps)).rejects.toMatchObject({
156
+ status: 409,
157
+ message: 'Restart that server yourself.',
158
+ });
159
+ expect(deps.install).not.toHaveBeenCalled();
160
+ });
161
+ });
162
+
163
+ describe('upgrade screen routes', () => {
164
+ it('hides the gate for 2.0.15 and reports install failure without upgraded:true', async () => {
165
+ const app = express();
166
+ app.use(express.json());
167
+ const readServeVersion = vi.fn()
168
+ .mockResolvedValueOnce('2.0.15')
169
+ .mockResolvedValueOnce('2.0.12')
170
+ .mockResolvedValueOnce('2.0.12');
171
+ registerUpgradeScreenRoutes(app, {
172
+ resolveOwnership: async () => ({ ownership: 'owned-cache', guidance: null }),
173
+ readServeVersion,
174
+ readCliVersion: () => '2.0.12',
175
+ platformCanInstall: true,
176
+ install: vi.fn(async () => {
177
+ throw new Error('registry unreachable');
178
+ }),
179
+ readBinaryVersion: () => '2.0.15',
180
+ persistBinary: vi.fn(),
181
+ forceBinary: vi.fn(),
182
+ restart: vi.fn(),
183
+ });
184
+
185
+ const compatible = await request(app).get('/api/opencode/compatibility').expect(200);
186
+ expect(compatible.body).toMatchObject({ state: 'compatible', version: '2.0.15', minimumVersion: '2.0.15' });
187
+
188
+ const failed = await request(app).post('/api/opencode/install-required').expect(500);
189
+ expect(failed.body).toMatchObject({
190
+ success: false,
191
+ upgraded: false,
192
+ error: 'registry unreachable',
193
+ });
194
+ expect(failed.body.upgraded).not.toBe(true);
195
+ });
196
+ });
@@ -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);
@@ -0,0 +1,155 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import path from "node:path"
4
+ import { pathToFileURL } from "node:url"
5
+
6
+ const stripJsonc = (text) => text
7
+ .replace(/\/\*[\s\S]*?\*\//g, "")
8
+ .replace(/(^|[^:\\])\/\/.*$/gm, "$1")
9
+
10
+ const readConfigPlugins = () => {
11
+ const specs = []
12
+ const files = []
13
+ if (process.env.OPENCODE_CONFIG) files.push(process.env.OPENCODE_CONFIG)
14
+ const dirs = [process.env.OPENCODE_CONFIG_DIR, path.join(homedir(), ".config", "opencode")].filter(Boolean)
15
+ for (const dir of dirs) {
16
+ files.push(path.join(dir, "opencode.jsonc"), path.join(dir, "opencode.json"), path.join(dir, "config.json"))
17
+ }
18
+ for (const file of files) {
19
+ try {
20
+ const parsed = JSON.parse(stripJsonc(readFileSync(file, "utf8")))
21
+ const plugin = parsed?.plugin
22
+ const list = Array.isArray(plugin) ? plugin : plugin ? [plugin] : []
23
+ for (const entry of list) {
24
+ if (typeof entry === "string") specs.push(entry)
25
+ else if (Array.isArray(entry) && typeof entry[0] === "string") specs.push(entry[0])
26
+ }
27
+ } catch {
28
+ // Missing or invalid config files are skipped; OpenCode still loads the ones it can parse.
29
+ }
30
+ }
31
+ return [...new Set(specs)]
32
+ }
33
+
34
+ const packageName = (spec) => {
35
+ const slash = spec.startsWith("@") ? spec.indexOf("/", 1) : -1
36
+ const versionAt = spec.lastIndexOf("@")
37
+ if (versionAt > 0 && versionAt > slash) return spec.slice(0, versionAt)
38
+ return spec
39
+ }
40
+
41
+ const resolvePackageEntry = (spec) => {
42
+ const name = packageName(spec)
43
+ const roots = [
44
+ path.join(homedir(), ".cache", "opencode", "npm"),
45
+ path.join(homedir(), ".cache", "opencode", "packages"),
46
+ ]
47
+ const candidates = []
48
+ for (const root of roots) {
49
+ let entries = []
50
+ try {
51
+ entries = readdirSync(root)
52
+ } catch {
53
+ continue
54
+ }
55
+ for (const entry of entries) {
56
+ if (entry !== name && !entry.startsWith(`${name}@`)) continue
57
+ const base = path.join(root, entry)
58
+ const packageDirs = [path.join(base, "node_modules", name)]
59
+ try {
60
+ for (const child of readdirSync(base)) {
61
+ if (/^\d+$/.test(child)) packageDirs.push(path.join(base, child, "node_modules", name))
62
+ }
63
+ } catch {
64
+ // The cache entry may be the package directory itself.
65
+ }
66
+ for (const pkgDir of packageDirs) {
67
+ try {
68
+ const pkg = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf8"))
69
+ if (pkg.name !== name) continue
70
+ const exported = pkg.exports?.["."]
71
+ const rel = typeof exported === "string" ? exported : pkg.module || pkg.main || "index.mjs"
72
+ candidates.push({
73
+ dir: pkgDir,
74
+ entry: path.resolve(pkgDir, rel),
75
+ mtime: statSync(path.join(pkgDir, "package.json")).mtimeMs,
76
+ })
77
+ } catch {
78
+ // Not a readable package directory.
79
+ }
80
+ }
81
+ }
82
+ }
83
+ candidates.sort((left, right) => right.mtime - left.mtime)
84
+ return candidates[0] || null
85
+ }
86
+
87
+ const usesCatalog = (dir) => {
88
+ const stack = [dir]
89
+ let seen = 0
90
+ while (stack.length > 0 && seen < 80) {
91
+ const current = stack.pop()
92
+ let names = []
93
+ try {
94
+ names = readdirSync(current)
95
+ } catch {
96
+ continue
97
+ }
98
+ for (const name of names) {
99
+ if (name === "node_modules" || name === ".git") continue
100
+ const full = path.join(current, name)
101
+ let info
102
+ try {
103
+ info = statSync(full)
104
+ } catch {
105
+ continue
106
+ }
107
+ if (info.isDirectory()) {
108
+ stack.push(full)
109
+ continue
110
+ }
111
+ if (!/\.(?:mjs|cjs|js)$/.test(name) || info.size > 2_000_000) continue
112
+ seen += 1
113
+ try {
114
+ if (readFileSync(full, "utf8").includes(".catalog")) return true
115
+ } catch {
116
+ // Unreadable files are not catalog callers.
117
+ }
118
+ }
119
+ }
120
+ return false
121
+ }
122
+
123
+ const catalogFor = (ctx) => ({
124
+ // OpenCode 2.0.12 removed ctx.catalog. Model plugins still call
125
+ // draft.provider.update / draft.model.update inside catalog.transform.
126
+ transform: (fn) => ctx.provider?.transform?.((draft) => fn({
127
+ provider: {
128
+ update: (id, updater) => draft.update(id, updater),
129
+ add: (...args) => draft.add?.(...args),
130
+ },
131
+ model: {
132
+ update: (providerID, modelID, updater) => draft.models?.update?.(providerID, modelID, updater),
133
+ set: (providerID, modelID, value) => draft.models?.set?.(providerID, modelID, value),
134
+ },
135
+ })),
136
+ reload: () => ctx.provider?.reload?.() ?? ctx.model?.reload?.(),
137
+ })
138
+
139
+ export default {
140
+ id: "openchamber-v2-plugin-host-shim",
141
+ setup: async (ctx) => {
142
+ if (typeof ctx?.catalog?.transform === "function") return
143
+ const patched = { ...ctx, catalog: catalogFor(ctx) }
144
+ for (const spec of readConfigPlugins()) {
145
+ const resolved = resolvePackageEntry(spec)
146
+ if (!resolved || !usesCatalog(resolved.dir)) continue
147
+ try {
148
+ const mod = await import(pathToFileURL(resolved.entry).href)
149
+ if (typeof mod.default?.setup === "function") await mod.default.setup(patched)
150
+ } catch (error) {
151
+ console.error(`[openchamber] v2 plugin host shim skipped ${spec}: ${error instanceof Error ? error.message : String(error)}`)
152
+ }
153
+ }
154
+ },
155
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "name": "openchamber-v2-plugin-host-shim"
4
+ }