@vantran-se/openchamber-web 1.24.4 → 2.0.2

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-B82Oye3V.js} +1 -1
  5. package/dist/assets/DiffView-DJLmWn6Z.js +4 -0
  6. package/dist/assets/FilesView-DjksmZRo.js +17 -0
  7. package/dist/assets/{GitView-Byu2DY7s.js → GitView-DiUsUIgr.js} +4 -4
  8. package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-CYwWYLr3.js} +1 -1
  9. package/dist/assets/GuestSurfacePane-CmctyRI4.js +1 -0
  10. package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-DOHHcbgJ.js} +1 -1
  11. package/dist/assets/LinearIssuesView-DaqnylcV.js +1 -0
  12. package/dist/assets/MarkdownImageGallery-HJNvWt7L.js +1 -0
  13. package/dist/assets/MarkdownRendererImpl-nDZRGg2I.js +12 -0
  14. package/dist/assets/OnboardingScreen-C2B5Bsof.js +1 -0
  15. package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-C4FLBjpt.js} +1 -1
  16. package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-C_1S9c9n.js} +1 -1
  17. package/dist/assets/PlanView-_eZvb-G5.js +5 -0
  18. package/dist/assets/PluginPane-sM8fcZIV.js +3 -0
  19. package/dist/assets/SettingsView-BRfr46hb.js +29 -0
  20. package/dist/assets/SettingsWindow-CcUDwQHg.js +1 -0
  21. package/dist/assets/SubmoduleDiffSummary-CrN1ZGwl.js +1 -0
  22. package/dist/assets/ToolOutputDialog-Cij4jI09.js +15 -0
  23. package/dist/assets/ToolPartDiffPreview-BXzwQ-JY.js +8 -0
  24. package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-DSWTLJNH.js} +4 -4
  25. package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-CpUUgY8H.js} +2 -2
  26. package/dist/assets/appThemeRegistry-B5tHTkR8.js +1 -0
  27. package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-D7D8qV8p.js} +1 -1
  28. package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-cq9uQ30i.js} +1 -1
  29. package/dist/assets/de-CUO1-KqF.js +10 -0
  30. package/dist/assets/es-BlbC6sGJ.js +16 -0
  31. package/dist/assets/fr-D2nwe_0W.js +14 -0
  32. package/dist/assets/index-BQOoYIOu.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-D0obJ2p8.js} +1 -1
  36. package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-DB3_rjyW.js} +1 -1
  37. package/dist/assets/ja-CA7vnDsZ.js +16 -0
  38. package/dist/assets/ko-Chy6a9l-.js +16 -0
  39. package/dist/assets/main-B9JFK3vF.js +896 -0
  40. package/dist/assets/main-Bhiqnrhd.js +2 -0
  41. package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-tfqOFaBd.js} +4 -4
  42. package/dist/assets/miniChat-B0UOL5zA.js +2 -0
  43. package/dist/assets/mobile-BJppFIEo.js +2 -0
  44. package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-Db6KuwCP.js} +3 -3
  45. package/dist/assets/pl-CTR5A_1P.js +16 -0
  46. package/dist/assets/pt-BR-BE1O1KEx.js +16 -0
  47. package/dist/assets/renderElectronMiniChatApp-Yz5sfoFh.js +2 -0
  48. package/dist/assets/renderMobileApp-QQ-yB8aY.js +25 -0
  49. package/dist/assets/runtimeEndpointReset-DKDdzRzm.js +27 -0
  50. package/dist/assets/scheduledTasksApi-C5sCuNqf.js +1 -0
  51. package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-xURoo5t_.js} +1 -1
  52. package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-BZLGuYTU.js} +1 -1
  53. package/dist/assets/tr-Bh5vK6cW.js +7 -0
  54. package/dist/assets/uk-CLDkZFy_.js +16 -0
  55. package/dist/assets/useAppFontEffects-BxrtCnfR.js +7337 -0
  56. package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CeXuR-SR.js} +2 -2
  57. package/dist/assets/useRootScrollLock-DgWxezc1.js +2 -0
  58. package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-DOuaQUZ0.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-63H1h21j.js} +2 -2
  64. package/dist/assets/zh-CN-DODgcmYj.js +16 -0
  65. package/dist/assets/zh-TW-ByNfIwLA.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
@@ -0,0 +1,801 @@
1
+ /**
2
+ * Canonical OpenCode 2.x config shapes for every OpenChamber entity surface.
3
+ *
4
+ * OpenChamber reads user-authored OpenCode config, so it must accept the v1
5
+ * spellings OpenCode 2 still decodes (section key `agent`, permission maps,
6
+ * `npm`/`api` providers, plugin tuples, ...). Everything OpenChamber WRITES is
7
+ * native v2. These conversions are pure so the web server and the VS Code
8
+ * extension host cannot drift: `packages/vscode/src/opencode-config-v2.ts`
9
+ * re-exports this module.
10
+ *
11
+ * Reference: OpenCode `packages/core/src/config/normalize.ts`,
12
+ * `packages/core/src/v1/config/migrate.ts`, and `services/www` migrate-v1 docs.
13
+ */
14
+
15
+ // ============== SECTIONS ==============
16
+
17
+ /**
18
+ * Config section keys per entity. `v2` is what OpenChamber writes; `v1` is the
19
+ * legacy key OpenCode still decodes and OpenChamber still reads.
20
+ */
21
+ const SECTIONS = {
22
+ agents: { v2: 'agents', v1: 'agent' },
23
+ commands: { v2: 'commands', v1: 'command' },
24
+ providers: { v2: 'providers', v1: 'provider' },
25
+ };
26
+
27
+ function isRecord(value) {
28
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+
31
+ function sectionKeys(kind) {
32
+ const section = SECTIONS[kind];
33
+ if (!section) throw new Error(`Unknown config section: ${kind}`);
34
+ return section;
35
+ }
36
+
37
+ /**
38
+ * Read one entry from a config object, preferring the v2 section key.
39
+ * Returns the section key the entry was found under so writers can rewrite the
40
+ * same file in place.
41
+ */
42
+ function readSectionEntry(config, kind, name) {
43
+ const { v2, v1 } = sectionKeys(kind);
44
+ if (isRecord(config?.[v2]) && config[v2][name] !== undefined) {
45
+ return { value: config[v2][name], key: v2, legacy: false };
46
+ }
47
+ if (isRecord(config?.[v1]) && config[v1][name] !== undefined) {
48
+ return { value: config[v1][name], key: v1, legacy: true };
49
+ }
50
+ return { value: undefined, key: null, legacy: false };
51
+ }
52
+
53
+ /**
54
+ * Write an entry in native v2 shape. When the name currently lives under the v1
55
+ * key in this same file, the legacy entry is dropped so the two spellings can
56
+ * never disagree; unrelated v1 siblings are left untouched.
57
+ */
58
+ function writeSectionEntry(config, kind, name, value) {
59
+ const { v2, v1 } = sectionKeys(kind);
60
+ if (!isRecord(config[v2])) config[v2] = {};
61
+ config[v2][name] = value;
62
+ if (isRecord(config[v1]) && config[v1][name] !== undefined) {
63
+ delete config[v1][name];
64
+ if (Object.keys(config[v1]).length === 0) delete config[v1];
65
+ }
66
+ return v2;
67
+ }
68
+
69
+ /** Remove an entry from both spellings. Returns true when anything was removed. */
70
+ function deleteSectionEntry(config, kind, name) {
71
+ const { v2, v1 } = sectionKeys(kind);
72
+ let removed = false;
73
+ for (const key of [v2, v1]) {
74
+ if (!isRecord(config?.[key]) || config[key][name] === undefined) continue;
75
+ delete config[key][name];
76
+ removed = true;
77
+ if (Object.keys(config[key]).length === 0) delete config[key];
78
+ }
79
+ return removed;
80
+ }
81
+
82
+ // ============== PERMISSIONS ==============
83
+
84
+ const PERMISSION_EFFECTS = new Set(['allow', 'deny', 'ask']);
85
+
86
+ /**
87
+ * v1 permission/tool keys renamed in v2. Mirrors
88
+ * `ConfigMigrateV1.normalizeAction`, so migrated rules keep matching.
89
+ */
90
+ function normalizePermissionAction(action) {
91
+ if (action === 'write' || action === 'patch') return 'edit';
92
+ if (action === 'task') return 'subagent';
93
+ if (action === 'bash') return 'shell';
94
+ return action;
95
+ }
96
+
97
+ function makeRule(action, resource, effect) {
98
+ return { action: String(action), resource: String(resource), effect };
99
+ }
100
+
101
+ /**
102
+ * Translate a v1 `permission` value (`"ask"`, `{edit: "allow"}`, or
103
+ * `{bash: {"git push *": "ask"}}`) into the ordered v2 rule array.
104
+ */
105
+ function permissionMapToRules(value) {
106
+ if (value === undefined || value === null) return [];
107
+ if (typeof value === 'string') {
108
+ return PERMISSION_EFFECTS.has(value) ? [makeRule('*', '*', value)] : [];
109
+ }
110
+ if (!isRecord(value)) return [];
111
+ const rules = [];
112
+ for (const [rawAction, raw] of Object.entries(value)) {
113
+ const action = normalizePermissionAction(rawAction);
114
+ if (typeof raw === 'string') {
115
+ if (PERMISSION_EFFECTS.has(raw)) rules.push(makeRule(action, '*', raw));
116
+ continue;
117
+ }
118
+ if (!isRecord(raw)) continue;
119
+ for (const [resource, effect] of Object.entries(raw)) {
120
+ if (PERMISSION_EFFECTS.has(effect)) rules.push(makeRule(action, resource, effect));
121
+ }
122
+ }
123
+ return rules;
124
+ }
125
+
126
+ /** v1 `tools: {websearch: false}` becomes deny/allow rules, as the normalizer does. */
127
+ function toolsToRules(value) {
128
+ if (!isRecord(value)) return [];
129
+ return Object.entries(value)
130
+ .filter(([, enabled]) => typeof enabled === 'boolean')
131
+ .map(([action, enabled]) => makeRule(normalizePermissionAction(action), '*', enabled ? 'allow' : 'deny'));
132
+ }
133
+
134
+ function isPermissionRule(value) {
135
+ return (
136
+ isRecord(value)
137
+ && typeof value.action === 'string'
138
+ && typeof value.resource === 'string'
139
+ && PERMISSION_EFFECTS.has(value.effect)
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Accept either shape and always answer the ordered v2 rule array: v2
145
+ * `permissions` arrays pass through, v1 `permission` maps/strings translate.
146
+ */
147
+ function normalizePermissionRules(value) {
148
+ if (Array.isArray(value)) return value.filter(isPermissionRule).map((rule) => makeRule(rule.action, rule.resource, rule.effect));
149
+ return permissionMapToRules(value);
150
+ }
151
+
152
+ /**
153
+ * Rules that apply to an agent, in evaluation order (global first, agent last;
154
+ * last match wins). `source` marks where each rule came from so an editor can
155
+ * show which are inherited.
156
+ */
157
+ function effectiveAgentRules(globalRules, agentRules) {
158
+ return [
159
+ ...normalizePermissionRules(globalRules).map((rule) => ({ ...rule, source: 'global' })),
160
+ ...normalizePermissionRules(agentRules).map((rule) => ({ ...rule, source: 'agent' })),
161
+ ];
162
+ }
163
+
164
+ /**
165
+ * Global permission rules of a config object: v1 `tools`, then v1 `permission`,
166
+ * then native `permissions` — the order `normalize.ts` concatenates them in.
167
+ */
168
+ function readGlobalPermissionRules(config) {
169
+ if (!isRecord(config)) return [];
170
+ return [
171
+ ...toolsToRules(config.tools),
172
+ ...permissionMapToRules(config.permission),
173
+ ...normalizePermissionRules(config.permissions),
174
+ ];
175
+ }
176
+
177
+ // ============== MODEL SELECTION ==============
178
+
179
+ /**
180
+ * Split `provider/model#variant` (or a v1 `{model, variant}` pair) into parts.
181
+ * Returns null when the reference is not a valid v2 selection, matching
182
+ * `Model.Ref.parse`.
183
+ */
184
+ function parseModelSelection(model, variant) {
185
+ if (isRecord(model)) {
186
+ return parseModelSelection(
187
+ typeof model.providerID === 'string' && typeof (model.modelID ?? model.model) === 'string'
188
+ ? `${model.providerID}/${model.modelID ?? model.model}`
189
+ : undefined,
190
+ model.variant,
191
+ );
192
+ }
193
+ if (typeof model !== 'string') return null;
194
+ const trimmed = model.trim();
195
+ const separator = trimmed.indexOf('/');
196
+ if (separator <= 0) return null;
197
+ const providerID = trimmed.slice(0, separator);
198
+ if (providerID.includes('#')) return null;
199
+ const hash = trimmed.indexOf('#', separator + 1);
200
+ const modelID = trimmed.slice(separator + 1, hash === -1 ? undefined : hash);
201
+ const embedded = hash === -1 ? undefined : trimmed.slice(hash + 1);
202
+ if (!modelID) return null;
203
+ const chosen = embedded !== undefined ? embedded : (typeof variant === 'string' ? variant.trim() : undefined);
204
+ if (chosen !== undefined && (!chosen || chosen.includes('#'))) return { providerID, modelID };
205
+ return chosen ? { providerID, modelID, variant: chosen } : { providerID, modelID };
206
+ }
207
+
208
+ /** Join a selection back into the v2 `provider/model#variant` string. */
209
+ function formatModelSelection(selection) {
210
+ if (typeof selection === 'string') {
211
+ const parsed = parseModelSelection(selection);
212
+ return parsed ? formatModelSelection(parsed) : null;
213
+ }
214
+ if (!isRecord(selection)) return null;
215
+ const providerID = typeof selection.providerID === 'string' ? selection.providerID.trim() : '';
216
+ const modelID = typeof (selection.modelID ?? selection.model) === 'string'
217
+ ? String(selection.modelID ?? selection.model).trim()
218
+ : '';
219
+ if (!providerID || !modelID) return null;
220
+ const variant = typeof selection.variant === 'string' ? selection.variant.trim() : '';
221
+ return variant ? `${providerID}/${modelID}#${variant}` : `${providerID}/${modelID}`;
222
+ }
223
+
224
+ // ============== AGENTS ==============
225
+
226
+ /**
227
+ * Frontmatter keys OpenCode 2 treats as native. Any other key in an agent file
228
+ * routes the WHOLE file through the v1 decoder (`config/plugin/agent.ts`), so a
229
+ * writer must never mix native and legacy keys in one file.
230
+ */
231
+ const AGENT_NATIVE_KEYS = new Set([
232
+ 'model',
233
+ 'request',
234
+ 'system',
235
+ 'description',
236
+ 'mode',
237
+ 'hidden',
238
+ 'color',
239
+ 'steps',
240
+ 'disabled',
241
+ 'permissions',
242
+ ]);
243
+
244
+ const AGENT_REQUEST_BODY_KEYS = ['temperature', 'top_p'];
245
+
246
+ const AGENT_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
247
+ const AGENT_COLOR_FALLBACK = '#aaaaaa';
248
+
249
+ /**
250
+ * v2 accepts only a six-digit hex color (`schema/src/config/agent.ts`). v1
251
+ * also allowed theme names (`primary`, ...); OpenCode's own migration
252
+ * (`core/src/v1/config/migrate.ts`) turns those into `#aaaaaa`, so a record
253
+ * OpenChamber rewrites stays decodable instead of being skipped.
254
+ */
255
+ function toAgentColor(value) {
256
+ if (typeof value !== 'string') return undefined;
257
+ const trimmed = value.trim();
258
+ if (!trimmed) return undefined;
259
+ return AGENT_COLOR_PATTERN.test(trimmed) ? trimmed : AGENT_COLOR_FALLBACK;
260
+ }
261
+
262
+ function pickDefined(entries) {
263
+ return Object.fromEntries(entries.filter(([, value]) => value !== undefined && value !== null));
264
+ }
265
+
266
+ function normalizeRequest(value, legacy) {
267
+ const headers = isRecord(value?.headers) ? { ...value.headers } : undefined;
268
+ const fromNative = isRecord(value?.body) ? { ...value.body } : {};
269
+ const fromLegacy = isRecord(legacy?.options) ? { ...legacy.options } : {};
270
+ for (const key of AGENT_REQUEST_BODY_KEYS) {
271
+ if (typeof legacy?.[key] === 'number') fromLegacy[key] = legacy[key];
272
+ }
273
+ const body = { ...fromLegacy, ...fromNative };
274
+ const request = pickDefined([
275
+ ['headers', headers && Object.keys(headers).length ? headers : undefined],
276
+ ['body', Object.keys(body).length ? body : undefined],
277
+ ]);
278
+ return Object.keys(request).length ? request : undefined;
279
+ }
280
+
281
+ /**
282
+ * Read an agent from either spelling into the canonical API shape.
283
+ * `body` is the markdown body for .md agents; it always wins over a frontmatter
284
+ * `system` key, because OpenCode overrides `system` with the body.
285
+ */
286
+ function toAgentEntity(raw, body) {
287
+ const source = isRecord(raw) ? raw : {};
288
+ const system = typeof body === 'string' && body.length > 0
289
+ ? body
290
+ : (typeof source.system === 'string' ? source.system : (typeof source.prompt === 'string' ? source.prompt : undefined));
291
+ const model = formatModelSelection(
292
+ isRecord(source.model) ? source.model : parseModelSelection(source.model, source.variant),
293
+ );
294
+ const steps = typeof source.steps === 'number'
295
+ ? source.steps
296
+ : (typeof source.maxSteps === 'number' ? source.maxSteps : undefined);
297
+ const disabled = typeof source.disabled === 'boolean'
298
+ ? source.disabled
299
+ : (typeof source.disable === 'boolean' ? source.disable : undefined);
300
+ const permissions = source.permissions !== undefined
301
+ ? normalizePermissionRules(source.permissions)
302
+ : permissionMapToRules(source.permission);
303
+ return pickDefined([
304
+ ['system', system],
305
+ ['description', typeof source.description === 'string' ? source.description : undefined],
306
+ ['model', model],
307
+ ['mode', ['primary', 'subagent', 'all'].includes(source.mode) ? source.mode : undefined],
308
+ ['hidden', typeof source.hidden === 'boolean' ? source.hidden : undefined],
309
+ ['color', toAgentColor(source.color)],
310
+ ['steps', steps],
311
+ ['disabled', disabled],
312
+ ['request', normalizeRequest(source.request, source)],
313
+ ['permissions', source.permissions !== undefined || source.permission !== undefined ? permissions : undefined],
314
+ ]);
315
+ }
316
+
317
+ /**
318
+ * Native v2 agent fields for persistence. `system` is returned separately
319
+ * because a markdown agent carries it as the body, never as frontmatter.
320
+ */
321
+ function fromAgentEntity(entity) {
322
+ const canonical = toAgentEntity(entity);
323
+ const { system, ...fields } = canonical;
324
+ return { fields, system: typeof system === 'string' ? system : '' };
325
+ }
326
+
327
+ /** True when frontmatter uses a v1-only agent key and OpenChamber must rewrite it. */
328
+ function isLegacyAgentFrontmatter(frontmatter) {
329
+ if (!isRecord(frontmatter)) return false;
330
+ return Object.keys(frontmatter).some((key) => key !== 'variant' && !AGENT_NATIVE_KEYS.has(key));
331
+ }
332
+
333
+ // ============== COMMANDS ==============
334
+
335
+ function toCommandEntity(raw, body) {
336
+ const source = isRecord(raw) ? raw : {};
337
+ const template = typeof body === 'string' && body.length > 0
338
+ ? body
339
+ : (typeof source.template === 'string' ? source.template : undefined);
340
+ const subagent = typeof source.subagent === 'boolean'
341
+ ? source.subagent
342
+ : (typeof source.subtask === 'boolean' ? source.subtask : undefined);
343
+ return pickDefined([
344
+ ['template', template],
345
+ ['description', typeof source.description === 'string' ? source.description : undefined],
346
+ ['agent', typeof source.agent === 'string' ? source.agent : undefined],
347
+ ['model', formatModelSelection(isRecord(source.model) ? source.model : parseModelSelection(source.model, source.variant))],
348
+ ['subagent', subagent],
349
+ ]);
350
+ }
351
+
352
+ function fromCommandEntity(entity) {
353
+ const canonical = toCommandEntity(entity);
354
+ const { template, ...fields } = canonical;
355
+ return { fields, template: typeof template === 'string' ? template : '' };
356
+ }
357
+
358
+ const COMMAND_NATIVE_KEYS = new Set(['template', 'description', 'agent', 'model', 'subagent']);
359
+
360
+ function isLegacyCommandFrontmatter(frontmatter) {
361
+ if (!isRecord(frontmatter)) return false;
362
+ return Object.keys(frontmatter).some((key) => !COMMAND_NATIVE_KEYS.has(key));
363
+ }
364
+
365
+ // ============== MCP ==============
366
+
367
+ function cleanStringMap(value) {
368
+ if (!isRecord(value)) return undefined;
369
+ const cleaned = {};
370
+ for (const [key, entry] of Object.entries(value)) {
371
+ if (key && entry !== undefined && entry !== null) cleaned[key] = String(entry);
372
+ }
373
+ return Object.keys(cleaned).length ? cleaned : undefined;
374
+ }
375
+
376
+ function positiveInt(value) {
377
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.trunc(value) : undefined;
378
+ }
379
+
380
+ /** v1 `timeout: 30000` becomes `{catalog, execution}`; a v2 object passes through. */
381
+ function toMcpTimeout(value) {
382
+ if (isRecord(value)) {
383
+ const timeout = pickDefined([
384
+ ['startup', positiveInt(value.startup)],
385
+ ['catalog', positiveInt(value.catalog)],
386
+ ['execution', positiveInt(value.execution)],
387
+ ]);
388
+ return Object.keys(timeout).length ? timeout : undefined;
389
+ }
390
+ const flat = positiveInt(value);
391
+ return flat === undefined ? undefined : { catalog: flat, execution: flat };
392
+ }
393
+
394
+ const MCP_PROTOCOLS = new Set(['legacy', 'auto', '2026-07-28']);
395
+
396
+ /** MCP protocol negotiation, added in OpenCode 2.0.8. Unknown values are dropped. */
397
+ function toMcpProtocol(value) {
398
+ return MCP_PROTOCOLS.has(value) ? value : undefined;
399
+ }
400
+
401
+ /** v1 OAuth camelCase becomes v2 snake_case. */
402
+ function toMcpOAuth(value) {
403
+ if (value === false) return false;
404
+ if (!isRecord(value)) return undefined;
405
+ const oauth = pickDefined([
406
+ ['client_id', trimmedString(value.client_id ?? value.clientId)],
407
+ ['client_secret', trimmedString(value.client_secret ?? value.clientSecret)],
408
+ ['scope', trimmedString(value.scope)],
409
+ ['callback_port', positiveInt(value.callback_port ?? value.callbackPort)],
410
+ ['redirect_uri', trimmedString(value.redirect_uri ?? value.redirectUri)],
411
+ // OpenCode 2.0.8 addition. OpenChamber has no UI for it; it is carried
412
+ // through so a hand-written value survives an edit made here.
413
+ ['auth_server_metadata_url', trimmedString(value.auth_server_metadata_url)],
414
+ ]);
415
+ return Object.keys(oauth).length ? oauth : undefined;
416
+ }
417
+
418
+ function trimmedString(value) {
419
+ if (typeof value !== 'string') return undefined;
420
+ const trimmed = value.trim();
421
+ return trimmed ? trimmed : undefined;
422
+ }
423
+
424
+ /**
425
+ * Canonical v2 MCP server entry. `type` is required (v2 drops enabled-only v1
426
+ * entries entirely), `enabled` inverts into `disabled`, timeouts split.
427
+ */
428
+ function toMcpEntity(raw) {
429
+ const source = isRecord(raw) ? raw : {};
430
+ const type = source.type === 'remote' ? 'remote' : 'local';
431
+ const disabled = typeof source.disabled === 'boolean'
432
+ ? source.disabled
433
+ : (typeof source.enabled === 'boolean' ? !source.enabled : undefined);
434
+ const shared = pickDefined([
435
+ ['disabled', disabled],
436
+ ['codemode', typeof source.codemode === 'boolean' ? source.codemode : undefined],
437
+ ['timeout', toMcpTimeout(source.timeout)],
438
+ // OpenCode 2.0.8 addition, on both local and remote entries. No OpenChamber
439
+ // UI for it yet; carried through so a hand-written value is not dropped.
440
+ ['protocol', toMcpProtocol(source.protocol)],
441
+ ]);
442
+ if (type === 'local') {
443
+ const command = Array.isArray(source.command) && source.command.length > 0
444
+ ? source.command.map(String)
445
+ : undefined;
446
+ return pickDefined([
447
+ ['type', 'local'],
448
+ ['command', command],
449
+ ['cwd', trimmedString(source.cwd)],
450
+ ['environment', cleanStringMap(source.environment)],
451
+ ...Object.entries(shared),
452
+ ]);
453
+ }
454
+ return pickDefined([
455
+ ['type', 'remote'],
456
+ ['url', trimmedString(source.url)],
457
+ ['headers', cleanStringMap(source.headers)],
458
+ ['oauth', toMcpOAuth(source.oauth)],
459
+ ...Object.entries(shared),
460
+ ]);
461
+ }
462
+
463
+ /**
464
+ * MCP servers live at `mcp.servers` in v2 and directly under `mcp` in v1.
465
+ * Anything without a `type` is not a server entry (`mcp.timeout`, and the
466
+ * enabled-only v1 entries v2 drops outright).
467
+ */
468
+ function isMcpServerValue(value) {
469
+ return isRecord(value) && (value.type === 'local' || value.type === 'remote');
470
+ }
471
+
472
+ function readMcpEntry(config, name) {
473
+ const mcp = isRecord(config?.mcp) ? config.mcp : null;
474
+ if (!mcp) return { value: undefined, key: null, legacy: false };
475
+ if (isRecord(mcp.servers) && mcp.servers[name] !== undefined) {
476
+ return { value: mcp.servers[name], key: 'mcp.servers', legacy: false };
477
+ }
478
+ if (name !== 'servers' && name !== 'timeout' && mcp[name] !== undefined) {
479
+ return { value: mcp[name], key: 'mcp', legacy: true };
480
+ }
481
+ return { value: undefined, key: null, legacy: false };
482
+ }
483
+
484
+ function readMcpEntries(config) {
485
+ const mcp = isRecord(config?.mcp) ? config.mcp : {};
486
+ const result = new Map();
487
+ for (const [name, value] of Object.entries(mcp)) {
488
+ if (name === 'servers' || name === 'timeout' || !isMcpServerValue(value)) continue;
489
+ result.set(name, { value, key: 'mcp', legacy: true });
490
+ }
491
+ if (isRecord(mcp.servers)) {
492
+ for (const [name, value] of Object.entries(mcp.servers)) {
493
+ result.set(name, { value, key: 'mcp.servers', legacy: false });
494
+ }
495
+ }
496
+ return result;
497
+ }
498
+
499
+ /**
500
+ * MCP entries across config layers, lowest precedence first. Each layer is
501
+ * normalized on its own before precedence applies: merging the raw objects
502
+ * first would let a v2 `mcp.servers.<name>` in the user file shadow a v1
503
+ * `mcp.<name>` override in the project file.
504
+ */
505
+ function readLayeredMcpEntries(configs) {
506
+ const result = new Map();
507
+ for (const config of configs) {
508
+ for (const [name, entry] of readMcpEntries(config)) result.set(name, entry);
509
+ }
510
+ return result;
511
+ }
512
+
513
+ function writeMcpEntry(config, name, value) {
514
+ if (!isRecord(config.mcp)) config.mcp = {};
515
+ if (!isRecord(config.mcp.servers)) config.mcp.servers = {};
516
+ config.mcp.servers[name] = value;
517
+ if (config.mcp[name] !== undefined && name !== 'servers' && name !== 'timeout') delete config.mcp[name];
518
+ return 'mcp.servers';
519
+ }
520
+
521
+ function deleteMcpEntry(config, name) {
522
+ const mcp = isRecord(config?.mcp) ? config.mcp : null;
523
+ if (!mcp) return false;
524
+ let removed = false;
525
+ if (isRecord(mcp.servers) && mcp.servers[name] !== undefined) {
526
+ delete mcp.servers[name];
527
+ removed = true;
528
+ if (Object.keys(mcp.servers).length === 0) delete mcp.servers;
529
+ }
530
+ if (name !== 'servers' && name !== 'timeout' && mcp[name] !== undefined) {
531
+ delete mcp[name];
532
+ removed = true;
533
+ }
534
+ if (Object.keys(mcp).length === 0) delete config.mcp;
535
+ return removed;
536
+ }
537
+
538
+ // ============== PROVIDERS ==============
539
+
540
+ const AISDK_PREFIX = 'aisdk:';
541
+
542
+ /** v1 `npm: "@ai-sdk/x"` becomes v2 `package: "aisdk:@ai-sdk/x"`. */
543
+ function toProviderPackage(value) {
544
+ const trimmed = trimmedString(value);
545
+ if (!trimmed) return undefined;
546
+ return trimmed.includes(':') ? trimmed : `${AISDK_PREFIX}${trimmed}`;
547
+ }
548
+
549
+ /** Strip the `aisdk:` prefix for editors that speak npm package names. */
550
+ function toNpmPackage(value) {
551
+ const trimmed = trimmedString(value);
552
+ if (!trimmed) return undefined;
553
+ return trimmed.startsWith(AISDK_PREFIX) ? trimmed.slice(AISDK_PREFIX.length) : trimmed;
554
+ }
555
+
556
+ function toModelVariants(value) {
557
+ if (Array.isArray(value)) {
558
+ return value
559
+ .filter((entry) => isRecord(entry) && trimmedString(entry.id))
560
+ .map((entry) => pickDefined([
561
+ ['id', trimmedString(entry.id)],
562
+ ['settings', isRecord(entry.settings) ? { ...entry.settings } : undefined],
563
+ ['headers', cleanStringMap(entry.headers)],
564
+ ['body', isRecord(entry.body) ? { ...entry.body } : undefined],
565
+ ]));
566
+ }
567
+ if (!isRecord(value)) return undefined;
568
+ return Object.entries(value).map(([id, settings]) => pickDefined([
569
+ ['id', id],
570
+ ['settings', isRecord(settings) ? { ...settings } : undefined],
571
+ ]));
572
+ }
573
+
574
+ function toModelCapabilities(source) {
575
+ const modalities = isRecord(source.modalities) ? source.modalities : {};
576
+ if (isRecord(source.capabilities)) {
577
+ return pickDefined([
578
+ ['tools', typeof source.capabilities.tools === 'boolean' ? source.capabilities.tools : true],
579
+ ['input', Array.isArray(source.capabilities.input) ? source.capabilities.input.map(String) : ['text', 'image']],
580
+ ['output', Array.isArray(source.capabilities.output) ? source.capabilities.output.map(String) : ['text']],
581
+ ]);
582
+ }
583
+ if (typeof source.tool_call !== 'boolean' && !Array.isArray(modalities.input) && !Array.isArray(modalities.output)) {
584
+ return undefined;
585
+ }
586
+ return {
587
+ tools: typeof source.tool_call === 'boolean' ? source.tool_call : true,
588
+ input: Array.isArray(modalities.input) ? modalities.input.map(String) : ['text', 'image'],
589
+ output: Array.isArray(modalities.output) ? modalities.output.map(String) : ['text'],
590
+ };
591
+ }
592
+
593
+ function toModelCost(value) {
594
+ if (Array.isArray(value)) return value;
595
+ if (!isRecord(value)) return undefined;
596
+ const nativeCache = isRecord(value.cache) ? value.cache : {};
597
+ const cache = pickDefined([
598
+ ['read', typeof nativeCache.read === 'number' ? nativeCache.read : value.cache_read],
599
+ ['write', typeof nativeCache.write === 'number' ? nativeCache.write : value.cache_write],
600
+ ]);
601
+ return pickDefined([
602
+ ['tier', isRecord(value.tier) ? { ...value.tier } : undefined],
603
+ ['input', typeof value.input === 'number' ? value.input : undefined],
604
+ ['output', typeof value.output === 'number' ? value.output : undefined],
605
+ ['cache', Object.keys(cache).length ? cache : undefined],
606
+ ]);
607
+ }
608
+
609
+ /**
610
+ * v2 `compatibility` passes through; v1 `interleaved` migrates the way
611
+ * OpenCode does (`core/src/model.ts` `compatibility()`): a string or
612
+ * `{field}` names the reasoning field, a boolean carries nothing.
613
+ */
614
+ function toModelCompatibility(source) {
615
+ if (isRecord(source.compatibility)) {
616
+ return Object.keys(source.compatibility).length ? { ...source.compatibility } : undefined;
617
+ }
618
+ const interleaved = source.interleaved;
619
+ if (typeof interleaved === 'string') return interleaved ? { reasoningField: interleaved } : undefined;
620
+ if (isRecord(interleaved) && typeof interleaved.field === 'string') return { reasoningField: interleaved.field };
621
+ return undefined;
622
+ }
623
+
624
+ /** Canonical v2 model entry: `modelID`, `capabilities`, `cache.read/write`, variants array. */
625
+ function toProviderModelEntity(raw) {
626
+ const source = isRecord(raw) ? raw : {};
627
+ const legacyOptions = isRecord(source.options) ? { ...source.options } : undefined;
628
+ const settings = isRecord(source.settings) ? { ...source.settings } : legacyOptions;
629
+ return pickDefined([
630
+ ['modelID', trimmedString(source.modelID ?? source.id)],
631
+ ['name', trimmedString(source.name)],
632
+ ['family', trimmedString(source.family)],
633
+ ['compatibility', toModelCompatibility(source)],
634
+ ['package', toProviderPackage(source.package ?? source.provider?.npm)],
635
+ ['settings', source.provider?.api ? { ...(settings ?? {}), baseURL: source.provider.api } : settings],
636
+ ['headers', cleanStringMap(source.headers)],
637
+ ['body', isRecord(source.body) ? { ...source.body } : undefined],
638
+ ['capabilities', toModelCapabilities(source)],
639
+ ['variants', toModelVariants(source.variants)],
640
+ ['cost', toModelCost(source.cost)],
641
+ ['limit', isRecord(source.limit) ? { ...source.limit } : undefined],
642
+ ['disabled', typeof source.disabled === 'boolean' ? source.disabled : (source.status === 'deprecated' ? true : undefined)],
643
+ ]);
644
+ }
645
+
646
+ /** Canonical v2 provider entry: `package` with `aisdk:`, `settings.baseURL`, split overlays. */
647
+ function toProviderEntity(raw) {
648
+ const source = isRecord(raw) ? raw : {};
649
+ const legacyOptions = isRecord(source.options) ? { ...source.options } : {};
650
+ const { headers: legacyHeaders, body: legacyBody, ...legacySettings } = legacyOptions;
651
+ const settings = { ...legacySettings };
652
+ if (isRecord(source.settings)) Object.assign(settings, source.settings);
653
+ const baseURL = trimmedString(source.settings?.baseURL) ?? trimmedString(source.api) ?? trimmedString(legacySettings.baseURL);
654
+ if (baseURL) settings.baseURL = baseURL;
655
+ const models = isRecord(source.models)
656
+ ? Object.fromEntries(Object.entries(source.models).map(([id, model]) => [id, toProviderModelEntity(model)]))
657
+ : undefined;
658
+ return pickDefined([
659
+ // v2-only: the built-in provider this entry inherits models/defaults from.
660
+ ['canonical', trimmedString(source.canonical)],
661
+ ['name', trimmedString(source.name)],
662
+ ['package', toProviderPackage(source.package ?? source.npm)],
663
+ ['env', Array.isArray(source.env) ? source.env.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim()) : undefined],
664
+ ['settings', Object.keys(settings).length ? settings : undefined],
665
+ ['headers', cleanStringMap(source.headers ?? legacyHeaders)],
666
+ ['body', isRecord(source.body ?? legacyBody) ? { ...(source.body ?? legacyBody) } : undefined],
667
+ ['models', models],
668
+ ]);
669
+ }
670
+
671
+ // ============== PLUGINS ==============
672
+
673
+ const PLUGIN_SECTION = { v2: 'plugins', v1: 'plugin' };
674
+
675
+ /** Accept a v1 string/tuple or a v2 `{package, options}` object. */
676
+ function toPluginEntity(raw) {
677
+ if (typeof raw === 'string') {
678
+ const spec = raw.trim();
679
+ return spec ? { package: spec } : null;
680
+ }
681
+ if (Array.isArray(raw)) {
682
+ const spec = typeof raw[0] === 'string' ? raw[0].trim() : '';
683
+ if (!spec) return null;
684
+ return isRecord(raw[1]) && Object.keys(raw[1]).length ? { package: spec, options: { ...raw[1] } } : { package: spec };
685
+ }
686
+ if (isRecord(raw)) {
687
+ const spec = trimmedString(raw.package ?? raw.spec);
688
+ if (!spec) return null;
689
+ return isRecord(raw.options) && Object.keys(raw.options).length
690
+ ? { package: spec, options: { ...raw.options } }
691
+ : { package: spec };
692
+ }
693
+ return null;
694
+ }
695
+
696
+ /**
697
+ * v2 serialization: a bare string when there are no options, otherwise
698
+ * `{package, options}`. Never a tuple.
699
+ */
700
+ function fromPluginEntity(entity) {
701
+ const normalized = toPluginEntity(entity);
702
+ if (!normalized) return null;
703
+ return normalized.options ? { package: normalized.package, options: normalized.options } : normalized.package;
704
+ }
705
+
706
+ /** Plugin list of a config object, v1 entries first (the normalizer's order). */
707
+ function readPluginList(config) {
708
+ const legacy = Array.isArray(config?.[PLUGIN_SECTION.v1]) ? config[PLUGIN_SECTION.v1] : [];
709
+ const native = Array.isArray(config?.[PLUGIN_SECTION.v2]) ? config[PLUGIN_SECTION.v2] : [];
710
+ return [
711
+ ...legacy.map((raw) => ({ entry: toPluginEntity(raw), key: PLUGIN_SECTION.v1, legacy: true })),
712
+ ...native.map((raw) => ({ entry: toPluginEntity(raw), key: PLUGIN_SECTION.v2, legacy: false })),
713
+ ].filter((item) => item.entry !== null);
714
+ }
715
+
716
+ // ============== WEB SEARCH ==============
717
+
718
+ /**
719
+ * The `websearch` key: `false` turns search off, `{ provider }` names a
720
+ * provider id or `"random"`. OpenChamber takes the choice flat (`false`, a
721
+ * string, or `null` to remove the key so OpenCode falls back to the answer
722
+ * given in chat, asking only when there is none).
723
+ * Returns `undefined` for anything else.
724
+ */
725
+ function parseWebSearchSelection(value) {
726
+ if (value === null || value === false) return value;
727
+ if (typeof value !== 'string') return undefined;
728
+ const trimmed = value.trim();
729
+ return trimmed ? trimmed : undefined;
730
+ }
731
+
732
+ /** Applies a parsed choice to a config object; returns whether it changed. */
733
+ function writeWebSearchSelection(config, selection) {
734
+ const before = JSON.stringify(config.websearch);
735
+ if (selection === null) {
736
+ delete config.websearch;
737
+ } else if (selection === false) {
738
+ config.websearch = false;
739
+ } else {
740
+ config.websearch = { provider: selection };
741
+ }
742
+ return JSON.stringify(config.websearch) !== before;
743
+ }
744
+
745
+ const hasWebSearchKey = (config) => config != null && Object.hasOwn(config, 'websearch');
746
+
747
+ /**
748
+ * The project config path whose `websearch` wins over the file the Settings
749
+ * choice is written to, or `null`. OpenCode merges user < project files (every
750
+ * `opencode.json[c]` and `.opencode/opencode.json[c]` from the directory up to
751
+ * the project root) < `OPENCODE_CONFIG`, so any project file with the key
752
+ * decides unless `OPENCODE_CONFIG` sets it too. `layers` is what
753
+ * `readConfigLayers(directory)` returns; `projectFiles` lists the existing
754
+ * project config files deepest first, as `{ path, config }`.
755
+ */
756
+ function findWebSearchProjectOverride(layers, projectFiles) {
757
+ if (hasWebSearchKey(layers?.customConfig)) return null;
758
+ const userPath = layers?.paths?.userPath ?? null;
759
+ for (const file of projectFiles ?? []) {
760
+ if (!file?.path || file.path === userPath) continue;
761
+ if (hasWebSearchKey(file.config)) return file.path;
762
+ }
763
+ return null;
764
+ }
765
+
766
+ export {
767
+ PLUGIN_SECTION,
768
+ isRecord,
769
+ readSectionEntry,
770
+ writeSectionEntry,
771
+ deleteSectionEntry,
772
+ normalizePermissionAction,
773
+ permissionMapToRules,
774
+ normalizePermissionRules,
775
+ effectiveAgentRules,
776
+ readGlobalPermissionRules,
777
+ parseModelSelection,
778
+ formatModelSelection,
779
+ toAgentEntity,
780
+ fromAgentEntity,
781
+ isLegacyAgentFrontmatter,
782
+ toCommandEntity,
783
+ fromCommandEntity,
784
+ isLegacyCommandFrontmatter,
785
+ toMcpEntity,
786
+ readMcpEntry,
787
+ readMcpEntries,
788
+ readLayeredMcpEntries,
789
+ writeMcpEntry,
790
+ deleteMcpEntry,
791
+ toModelVariants,
792
+ toProviderPackage,
793
+ toNpmPackage,
794
+ toProviderEntity,
795
+ toPluginEntity,
796
+ fromPluginEntity,
797
+ readPluginList,
798
+ parseWebSearchSelection,
799
+ writeWebSearchSelection,
800
+ findWebSearchProjectOverride,
801
+ };