@vantran-se/openchamber-web 1.24.4 → 2.0.1

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 (367) hide show
  1. package/README.md +2 -8
  2. package/bin/lib/commands-connect-url.js +3 -8
  3. package/bin/lib/commands-tunnel.js +3 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
  5. package/dist/assets/DiffView-CjxUovbM.js +4 -0
  6. package/dist/assets/FilesView-DFWD0RQR.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
  11. package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
  14. package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
  17. package/dist/assets/PlanView-BCk_VobP.js +5 -0
  18. package/dist/assets/PluginPane-BPP1dbck.js +3 -0
  19. package/dist/assets/SettingsView-kMmrpn43.js +29 -0
  20. package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
  22. package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
  26. package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
  29. package/dist/assets/de-CndEg67T.js +10 -0
  30. package/dist/assets/es-DglvPlaC.js +16 -0
  31. package/dist/assets/fr-DejPCpbS.js +14 -0
  32. package/dist/assets/index-C2r64QfD.js +1 -0
  33. package/dist/assets/index-CLcM8HKx.css +1 -0
  34. package/dist/assets/index-CvLgnuNI.js +41 -0
  35. package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
  37. package/dist/assets/ja-CVpQawCd.js +16 -0
  38. package/dist/assets/ko-Bw446h9j.js +16 -0
  39. package/dist/assets/main-Bz4WWXbr.js +2 -0
  40. package/dist/assets/main-oreqDsOV.js +896 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
  42. package/dist/assets/miniChat-D1v2gsBI.js +2 -0
  43. package/dist/assets/mobile-DBj3lCf2.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
  45. package/dist/assets/pl-BYNlPdhm.js +16 -0
  46. package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
  48. package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
  50. package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
  53. package/dist/assets/tr-DzmbxLy-.js +7 -0
  54. package/dist/assets/uk-C7vQnwro.js +16 -0
  55. package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
  57. package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
  59. package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
  60. package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
  61. package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
  62. package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
  63. package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
  64. package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
  65. package/dist/assets/zh-TW-DtII5bDb.js +16 -0
  66. package/dist/index.html +13 -7
  67. package/dist/mini-chat.html +5 -5
  68. package/dist/mobile.html +5 -5
  69. package/package.json +6 -5
  70. package/server/index.d.ts +1 -0
  71. package/server/index.js +246 -91
  72. package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
  73. package/server/lib/agent-tool/runtime.js +135 -54
  74. package/server/lib/agent-tool/runtime.test.js +155 -87
  75. package/server/lib/browser-control/DOCUMENTATION.md +37 -2
  76. package/server/lib/browser-control/provider.js +217 -0
  77. package/server/lib/browser-control/provider.test.js +251 -0
  78. package/server/lib/context-obligatory/runtime.js +61 -50
  79. package/server/lib/context-obligatory/runtime.test.js +62 -162
  80. package/server/lib/event-stream/DOCUMENTATION.md +12 -1
  81. package/server/lib/event-stream/delta-coalescer.js +0 -0
  82. package/server/lib/event-stream/delta-coalescer.test.js +68 -57
  83. package/server/lib/event-stream/directory-ws-bridge.js +1 -1
  84. package/server/lib/event-stream/global-hub.js +10 -1
  85. package/server/lib/event-stream/global-hub.test.js +27 -26
  86. package/server/lib/event-stream/index.js +6 -0
  87. package/server/lib/event-stream/protocol.js +26 -19
  88. package/server/lib/event-stream/protocol.test.js +18 -7
  89. package/server/lib/event-stream/rebind.test.js +5 -5
  90. package/server/lib/event-stream/runtime.test.js +17 -15
  91. package/server/lib/event-stream/translate-v2.js +313 -0
  92. package/server/lib/event-stream/translate-v2.test.js +209 -0
  93. package/server/lib/event-stream/upstream-reader.test.js +16 -16
  94. package/server/lib/fs/DOCUMENTATION.md +4 -2
  95. package/server/lib/fs/byte-range.js +35 -0
  96. package/server/lib/fs/byte-range.test.js +31 -0
  97. package/server/lib/fs/routes.js +47 -42
  98. package/server/lib/fs/routes.test.js +123 -18
  99. package/server/lib/git/service.js +12 -0
  100. package/server/lib/github/DOCUMENTATION.md +1 -1
  101. package/server/lib/github/pr-status.js +22 -2
  102. package/server/lib/github/pr-status.test.js +27 -14
  103. package/server/lib/guests/DOCUMENTATION.md +13 -0
  104. package/server/lib/guests/catalog.js +12 -0
  105. package/server/lib/guests/catalog.test.js +33 -0
  106. package/server/lib/guests/routes.js +10 -1
  107. package/server/lib/guests/service.js +192 -14
  108. package/server/lib/guests/service.test.js +201 -1
  109. package/server/lib/guests/surface.js +630 -0
  110. package/server/lib/guests/surface.test.js +486 -0
  111. package/server/lib/markdown-image-grants/routes.js +18 -5
  112. package/server/lib/markdown-image-grants/routes.test.js +9 -6
  113. package/server/lib/message-queue/DOCUMENTATION.md +33 -22
  114. package/server/lib/message-queue/runtime.js +155 -100
  115. package/server/lib/message-queue/runtime.test.js +168 -82
  116. package/server/lib/notifications/DOCUMENTATION.md +28 -0
  117. package/server/lib/notifications/control-stream.http.test.js +68 -0
  118. package/server/lib/notifications/emitter-runtime.js +15 -0
  119. package/server/lib/notifications/emitter-runtime.test.js +32 -0
  120. package/server/lib/notifications/push-runtime.js +5 -1
  121. package/server/lib/notifications/push-runtime.test.js +2 -2
  122. package/server/lib/notifications/routes.js +1 -1
  123. package/server/lib/notifications/runtime.js +115 -26
  124. package/server/lib/notifications/template-runtime.js +13 -15
  125. package/server/lib/notifications/template-runtime.test.js +16 -9
  126. package/server/lib/notifications/trigger-runtime.test.js +187 -0
  127. package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
  128. package/server/lib/openchamber-control/actions.js +3 -2
  129. package/server/lib/openchamber-control/error.js +1 -1
  130. package/server/lib/openchamber-control/file-open.js +59 -0
  131. package/server/lib/openchamber-control/file-open.test.js +57 -0
  132. package/server/lib/openchamber-control/routes.test.js +12 -0
  133. package/server/lib/openchamber-control/service.js +102 -49
  134. package/server/lib/openchamber-control/service.test.js +84 -44
  135. package/server/lib/openchamber-sessions/archive-store.js +224 -0
  136. package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
  137. package/server/lib/openchamber-sessions/opencode-client.js +22 -0
  138. package/server/lib/openchamber-sessions/routes.js +398 -328
  139. package/server/lib/openchamber-sessions/routes.test.js +777 -740
  140. package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
  141. package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
  142. package/server/lib/opencode/DOCUMENTATION.md +449 -29
  143. package/server/lib/opencode/agents.js +233 -327
  144. package/server/lib/opencode/auth.js +41 -44
  145. package/server/lib/opencode/auth.test.js +92 -0
  146. package/server/lib/opencode/cli-upgrade.d.ts +4 -0
  147. package/server/lib/opencode/cli-upgrade.js +18 -0
  148. package/server/lib/opencode/cli-upgrade.test.js +36 -0
  149. package/server/lib/opencode/commands.js +164 -145
  150. package/server/lib/opencode/compatibility.d.ts +16 -0
  151. package/server/lib/opencode/compatibility.js +88 -0
  152. package/server/lib/opencode/compatibility.test.js +62 -0
  153. package/server/lib/opencode/config-entity-routes.js +58 -29
  154. package/server/lib/opencode/config-entity-v2.test.js +290 -0
  155. package/server/lib/opencode/config-mutation-response.js +17 -7
  156. package/server/lib/opencode/config-mutation-response.test.js +4 -7
  157. package/server/lib/opencode/config-paths.test.js +2 -2
  158. package/server/lib/opencode/config-v2.d.ts +183 -0
  159. package/server/lib/opencode/config-v2.js +801 -0
  160. package/server/lib/opencode/config-v2.test.js +466 -0
  161. package/server/lib/opencode/credential-db.js +144 -0
  162. package/server/lib/opencode/credential-db.test.js +104 -0
  163. package/server/lib/opencode/env-runtime.js +48 -9
  164. package/server/lib/opencode/env-runtime.test.js +45 -0
  165. package/server/lib/opencode/feature-routes-runtime.js +15 -5
  166. package/server/lib/opencode/lifecycle-process.test.js +7 -4
  167. package/server/lib/opencode/lifecycle.js +85 -30
  168. package/server/lib/opencode/lifecycle.test.js +80 -8
  169. package/server/lib/opencode/login-shell-env.js +27 -0
  170. package/server/lib/opencode/managed-config-file.js +119 -0
  171. package/server/lib/opencode/managed-config-file.test.js +152 -0
  172. package/server/lib/opencode/managed-plugin-config.js +33 -13
  173. package/server/lib/opencode/mcp.js +43 -149
  174. package/server/lib/opencode/network-runtime.js +6 -7
  175. package/server/lib/opencode/openchamber-routes.test.js +2 -5
  176. package/server/lib/opencode/plugin-routes.js +3 -3
  177. package/server/lib/opencode/plugin-routes.test.js +6 -24
  178. package/server/lib/opencode/plugins.js +91 -40
  179. package/server/lib/opencode/plugins.test.js +28 -15
  180. package/server/lib/opencode/providers.js +63 -83
  181. package/server/lib/opencode/providers.test.js +128 -59
  182. package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
  183. package/server/lib/opencode/proxy.js +179 -129
  184. package/server/lib/opencode/pwa-manifest-routes.js +4 -2
  185. package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
  186. package/server/lib/opencode/response-envelope.js +14 -0
  187. package/server/lib/opencode/response-envelope.test.js +23 -0
  188. package/server/lib/opencode/routes-upgrade.test.js +73 -116
  189. package/server/lib/opencode/routes.js +121 -428
  190. package/server/lib/opencode/server-utils-runtime.js +10 -4
  191. package/server/lib/opencode/session-activity.js +78 -0
  192. package/server/lib/opencode/session-runtime.js +21 -10
  193. package/server/lib/opencode/session-runtime.test.js +9 -9
  194. package/server/lib/opencode/settings-helpers.js +9 -9
  195. package/server/lib/opencode/settings-helpers.test.js +7 -12
  196. package/server/lib/opencode/settings-registry.json +6 -8
  197. package/server/lib/opencode/settings-runtime.js +18 -1
  198. package/server/lib/opencode/settings-runtime.test.js +31 -2
  199. package/server/lib/opencode/shared.js +54 -22
  200. package/server/lib/opencode/shared.test.js +8 -4
  201. package/server/lib/opencode/shutdown-runtime.js +54 -7
  202. package/server/lib/opencode/shutdown-runtime.test.js +224 -30
  203. package/server/lib/opencode/skill-routes.js +23 -24
  204. package/server/lib/opencode/skills.js +7 -5
  205. package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
  206. package/server/lib/opencode/upgrade-capability.test.js +1 -1
  207. package/server/lib/opencode/v1-migration-topup.js +288 -0
  208. package/server/lib/opencode/v1-migration-topup.test.js +266 -0
  209. package/server/lib/opencode/v2-install.d.ts +8 -0
  210. package/server/lib/opencode/v2-install.js +158 -0
  211. package/server/lib/opencode/v2-install.test.js +124 -0
  212. package/server/lib/opencode/watcher.js +10 -3
  213. package/server/lib/opencode/watcher.test.js +17 -21
  214. package/server/lib/opencode/websearch-config.js +66 -0
  215. package/server/lib/package-manager.js +55 -68
  216. package/server/lib/package-manager.test.js +241 -75
  217. package/server/lib/permission-auto-accept/runtime.js +38 -22
  218. package/server/lib/permission-auto-accept/runtime.test.js +39 -17
  219. package/server/lib/quota/DOCUMENTATION.md +4 -0
  220. package/server/lib/quota/providers/xai.js +4 -4
  221. package/server/lib/relay/tunnel-host.js +4 -1
  222. package/server/lib/relay/tunnel-host.test.js +7 -0
  223. package/server/lib/routing/DOCUMENTATION.md +52 -21
  224. package/server/lib/routing/defaults.js +23 -6
  225. package/server/lib/routing/jev.js +17 -7
  226. package/server/lib/routing/routes.http.test.js +68 -33
  227. package/server/lib/routing/routes.js +65 -22
  228. package/server/lib/routing/runtime.js +145 -48
  229. package/server/lib/routing/runtime.test.js +79 -55
  230. package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
  231. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
  232. package/server/lib/scheduled-tasks/runtime.js +81 -74
  233. package/server/lib/scheduled-tasks/runtime.test.js +53 -1
  234. package/server/lib/session-assist/DOCUMENTATION.md +8 -3
  235. package/server/lib/session-assist/context.js +122 -46
  236. package/server/lib/session-assist/context.test.js +117 -49
  237. package/server/lib/session-assist/runtime.js +52 -22
  238. package/server/lib/session-assist/runtime.test.js +145 -173
  239. package/server/lib/session-goal/DOCUMENTATION.md +23 -15
  240. package/server/lib/session-goal/create.js +9 -12
  241. package/server/lib/session-goal/create.test.js +27 -28
  242. package/server/lib/session-goal/runtime.js +164 -77
  243. package/server/lib/session-goal/runtime.test.js +271 -519
  244. package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
  245. package/server/lib/session-knowledge/runtime.js +59 -41
  246. package/server/lib/session-knowledge/runtime.test.js +90 -20
  247. package/server/lib/small-model/DOCUMENTATION.md +154 -208
  248. package/server/lib/small-model/client.js +113 -0
  249. package/server/lib/small-model/index.js +296 -158
  250. package/server/lib/small-model/index.test.js +423 -256
  251. package/server/lib/small-model/routes.js +1 -1
  252. package/server/lib/spaces/DOCUMENTATION.md +807 -0
  253. package/server/lib/spaces/code-in-bait.js +185 -0
  254. package/server/lib/spaces/code-in.js +641 -0
  255. package/server/lib/spaces/code-in.test.js +1048 -0
  256. package/server/lib/spaces/errors.js +8 -0
  257. package/server/lib/spaces/exec-http.js +94 -0
  258. package/server/lib/spaces/gatekeeper-channel.js +202 -0
  259. package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
  260. package/server/lib/spaces/gatekeeper-program.cjs +914 -0
  261. package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
  262. package/server/lib/spaces/hardening.js +457 -0
  263. package/server/lib/spaces/hardening.test.js +368 -0
  264. package/server/lib/spaces/host-git.js +127 -0
  265. package/server/lib/spaces/host-git.test.js +137 -0
  266. package/server/lib/spaces/labels.js +234 -0
  267. package/server/lib/spaces/labels.test.js +219 -0
  268. package/server/lib/spaces/layout.js +184 -0
  269. package/server/lib/spaces/layout.test.js +30 -0
  270. package/server/lib/spaces/manager.js +82 -0
  271. package/server/lib/spaces/manager.test.js +114 -0
  272. package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
  273. package/server/lib/spaces/places/contract-suite.js +156 -0
  274. package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
  275. package/server/lib/spaces/places/contract.test.js +6 -0
  276. package/server/lib/spaces/places/docker-engine.js +126 -0
  277. package/server/lib/spaces/places/docker-live-support.js +202 -0
  278. package/server/lib/spaces/places/docker-tools.js +186 -0
  279. package/server/lib/spaces/places/docker.js +587 -0
  280. package/server/lib/spaces/places/docker.test.js +1610 -0
  281. package/server/lib/spaces/places/escape-suite.js +1018 -0
  282. package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
  283. package/server/lib/spaces/places/fake-docker.js +375 -0
  284. package/server/lib/spaces/places/memory-place.js +103 -0
  285. package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
  286. package/server/lib/spaces/places/registry.js +43 -0
  287. package/server/lib/spaces/places/registry.test.js +50 -0
  288. package/server/lib/spaces/places/server.docker.live.test.js +225 -0
  289. package/server/lib/spaces/run-command.js +261 -0
  290. package/server/lib/spaces/run-command.test.js +396 -0
  291. package/server/lib/spaces/space-server.js +190 -0
  292. package/server/lib/spaces/space-server.test.js +335 -0
  293. package/server/lib/spaces/tools-filler.js +82 -0
  294. package/server/lib/spaces/tools-filler.test.js +148 -0
  295. package/server/lib/spaces/tools-pack.js +49 -0
  296. package/server/lib/spaces/tools-pack.test.js +57 -0
  297. package/server/lib/spaces/tools.js +138 -0
  298. package/server/lib/spaces/tools.test.js +153 -0
  299. package/server/lib/tts/openai-sdk.js +11 -0
  300. package/server/lib/tts/service.js +7 -5
  301. package/server/lib/tts/stt.js +3 -2
  302. package/server/lib/tunnels/DOCUMENTATION.md +2 -0
  303. package/server/lib/tunnels/routes.js +7 -0
  304. package/server/lib/tunnels/routes.test.js +55 -0
  305. package/server/lib/ui-auth/ui-auth.js +38 -2
  306. package/server/lib/ui-auth/ui-auth.test.js +42 -0
  307. package/server/lib/ui-auth/ui-passkeys.js +12 -10
  308. package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
  309. package/server/lib/walkthrough/pull-request.js +61 -8
  310. package/server/lib/walkthrough/pull-request.test.js +54 -1
  311. package/server/lib/walkthrough/routes.js +20 -0
  312. package/server/lib/walkthrough/routes.test.js +19 -0
  313. package/server/opencode-proxy.test.js +190 -166
  314. package/server/opencode-worktree-readiness.test.js +5 -4
  315. package/dist/assets/DiffView-B2CD9wC8.js +0 -4
  316. package/dist/assets/FilesView-C4vMFclc.js +0 -13
  317. package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
  318. package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
  320. package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
  321. package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
  322. package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
  323. package/dist/assets/SettingsView-CSinuH-1.js +0 -29
  324. package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
  325. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
  326. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
  327. package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
  328. package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
  329. package/dist/assets/de-Bro6_zyl.js +0 -10
  330. package/dist/assets/es-Bb9XQZvd.js +0 -16
  331. package/dist/assets/fr-Dgfzn-IL.js +0 -14
  332. package/dist/assets/index-CtQNmqUL.js +0 -1
  333. package/dist/assets/index-DqaMWNrC.css +0 -1
  334. package/dist/assets/index-Dqc6USl3.js +0 -40
  335. package/dist/assets/ja-BYsakGFh.js +0 -16
  336. package/dist/assets/ko-BSaQz9aN.js +0 -16
  337. package/dist/assets/main-B62_pjL5.js +0 -2
  338. package/dist/assets/main-BEdcOI2P.js +0 -896
  339. package/dist/assets/miniChat-LT8Rqofr.js +0 -2
  340. package/dist/assets/mobile-CqarEynh.js +0 -2
  341. package/dist/assets/pl-OIviPdoj.js +0 -16
  342. package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
  343. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
  344. package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
  345. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
  346. package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
  347. package/dist/assets/tr-CQI-KPre.js +0 -7
  348. package/dist/assets/uk-CqW7R4DA.js +0 -16
  349. package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
  350. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
  351. package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
  352. package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
  353. package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
  354. package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
  355. package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
  356. package/server/lib/routing/feature-flag.js +0 -11
  357. package/server/lib/small-model/call.js +0 -851
  358. package/server/lib/small-model/call.test.js +0 -1292
  359. package/server/lib/small-model/catalog.js +0 -13
  360. package/server/lib/small-model/resolve.js +0 -131
  361. package/server/lib/small-model/resolve.test.js +0 -197
  362. package/server/lib/small-model/runtime-providers.js +0 -164
  363. package/server/lib/small-model/runtime-providers.test.js +0 -118
  364. package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
  365. package/server/lib/system-prompt/runtime.js +0 -49
  366. package/server/lib/system-prompt/runtime.test.js +0 -76
  367. package/server/lib/walkthrough/reproduce-2607.test.js +0 -148
@@ -9,8 +9,19 @@ import {
9
9
  getJsonWriteTarget,
10
10
  writeConfig,
11
11
  } from './shared.js';
12
+ import {
13
+ toMcpEntity,
14
+ readLayeredMcpEntries,
15
+ writeMcpEntry,
16
+ deleteMcpEntry,
17
+ } from './config-v2.js';
12
18
 
13
19
  // ============== MCP CONFIG HELPERS ==============
20
+ //
21
+ // OpenCode 2 keeps servers under `mcp.servers`; v1 kept them directly under
22
+ // `mcp`. OpenChamber reads both and always writes `mcp.servers` with the v2
23
+ // entry shape (`disabled`, `timeout: {catalog, execution}`, snake_case OAuth,
24
+ // required `type`).
14
25
 
15
26
  /**
16
27
  * Validate MCP server name
@@ -24,9 +35,6 @@ function validateMcpName(name) {
24
35
  }
25
36
  }
26
37
 
27
- /**
28
- * List all MCP server configs from user-level opencode.json
29
- */
30
38
  function resolveMcpScopeFromPath(layers, sourcePath) {
31
39
  if (!sourcePath) return null;
32
40
  return sourcePath === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
@@ -40,20 +48,23 @@ function ensureProjectMcpConfigPath(workingDirectory) {
40
48
  return path.join(configDir, 'opencode.json');
41
49
  }
42
50
 
51
+ /** Same precedence as `getJsonEntrySource`: custom > project > user. */
52
+ function readMcpEntriesAcrossLayers(layers) {
53
+ return readLayeredMcpEntries([layers?.userConfig, layers?.projectConfig, layers?.customConfig]);
54
+ }
55
+
43
56
  function listMcpConfigs(workingDirectory) {
44
57
  const layers = readConfigLayers(workingDirectory);
45
- const mcp = layers?.mergedConfig?.mcp || {};
46
-
47
- return Object.entries(mcp)
48
- .filter(([, entry]) => entry && typeof entry === 'object' && !Array.isArray(entry))
49
- .map(([name, entry]) => {
50
- const source = getJsonEntrySource(layers, 'mcp', name);
51
- return {
52
- name,
53
- ...buildMcpEntry(entry),
54
- scope: resolveMcpScopeFromPath(layers, source.path),
55
- };
56
- });
58
+ return Array.from(readMcpEntriesAcrossLayers(layers).entries()).map(([name, entry]) => {
59
+ const source = getJsonEntrySource(layers, 'mcp', name);
60
+ return {
61
+ name,
62
+ ...toMcpEntity(entry.value),
63
+ scope: resolveMcpScopeFromPath(layers, source.path),
64
+ sectionKey: source.sectionKey,
65
+ legacy: Boolean(source.legacy),
66
+ };
67
+ });
57
68
  }
58
69
 
59
70
  /**
@@ -61,7 +72,7 @@ function listMcpConfigs(workingDirectory) {
61
72
  */
62
73
  function getMcpConfig(name, workingDirectory) {
63
74
  const layers = readConfigLayers(workingDirectory);
64
- const entry = layers?.mergedConfig?.mcp?.[name];
75
+ const entry = readMcpEntriesAcrossLayers(layers).get(name);
65
76
 
66
77
  if (!entry) {
67
78
  return null;
@@ -69,8 +80,10 @@ function getMcpConfig(name, workingDirectory) {
69
80
  const source = getJsonEntrySource(layers, 'mcp', name);
70
81
  return {
71
82
  name,
72
- ...buildMcpEntry(entry),
83
+ ...toMcpEntity(entry.value),
73
84
  scope: resolveMcpScopeFromPath(layers, source.path),
85
+ sectionKey: source.sectionKey,
86
+ legacy: Boolean(source.legacy),
74
87
  };
75
88
  }
76
89
 
@@ -81,8 +94,7 @@ function createMcpConfig(name, mcpConfig, workingDirectory, scope) {
81
94
  validateMcpName(name);
82
95
 
83
96
  const layers = readConfigLayers(workingDirectory);
84
- const source = getJsonEntrySource(layers, 'mcp', name);
85
- if (source.exists) {
97
+ if (getJsonEntrySource(layers, 'mcp', name).exists) {
86
98
  throw new Error(`MCP server "${name}" already exists`);
87
99
  }
88
100
 
@@ -101,19 +113,17 @@ function createMcpConfig(name, mcpConfig, workingDirectory, scope) {
101
113
  config = jsonTarget.config || {};
102
114
  }
103
115
 
104
- if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
105
- config.mcp = {};
106
- }
107
-
108
- const { name: _ignoredName, ...entryData } = mcpConfig;
109
- config.mcp[name] = buildMcpEntry(entryData);
116
+ const { name: _ignoredName, scope: _ignoredScope, ...entryData } = mcpConfig || {};
117
+ writeMcpEntry(config, name, toMcpEntity(entryData));
110
118
 
111
119
  writeConfig(config, targetPath);
112
120
  console.log(`Created MCP server config: ${name}`);
121
+ return { path: targetPath };
113
122
  }
114
123
 
115
124
  /**
116
- * Update an existing MCP server config entry
125
+ * Update an existing MCP server config entry. A server still stored under the
126
+ * v1 `mcp.<name>` key is rewritten into `mcp.servers` in the same file.
117
127
  */
118
128
  function updateMcpConfig(name, updates, workingDirectory) {
119
129
  const layers = readConfigLayers(workingDirectory);
@@ -126,17 +136,13 @@ function updateMcpConfig(name, updates, workingDirectory) {
126
136
  const targetPath = source.path || CONFIG_FILE;
127
137
  const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
128
138
 
129
- if (!config.mcp || typeof config.mcp !== 'object' || Array.isArray(config.mcp)) {
130
- config.mcp = {};
131
- }
132
-
133
- const existing = config.mcp[name];
134
- const { name: _ignoredName, ...updateData } = updates;
135
-
136
- config.mcp[name] = buildMcpEntry({ ...existing, ...updateData });
139
+ const existing = toMcpEntity(source.section);
140
+ const { name: _ignoredName, scope: _ignoredScope, ...updateData } = updates || {};
141
+ writeMcpEntry(config, name, toMcpEntity({ ...existing, ...updateData }));
137
142
 
138
143
  writeConfig(config, targetPath);
139
- console.log(`Updated MCP server config: ${name}`);
144
+ console.log(`Updated MCP server config: ${name} (${targetPath})`);
145
+ return { path: targetPath };
140
146
  }
141
147
 
142
148
  /**
@@ -148,125 +154,13 @@ function deleteMcpConfig(name, workingDirectory) {
148
154
  const targetPath = source.path || CONFIG_FILE;
149
155
  const config = source.config || (fs.existsSync(targetPath) ? readConfigFile(targetPath) : {});
150
156
 
151
- if (!config.mcp || typeof config.mcp !== 'object' || config.mcp[name] === undefined) {
157
+ if (!deleteMcpEntry(config, name)) {
152
158
  throw new Error(`MCP server "${name}" not found`);
153
159
  }
154
160
 
155
- delete config.mcp[name];
156
-
157
- if (Object.keys(config.mcp).length === 0) {
158
- delete config.mcp;
159
- }
160
-
161
161
  writeConfig(config, targetPath);
162
162
  console.log(`Deleted MCP server config: ${name}`);
163
- }
164
-
165
- /**
166
- * Build a clean MCP entry object, omitting undefined/null values
167
- */
168
- function buildMcpEntry(data) {
169
- const entry = (data && typeof data === 'object' && !Array.isArray(data))
170
- ? { ...data }
171
- : {};
172
-
173
- delete entry.name;
174
- delete entry.scope;
175
-
176
- // type is required
177
- entry.type = data.type === 'remote' ? 'remote' : 'local';
178
-
179
- if (entry.type === 'local') {
180
- // command must be a non-empty array of strings
181
- if (Array.isArray(data.command) && data.command.length > 0) {
182
- entry.command = data.command.map(String);
183
- } else {
184
- delete entry.command;
185
- }
186
-
187
- delete entry.url;
188
- delete entry.headers;
189
- delete entry.oauth;
190
- delete entry.timeout;
191
- } else {
192
- // remote: url required
193
- if (data.url && typeof data.url === 'string') {
194
- entry.url = data.url.trim();
195
- } else {
196
- delete entry.url;
197
- }
198
-
199
- delete entry.command;
200
-
201
- if (data.headers && typeof data.headers === 'object' && !Array.isArray(data.headers)) {
202
- const cleaned = {};
203
- for (const [k, v] of Object.entries(data.headers)) {
204
- if (k && v !== undefined && v !== null) {
205
- cleaned[k] = String(v);
206
- }
207
- }
208
- if (Object.keys(cleaned).length > 0) {
209
- entry.headers = cleaned;
210
- } else {
211
- delete entry.headers;
212
- }
213
- } else if (data.headers === undefined) {
214
- delete entry.headers;
215
- }
216
-
217
- if (data.oauth === false) {
218
- entry.oauth = false;
219
- } else if (data.oauth && typeof data.oauth === 'object' && !Array.isArray(data.oauth)) {
220
- const oauth = {};
221
- if (typeof data.oauth.clientId === 'string' && data.oauth.clientId.trim()) {
222
- oauth.clientId = data.oauth.clientId.trim();
223
- }
224
- if (typeof data.oauth.clientSecret === 'string' && data.oauth.clientSecret.trim()) {
225
- oauth.clientSecret = data.oauth.clientSecret.trim();
226
- }
227
- if (typeof data.oauth.scope === 'string' && data.oauth.scope.trim()) {
228
- oauth.scope = data.oauth.scope.trim();
229
- }
230
- if (typeof data.oauth.redirectUri === 'string' && data.oauth.redirectUri.trim()) {
231
- oauth.redirectUri = data.oauth.redirectUri.trim();
232
- }
233
- if (Object.keys(oauth).length > 0) {
234
- entry.oauth = oauth;
235
- } else {
236
- delete entry.oauth;
237
- }
238
- } else if (data.oauth === undefined) {
239
- delete entry.oauth;
240
- }
241
-
242
- if (typeof data.timeout === 'number' && Number.isFinite(data.timeout) && data.timeout > 0) {
243
- entry.timeout = data.timeout;
244
- } else if (data.timeout === undefined || data.timeout === null || data.timeout === '') {
245
- delete entry.timeout;
246
- }
247
- }
248
-
249
- // environment: flat Record<string, string>
250
- if (data.environment && typeof data.environment === 'object' && !Array.isArray(data.environment)) {
251
- const cleaned = {};
252
- for (const [k, v] of Object.entries(data.environment)) {
253
- if (k && v !== undefined && v !== null) {
254
- cleaned[k] = String(v);
255
- }
256
- }
257
- if (Object.keys(cleaned).length > 0) {
258
- entry.environment = cleaned;
259
- } else {
260
- delete entry.environment;
261
- }
262
- } else if (data.environment === undefined) {
263
- delete entry.environment;
264
- }
265
-
266
- // enabled defaults to true
267
- entry.enabled = data.enabled !== false;
268
-
269
- return entry;
163
+ return { path: targetPath };
270
164
  }
271
165
 
272
166
  export {
@@ -1,3 +1,4 @@
1
+ import { readOpenCodeInfo, isSupportedOpenCodeVersion } from './compatibility.js';
1
2
  export const createOpenCodeNetworkRuntime = (deps) => {
2
3
  const {
3
4
  state,
@@ -46,7 +47,9 @@ export const createOpenCodeNetworkRuntime = (deps) => {
46
47
  try {
47
48
  const controller = new AbortController();
48
49
  timeout = setTimeout(() => controller.abort(), 3000);
49
- const response = await fetch(`${url.replace(/\/+$/, '')}/global/health`, {
50
+ // OpenCode 2.0.8 replaced `/api/health` with `/api/info`: a 200 is the
51
+ // readiness signal, the payload carries no `healthy` field.
52
+ const response = await fetch(`${url.replace(/\/+$/, '')}/api/info`, {
50
53
  method: 'GET',
51
54
  headers: {
52
55
  Accept: 'application/json',
@@ -57,12 +60,8 @@ export const createOpenCodeNetworkRuntime = (deps) => {
57
60
  clearTimeout(timeout);
58
61
  timeout = null;
59
62
 
60
- if (response.ok) {
61
- const body = await response.json().catch(() => null);
62
- if (body?.healthy === true) {
63
- return true;
64
- }
65
- }
63
+ const info = await readOpenCodeInfo(response);
64
+ if (info && isSupportedOpenCodeVersion(info.version)) return true;
66
65
  } catch {
67
66
  } finally {
68
67
  if (timeout) {
@@ -68,9 +68,7 @@ beforeEach(() => {
68
68
  packageManager.detectPackageManagerDetails.mockReturnValue({
69
69
  packageManager: 'npm',
70
70
  });
71
- packageManager.getUpdateCommand.mockReturnValue(
72
- 'npm install -g @vantran-se/openchamber-web@latest --registry=https://registry.npmjs.org',
73
- );
71
+ packageManager.getUpdateCommand.mockReturnValue('npm install -g @openchamber/web@latest');
74
72
  });
75
73
 
76
74
  afterEach(() => {
@@ -266,7 +264,7 @@ describe('OpenChamber foreground update route', () => {
266
264
  '--setenv=PATH=/home/syu/.npm-global/bin:/usr/bin:/bin',
267
265
  '/bin/sh',
268
266
  '-c',
269
- "set -eu\nnpm install -g @vantran-se/openchamber-web@latest --registry=https://registry.npmjs.org\nsystemctl --user restart 'openchamber@wsl.service'",
267
+ "set -eu\nnpm install -g @openchamber/web@latest\nsystemctl --user restart 'openchamber@wsl.service'",
270
268
  ], {
271
269
  encoding: 'utf8',
272
270
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -335,4 +333,3 @@ describe('OpenChamber web update route on Windows', () => {
335
333
  expect(logError).toHaveBeenCalledOnce();
336
334
  });
337
335
  });
338
-
@@ -3,7 +3,7 @@ import os from 'os';
3
3
 
4
4
  import { getNpmInfo as defaultGetNpmInfo } from './npm-registry.js';
5
5
  import { isExactSemver as defaultIsExactSemver, isPathSpec as defaultIsPathSpec, parseNpmSpec as defaultParseNpmSpec, parsePathSpec as defaultParsePathSpec } from './plugin-spec.js';
6
- import { buildDeferredRestartResponse } from './config-mutation-response.js';
6
+ import { buildAppliedResponse } from './config-mutation-response.js';
7
7
 
8
8
  const ENTRY_EXISTS_CODES = new Set(['ENTRY_EXISTS', 'EEXIST']);
9
9
  const FILE_EXISTS_CODES = new Set(['FILE_EXISTS', 'EEXIST']);
@@ -46,8 +46,8 @@ export const registerPluginRoutes = (app, dependencies) => {
46
46
  applyChange();
47
47
 
48
48
  const pastTense = operation.replace(/ion$/, 'ed').replace(/update$/, 'updated');
49
- return res.json(buildDeferredRestartResponse(
50
- `Plugin ${pastTense}. Restart OpenCode to apply.`,
49
+ return res.json(buildAppliedResponse(
50
+ `Plugin ${pastTense}.`,
51
51
  ));
52
52
  };
53
53
 
@@ -268,10 +268,7 @@ describe('opencode plugin routes', () => {
268
268
 
269
269
  expect(response.body).toMatchObject({
270
270
  success: true,
271
- requiresReload: false,
272
- requiresRestart: true,
273
- restartDeferred: true,
274
- message: 'Plugin entry created. Restart OpenCode to apply.',
271
+ message: 'Plugin entry created.',
275
272
  });
276
273
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
277
274
  });
@@ -307,10 +304,7 @@ describe('opencode plugin routes', () => {
307
304
 
308
305
  expect(response.body.success).toBe(true);
309
306
  expect(response.body).toMatchObject({
310
- requiresReload: false,
311
- requiresRestart: true,
312
- restartDeferred: true,
313
- message: 'Plugin entry updated. Restart OpenCode to apply.',
307
+ message: 'Plugin entry updated.',
314
308
  });
315
309
  const after = await request(app).get('/api/config/plugins').expect(200);
316
310
  expect(after.body.entries[0]).toEqual(expect.objectContaining({ spec: 'b', scope: 'user' }));
@@ -329,10 +323,7 @@ describe('opencode plugin routes', () => {
329
323
  expect(readJson(userConfigPath).plugin).toBeUndefined();
330
324
  expect(response.body).toMatchObject({
331
325
  success: true,
332
- requiresReload: false,
333
- requiresRestart: true,
334
- restartDeferred: true,
335
- message: 'Plugin entry deleted. Restart OpenCode to apply.',
326
+ message: 'Plugin entry deleted.',
336
327
  });
337
328
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
338
329
  });
@@ -342,10 +333,7 @@ describe('opencode plugin routes', () => {
342
333
 
343
334
  expect(response.body).toMatchObject({
344
335
  success: true,
345
- requiresReload: false,
346
- requiresRestart: true,
347
- restartDeferred: true,
348
- message: 'Plugin file created. Restart OpenCode to apply.',
336
+ message: 'Plugin file created.',
349
337
  });
350
338
  expect(fs.readFileSync(path.join(rootDir, 'plugins', 'test.js'), 'utf8')).toBe('//x');
351
339
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
@@ -375,10 +363,7 @@ describe('opencode plugin routes', () => {
375
363
  expect(fs.readFileSync(path.join(rootDir, 'plugins', 'test.js'), 'utf8')).toBe('//y');
376
364
  expect(response.body).toMatchObject({
377
365
  success: true,
378
- requiresReload: false,
379
- requiresRestart: true,
380
- restartDeferred: true,
381
- message: 'Plugin file updated. Restart OpenCode to apply.',
366
+ message: 'Plugin file updated.',
382
367
  });
383
368
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
384
369
  });
@@ -393,10 +378,7 @@ describe('opencode plugin routes', () => {
393
378
  expect(fs.existsSync(path.join(rootDir, 'plugins', 'test.js'))).toBe(false);
394
379
  expect(response.body).toMatchObject({
395
380
  success: true,
396
- requiresReload: false,
397
- requiresRestart: true,
398
- restartDeferred: true,
399
- message: 'Plugin file deleted. Restart OpenCode to apply.',
381
+ message: 'Plugin file deleted.',
400
382
  });
401
383
  expect(refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
402
384
  });
@@ -8,6 +8,11 @@ import {
8
8
  writeConfig,
9
9
  } from './shared.js';
10
10
  import { isPathSpec } from './plugin-spec.js';
11
+ import { PLUGIN_SECTION, toPluginEntity, fromPluginEntity, readPluginList } from './config-v2.js';
12
+
13
+ // OpenCode 2 reads `plugins` (bare string or `{package, options}`) and still
14
+ // decodes the v1 `plugin` array with its `[spec, options]` tuples. OpenChamber
15
+ // reads both and always writes `plugins` with v2 entries.
11
16
 
12
17
  const PLUGIN_FILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-_.]*\.(js|ts|mjs|cjs)$/;
13
18
 
@@ -77,7 +82,6 @@ function getActiveOpencodeConfigDir() {
77
82
  function getActiveUserConfigPaths() {
78
83
  const configDir = getActiveOpencodeConfigDir();
79
84
  return [
80
- path.join(configDir, 'config.json'),
81
85
  path.join(configDir, 'opencode.json'),
82
86
  path.join(configDir, 'opencode.jsonc'),
83
87
  ];
@@ -184,9 +188,12 @@ function getPluginTarget(id, workingDirectory) {
184
188
  validateScope(scope);
185
189
  const layers = readPluginConfigLayers(workingDirectory);
186
190
  const source = configSources(layers).find((candidate) => candidate.scope === scope);
187
- const plugin = Array.isArray(source?.config?.plugin) ? source.config.plugin : [];
191
+ if (!source) {
192
+ return null;
193
+ }
194
+ const plugin = pluginArrayForWrite(source.config);
188
195
  const index = plugin.findIndex((raw) => parsePluginRaw(raw).spec === spec);
189
- if (!source || index === -1) {
196
+ if (index === -1) {
190
197
  return null;
191
198
  }
192
199
  return { source, plugin, index };
@@ -203,6 +210,16 @@ function pluginDirForScope(scope, workingDirectory) {
203
210
  return path.join(getActiveOpencodeConfigDir(), 'plugins');
204
211
  }
205
212
 
213
+ /**
214
+ * The directories OpenCode 2 scans for local plugins in a scope: `plugins/`
215
+ * (where OpenChamber writes) and the v1 `plugin/`. Each holds `.ts`/`.js`
216
+ * files and plugin package directories.
217
+ */
218
+ function pluginDirsForScope(scope, workingDirectory) {
219
+ const preferred = pluginDirForScope(scope, workingDirectory);
220
+ return [preferred, path.join(path.dirname(preferred), 'plugin')];
221
+ }
222
+
206
223
  function fileTargetFromId(id, workingDirectory) {
207
224
  const decoded = decodePluginId(id);
208
225
  if (decoded.prefix !== 'file') {
@@ -231,41 +248,71 @@ function decodePluginId(id) {
231
248
  return { prefix: decoded.slice(0, separator), value: decoded.slice(separator + 1) };
232
249
  }
233
250
 
251
+ /** Accept a v1 string/tuple or a v2 `{package, options}` object. */
234
252
  function parsePluginRaw(raw) {
235
- if (typeof raw === 'string') {
236
- return { spec: validatePluginSpec(raw) };
237
- }
238
- if (Array.isArray(raw) && raw.length === 2 && isRecord(raw[1])) {
239
- return { spec: validatePluginSpec(raw[0]), options: { ...raw[1] } };
253
+ const entity = toPluginEntity(raw);
254
+ if (!entity) {
255
+ throw codedError('Plugin spec must be a string, [string, object], or {package, options}', 'INVALID_SPEC');
240
256
  }
241
- throw codedError('Plugin spec must be a string or [string, object]', 'INVALID_SPEC');
257
+ const parsed = { spec: validatePluginSpec(entity.package) };
258
+ if (hasOptions(entity.options)) parsed.options = { ...entity.options };
259
+ return parsed;
242
260
  }
243
261
 
262
+ /** Always v2: a bare string, or `{package, options}`. Never a tuple. */
244
263
  function serializePluginEntry(entry) {
245
264
  const spec = validatePluginSpec(entry?.spec);
246
- if (hasOptions(entry?.options)) {
247
- return [spec, { ...entry.options }];
265
+ const serialized = fromPluginEntity({ package: spec, options: hasOptions(entry?.options) ? entry.options : undefined });
266
+ if (serialized === null) {
267
+ throw codedError('Plugin spec must be a non-empty string', 'INVALID_SPEC');
248
268
  }
249
- return spec;
269
+ return serialized;
270
+ }
271
+
272
+ function migratePluginRaw(raw) {
273
+ try {
274
+ return serializePluginEntry(parsePluginRaw(raw));
275
+ } catch {
276
+ return raw;
277
+ }
278
+ }
279
+
280
+ /**
281
+ * The plugin array a write should target. Prefer the v2 `plugins` key; when the
282
+ * file only has the v1 `plugin` array, migrate the whole array into `plugins`
283
+ * in place so the two spellings cannot disagree.
284
+ */
285
+ function pluginArrayForWrite(config) {
286
+ const legacy = Array.isArray(config[PLUGIN_SECTION.v1]) ? config[PLUGIN_SECTION.v1] : null;
287
+ if (!Array.isArray(config[PLUGIN_SECTION.v2])) config[PLUGIN_SECTION.v2] = [];
288
+ if (legacy) {
289
+ config[PLUGIN_SECTION.v2] = [
290
+ // An entry we cannot parse is carried over untouched rather than dropped:
291
+ // it is the user's config, and losing it would be worse than leaving it
292
+ // in a shape OpenCode already refuses.
293
+ ...legacy.map((raw) => migratePluginRaw(raw)),
294
+ ...config[PLUGIN_SECTION.v2],
295
+ ];
296
+ delete config[PLUGIN_SECTION.v1];
297
+ }
298
+ return config[PLUGIN_SECTION.v2];
250
299
  }
251
300
 
252
301
  function listPluginEntries(workingDirectory) {
253
302
  const layers = readPluginConfigLayers(workingDirectory);
254
303
  return configSources(layers).flatMap((source) => {
255
- if (!Array.isArray(source.config?.plugin)) {
256
- return [];
257
- }
258
- return source.config.plugin.map((raw) => {
259
- const parsed = parsePluginRaw(raw);
260
- return {
261
- id: encodePluginId('config', `${source.scope}:${parsed.spec}`),
262
- spec: parsed.spec,
263
- ...(parsed.options !== undefined ? { options: parsed.options } : {}),
304
+ return readPluginList(source.config).map((item) => {
305
+ const entry = {
306
+ id: encodePluginId('config', `${source.scope}:${item.entry.package}`),
307
+ spec: item.entry.package,
264
308
  scope: source.scope,
265
309
  kind: 'config',
266
- parsedKind: parsedKindForSpec(parsed.spec),
310
+ parsedKind: parsedKindForSpec(item.entry.package),
267
311
  sourcePath: source.filePath,
312
+ legacy: item.legacy,
268
313
  };
314
+ if (item.entry.options !== undefined) entry.options = item.entry.options;
315
+ return entry;
269
316
  });
270
317
  });
271
318
  }
@@ -282,8 +329,7 @@ function createPluginEntry(entry, workingDirectory) {
282
329
  const layers = readPluginConfigLayers(workingDirectory);
283
330
  const existing = configSources(layers).find((source) => (
284
331
  source.scope === scope
285
- && Array.isArray(source.config?.plugin)
286
- && source.config.plugin.some((raw) => parsePluginRaw(raw).spec === spec)
332
+ && readPluginList(source.config).some((item) => item.entry.package === spec)
287
333
  ));
288
334
  if (existing) {
289
335
  throw codedError(`Plugin "${spec}" already exists`, 'ENTRY_EXISTS');
@@ -299,10 +345,7 @@ function createPluginEntry(entry, workingDirectory) {
299
345
  config = layers.paths.customPath ? layers.customConfig : layers.userConfig;
300
346
  }
301
347
 
302
- if (!Array.isArray(config.plugin)) {
303
- config.plugin = [];
304
- }
305
- config.plugin.push(serializePluginEntry({ spec, options: entry.options }));
348
+ pluginArrayForWrite(config).push(serializePluginEntry({ spec, options: entry.options }));
306
349
  writeConfig(config, targetPath);
307
350
  }
308
351
 
@@ -325,7 +368,7 @@ function deletePluginEntry(id, workingDirectory) {
325
368
  }
326
369
  target.plugin.splice(target.index, 1);
327
370
  if (target.plugin.length === 0) {
328
- delete target.source.config.plugin;
371
+ delete target.source.config[PLUGIN_SECTION.v2];
329
372
  }
330
373
  writeConfig(target.source.config, target.source.filePath);
331
374
  }
@@ -335,21 +378,29 @@ function listPluginDirFiles(workingDirectory) {
335
378
  if (workingDirectory) {
336
379
  scopes.push(AGENT_SCOPE.PROJECT);
337
380
  }
338
- return scopes.flatMap((scope) => {
339
- const dir = pluginDirForScope(scope, workingDirectory);
381
+ return scopes.flatMap((scope) => pluginDirsForScope(scope, workingDirectory).flatMap((dir) => {
340
382
  if (!fs.existsSync(dir)) {
341
383
  return [];
342
384
  }
385
+ const editable = dir === pluginDirForScope(scope, workingDirectory);
343
386
  return fs.readdirSync(dir, { withFileTypes: true })
344
- .filter((entry) => entry.isFile() && PLUGIN_FILE_NAME_PATTERN.test(entry.name) && !entry.name.includes('..'))
345
- .map((entry) => ({
346
- id: encodePluginId('file', `${scope}:${entry.name}`),
347
- fileName: entry.name,
348
- scope,
349
- kind: 'file',
350
- absolutePath: path.join(dir, entry.name),
351
- }));
352
- });
387
+ .filter((entry) => !entry.name.includes('..') && !entry.name.startsWith('.'))
388
+ .flatMap((entry) => {
389
+ if (entry.isFile() && PLUGIN_FILE_NAME_PATTERN.test(entry.name)) {
390
+ // Only files in the v2 `plugins/` directory are editable through
391
+ // the file id; a v1 `plugin/` file is listed for what it is.
392
+ return editable
393
+ ? [{ id: encodePluginId('file', `${scope}:${entry.name}`), fileName: entry.name, scope, kind: 'file', absolutePath: path.join(dir, entry.name) }]
394
+ : [{ id: encodePluginId('legacy', `${scope}:${entry.name}`), fileName: entry.name, scope, kind: 'package', absolutePath: path.join(dir, entry.name) }];
395
+ }
396
+ if (entry.isDirectory()) {
397
+ // A plugin package (a directory with its own entry point): OpenCode
398
+ // loads it, OpenChamber only lists it.
399
+ return [{ id: encodePluginId('package', `${scope}:${entry.name}`), fileName: entry.name, scope, kind: 'package', absolutePath: path.join(dir, entry.name) }];
400
+ }
401
+ return [];
402
+ });
403
+ }));
353
404
  }
354
405
 
355
406
  function readPluginDirFile(id, workingDirectory) {