@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,466 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ permissionMapToRules,
5
+ normalizePermissionRules,
6
+ normalizePermissionAction,
7
+ effectiveAgentRules,
8
+ readGlobalPermissionRules,
9
+ parseModelSelection,
10
+ formatModelSelection,
11
+ toAgentEntity,
12
+ fromAgentEntity,
13
+ isLegacyAgentFrontmatter,
14
+ toCommandEntity,
15
+ toMcpEntity,
16
+ toProviderEntity,
17
+ toPluginEntity,
18
+ fromPluginEntity,
19
+ readSectionEntry,
20
+ writeSectionEntry,
21
+ deleteSectionEntry,
22
+ readMcpEntries,
23
+ readLayeredMcpEntries,
24
+ writeMcpEntry,
25
+ parseWebSearchSelection,
26
+ writeWebSearchSelection,
27
+ findWebSearchProjectOverride,
28
+ } from './config-v2.js';
29
+
30
+ describe('permission translation', () => {
31
+ it('renames the v1 actions v2 renamed', () => {
32
+ expect(normalizePermissionAction('bash')).toBe('shell');
33
+ expect(normalizePermissionAction('task')).toBe('subagent');
34
+ expect(normalizePermissionAction('write')).toBe('edit');
35
+ expect(normalizePermissionAction('patch')).toBe('edit');
36
+ expect(normalizePermissionAction('read')).toBe('read');
37
+ });
38
+
39
+ it('turns a v1 permission map into an ordered rule array', () => {
40
+ expect(permissionMapToRules({
41
+ bash: { 'git push *': 'ask' },
42
+ edit: 'allow',
43
+ task: 'deny',
44
+ })).toEqual([
45
+ { action: 'shell', resource: 'git push *', effect: 'ask' },
46
+ { action: 'edit', resource: '*', effect: 'allow' },
47
+ { action: 'subagent', resource: '*', effect: 'deny' },
48
+ ]);
49
+ });
50
+
51
+ it('turns a bare v1 permission string into a catch-all rule', () => {
52
+ expect(permissionMapToRules('ask')).toEqual([{ action: '*', resource: '*', effect: 'ask' }]);
53
+ expect(permissionMapToRules('nonsense')).toEqual([]);
54
+ });
55
+
56
+ it('passes a v2 rule array through and drops malformed rules', () => {
57
+ expect(normalizePermissionRules([
58
+ { action: 'shell', resource: '*', effect: 'allow' },
59
+ { action: 'shell', resource: '*', effect: 'maybe' },
60
+ 'nope',
61
+ ])).toEqual([{ action: 'shell', resource: '*', effect: 'allow' }]);
62
+ });
63
+
64
+ it('reads global rules from tools, permission and permissions in normalizer order', () => {
65
+ expect(readGlobalPermissionRules({
66
+ tools: { websearch: false },
67
+ permission: { bash: 'ask' },
68
+ permissions: [{ action: 'read', resource: '*', effect: 'allow' }],
69
+ })).toEqual([
70
+ { action: 'websearch', resource: '*', effect: 'deny' },
71
+ { action: 'shell', resource: '*', effect: 'ask' },
72
+ { action: 'read', resource: '*', effect: 'allow' },
73
+ ]);
74
+ });
75
+
76
+ it('puts global rules before agent rules so the agent wins on a tie', () => {
77
+ expect(effectiveAgentRules(
78
+ [{ action: 'edit', resource: '*', effect: 'allow' }],
79
+ { edit: 'deny' },
80
+ )).toEqual([
81
+ { action: 'edit', resource: '*', effect: 'allow', source: 'global' },
82
+ { action: 'edit', resource: '*', effect: 'deny', source: 'agent' },
83
+ ]);
84
+ });
85
+ });
86
+
87
+ describe('model selection', () => {
88
+ it('splits provider/model#variant', () => {
89
+ expect(parseModelSelection('anthropic/claude-sonnet-4-5#high')).toEqual({
90
+ providerID: 'anthropic',
91
+ modelID: 'claude-sonnet-4-5',
92
+ variant: 'high',
93
+ });
94
+ });
95
+
96
+ it('joins a separate v1 variant onto the model reference', () => {
97
+ expect(formatModelSelection(parseModelSelection('anthropic/claude-sonnet-4-5', 'high')))
98
+ .toBe('anthropic/claude-sonnet-4-5#high');
99
+ });
100
+
101
+ it('rejects references v2 would not parse', () => {
102
+ expect(parseModelSelection('claude-sonnet')).toBeNull();
103
+ expect(parseModelSelection('/model')).toBeNull();
104
+ expect(parseModelSelection('anthropic/')).toBeNull();
105
+ });
106
+ });
107
+
108
+ describe('agent entity', () => {
109
+ it('reads a v1 agent into the v2 shape', () => {
110
+ expect(toAgentEntity({
111
+ prompt: 'Review carefully.',
112
+ model: 'anthropic/claude-sonnet-4-5',
113
+ variant: 'high',
114
+ temperature: 0.7,
115
+ top_p: 0.9,
116
+ maxSteps: 12,
117
+ disable: false,
118
+ permission: { bash: 'ask', edit: 'deny' },
119
+ })).toEqual({
120
+ system: 'Review carefully.',
121
+ model: 'anthropic/claude-sonnet-4-5#high',
122
+ steps: 12,
123
+ disabled: false,
124
+ request: { body: { temperature: 0.7, top_p: 0.9 } },
125
+ permissions: [
126
+ { action: 'shell', resource: '*', effect: 'ask' },
127
+ { action: 'edit', resource: '*', effect: 'deny' },
128
+ ],
129
+ });
130
+ });
131
+
132
+ it('reads a native v2 agent unchanged', () => {
133
+ const native = {
134
+ system: 'Be brief.',
135
+ model: 'openai/gpt-5#low',
136
+ mode: 'subagent',
137
+ hidden: true,
138
+ color: '#aabbcc',
139
+ steps: 4,
140
+ permissions: [{ action: 'read', resource: '*', effect: 'allow' }],
141
+ };
142
+ expect(toAgentEntity(native)).toEqual(native);
143
+ });
144
+
145
+ it('maps a v1 theme color onto the hex OpenCode migrates it to', () => {
146
+ // v2 only decodes `#rrggbb`; OpenCode's migration turns anything else into #aaaaaa.
147
+ expect(toAgentEntity({ color: 'primary' }).color).toBe('#aaaaaa');
148
+ expect(toAgentEntity({ color: '#abc' }).color).toBe('#aaaaaa');
149
+ expect(toAgentEntity({ color: '#AABBCC' }).color).toBe('#AABBCC');
150
+ expect(toAgentEntity({ color: '' }).color).toBeUndefined();
151
+ expect(fromAgentEntity({ color: 'accent', description: 'A' }).fields).toEqual({
152
+ color: '#aaaaaa',
153
+ description: 'A',
154
+ });
155
+ });
156
+
157
+ it('lets the markdown body win over a frontmatter system field', () => {
158
+ expect(toAgentEntity({ system: 'from frontmatter' }, 'from body').system).toBe('from body');
159
+ });
160
+
161
+ it('splits the system prompt out for markdown persistence', () => {
162
+ expect(fromAgentEntity({ system: 'Body text.', description: 'Agent' })).toEqual({
163
+ fields: { description: 'Agent' },
164
+ system: 'Body text.',
165
+ });
166
+ });
167
+
168
+ it('flags frontmatter that OpenCode would route through its v1 decoder', () => {
169
+ expect(isLegacyAgentFrontmatter({ model: 'a/b', permissions: [] })).toBe(false);
170
+ expect(isLegacyAgentFrontmatter({ model: 'a/b', variant: 'high' })).toBe(false);
171
+ expect(isLegacyAgentFrontmatter({ temperature: 0.5 })).toBe(true);
172
+ expect(isLegacyAgentFrontmatter({ permission: { bash: 'ask' } })).toBe(true);
173
+ });
174
+ });
175
+
176
+ describe('command entity', () => {
177
+ it('renames subtask to subagent and joins the variant', () => {
178
+ expect(toCommandEntity({
179
+ template: 'Review the current changes.',
180
+ model: 'anthropic/claude-sonnet-4-5',
181
+ variant: 'high',
182
+ subtask: true,
183
+ })).toEqual({
184
+ template: 'Review the current changes.',
185
+ model: 'anthropic/claude-sonnet-4-5#high',
186
+ subagent: true,
187
+ });
188
+ });
189
+ });
190
+
191
+ describe('mcp entity', () => {
192
+ it('migrates a v1 local server', () => {
193
+ expect(toMcpEntity({
194
+ type: 'local',
195
+ command: ['npx', '@playwright/mcp'],
196
+ enabled: true,
197
+ timeout: 30000,
198
+ })).toEqual({
199
+ type: 'local',
200
+ command: ['npx', '@playwright/mcp'],
201
+ disabled: false,
202
+ timeout: { catalog: 30000, execution: 30000 },
203
+ });
204
+ });
205
+
206
+ it('migrates v1 camelCase OAuth to snake_case', () => {
207
+ expect(toMcpEntity({
208
+ type: 'remote',
209
+ url: 'https://mcp.example.com',
210
+ oauth: { clientId: 'id', clientSecret: 'secret', callbackPort: 4242, redirectUri: 'http://localhost/cb' },
211
+ enabled: false,
212
+ })).toEqual({
213
+ type: 'remote',
214
+ url: 'https://mcp.example.com',
215
+ oauth: {
216
+ client_id: 'id',
217
+ client_secret: 'secret',
218
+ callback_port: 4242,
219
+ redirect_uri: 'http://localhost/cb',
220
+ },
221
+ disabled: true,
222
+ });
223
+ });
224
+
225
+ it('keeps a non-default protocol on both transports', () => {
226
+ expect(toMcpEntity({ type: 'local', command: ['a'], protocol: 'auto' }).protocol).toBe('auto');
227
+ expect(toMcpEntity({
228
+ type: 'remote',
229
+ url: 'https://mcp.example.com',
230
+ protocol: '2026-07-28',
231
+ }).protocol).toBe('2026-07-28');
232
+ });
233
+
234
+ it('drops the protocol key for legacy, for an unknown value and for a removal', () => {
235
+ // The editor saves "legacy" as `null`: absent is what OpenCode calls legacy,
236
+ // so the config file only ever names a non-default negotiation.
237
+ expect(toMcpEntity({ type: 'local', command: ['a'], protocol: null })).not.toHaveProperty('protocol');
238
+ expect(toMcpEntity({ type: 'local', command: ['a'], protocol: 'made-up' })).not.toHaveProperty('protocol');
239
+ expect(toMcpEntity({ type: 'local', command: ['a'], protocol: 'legacy' }).protocol).toBe('legacy');
240
+ });
241
+
242
+ it('keeps auth_server_metadata_url and drops it again when emptied', () => {
243
+ expect(toMcpEntity({
244
+ type: 'remote',
245
+ url: 'https://mcp.example.com',
246
+ oauth: {
247
+ client_id: 'id',
248
+ auth_server_metadata_url: 'https://auth.example.com/.well-known/oauth-authorization-server',
249
+ },
250
+ }).oauth).toEqual({
251
+ client_id: 'id',
252
+ auth_server_metadata_url: 'https://auth.example.com/.well-known/oauth-authorization-server',
253
+ });
254
+
255
+ // An emptied field arrives as '' because the editor rebuilds the whole
256
+ // oauth block; the key must not survive.
257
+ expect(toMcpEntity({
258
+ type: 'remote',
259
+ url: 'https://mcp.example.com',
260
+ oauth: { client_id: 'id', auth_server_metadata_url: ' ' },
261
+ }).oauth).toEqual({ client_id: 'id' });
262
+
263
+ // Nothing left in the block at all removes `oauth` itself.
264
+ expect(toMcpEntity({
265
+ type: 'remote',
266
+ url: 'https://mcp.example.com',
267
+ oauth: { auth_server_metadata_url: '' },
268
+ })).not.toHaveProperty('oauth');
269
+ });
270
+
271
+ it('reads v1 mcp.<name> and v2 mcp.servers, with v2 winning', () => {
272
+ const config = {
273
+ mcp: {
274
+ timeout: { catalog: 1000 },
275
+ legacy: { type: 'local', command: ['a'] },
276
+ shared: { type: 'local', command: ['old'] },
277
+ servers: { shared: { type: 'local', command: ['new'] } },
278
+ },
279
+ };
280
+ const entries = readMcpEntries(config);
281
+ expect([...entries.keys()]).toEqual(['legacy', 'shared']);
282
+ expect(entries.get('shared').value.command).toEqual(['new']);
283
+ });
284
+
285
+ it('applies layer precedence after normalizing each layer', () => {
286
+ // A raw deep merge would leave user `mcp.servers.shared` next to project
287
+ // `mcp.shared`, and the v2 spelling would then shadow the project override.
288
+ const user = { mcp: { servers: { shared: { type: 'remote', url: 'https://global' }, only: { type: 'local', command: ['u'] } } } };
289
+ const project = { mcp: { shared: { type: 'remote', url: 'https://project' } } };
290
+ const entries = readLayeredMcpEntries([user, project]);
291
+ expect(entries.get('shared')).toEqual({
292
+ value: { type: 'remote', url: 'https://project' },
293
+ key: 'mcp',
294
+ legacy: true,
295
+ });
296
+ expect(entries.get('only').value.command).toEqual(['u']);
297
+ expect(readLayeredMcpEntries([project, user]).get('shared').value.url).toBe('https://global');
298
+ });
299
+
300
+ it('rewrites a v1 mcp entry into mcp.servers in place', () => {
301
+ const config = { mcp: { legacy: { type: 'local', command: ['a'] } } };
302
+ writeMcpEntry(config, 'legacy', { type: 'local', command: ['b'] });
303
+ expect(config.mcp).toEqual({ servers: { legacy: { type: 'local', command: ['b'] } } });
304
+ });
305
+ });
306
+
307
+ describe('provider entity', () => {
308
+ it('migrates npm/api/options into package/settings', () => {
309
+ expect(toProviderEntity({
310
+ npm: '@ai-sdk/openai-compatible',
311
+ api: 'https://llm.example.com/v1',
312
+ options: { apiKey: '{env:ACME_API_KEY}', headers: { 'X-A': '1' } },
313
+ })).toEqual({
314
+ package: 'aisdk:@ai-sdk/openai-compatible',
315
+ settings: { apiKey: '{env:ACME_API_KEY}', baseURL: 'https://llm.example.com/v1' },
316
+ headers: { 'X-A': '1' },
317
+ });
318
+ });
319
+
320
+ it('keeps the v2-only canonical and compatibility fields', () => {
321
+ const native = {
322
+ canonical: 'openai',
323
+ name: 'Proxy',
324
+ package: 'aisdk:@ai-sdk/openai-compatible',
325
+ settings: { baseURL: 'https://proxy.example.com/v1' },
326
+ models: {
327
+ m: {
328
+ modelID: 'm',
329
+ name: 'M',
330
+ compatibility: { reasoningField: 'reasoning_content', requireReasoning: true, maxTokensField: 'max_tokens' },
331
+ },
332
+ },
333
+ };
334
+ expect(toProviderEntity(native)).toEqual(native);
335
+ expect(toProviderEntity({ models: { m: { compatibility: {} } } }).models.m).toEqual({});
336
+ });
337
+
338
+ it('migrates v1 interleaved into compatibility.reasoningField', () => {
339
+ const models = toProviderEntity({
340
+ models: {
341
+ text: { interleaved: 'reasoning_text' },
342
+ field: { interleaved: { field: 'reasoning' } },
343
+ flag: { interleaved: true },
344
+ },
345
+ }).models;
346
+ expect(models.text).toEqual({ compatibility: { reasoningField: 'reasoning_text' } });
347
+ expect(models.field).toEqual({ compatibility: { reasoningField: 'reasoning' } });
348
+ expect(models.flag).toEqual({});
349
+ });
350
+
351
+ it('migrates model fields', () => {
352
+ expect(toProviderEntity({
353
+ models: {
354
+ m: {
355
+ id: 'real-model-id',
356
+ name: 'M',
357
+ tool_call: false,
358
+ modalities: { input: ['text'], output: ['text'] },
359
+ status: 'deprecated',
360
+ cost: { input: 1, output: 2, cache_read: 0.1, cache_write: 0.2 },
361
+ variants: { high: { reasoningEffort: 'high' } },
362
+ },
363
+ },
364
+ }).models.m).toEqual({
365
+ modelID: 'real-model-id',
366
+ name: 'M',
367
+ capabilities: { tools: false, input: ['text'], output: ['text'] },
368
+ variants: [{ id: 'high', settings: { reasoningEffort: 'high' } }],
369
+ cost: { input: 1, output: 2, cache: { read: 0.1, write: 0.2 } },
370
+ disabled: true,
371
+ });
372
+ });
373
+ });
374
+
375
+ describe('plugin entity', () => {
376
+ it('accepts v1 strings and tuples and v2 objects', () => {
377
+ expect(toPluginEntity('pkg')).toEqual({ package: 'pkg' });
378
+ expect(toPluginEntity(['pkg', { a: 1 }])).toEqual({ package: 'pkg', options: { a: 1 } });
379
+ expect(toPluginEntity({ package: 'pkg', options: { a: 1 } })).toEqual({ package: 'pkg', options: { a: 1 } });
380
+ expect(toPluginEntity(42)).toBeNull();
381
+ });
382
+
383
+ it('serializes to a bare string or a v2 object, never a tuple', () => {
384
+ expect(fromPluginEntity(['pkg', {}])).toBe('pkg');
385
+ expect(fromPluginEntity(['pkg', { a: 1 }])).toEqual({ package: 'pkg', options: { a: 1 } });
386
+ });
387
+ });
388
+
389
+ describe('config sections', () => {
390
+ it('prefers the v2 key and reports where an entry was found', () => {
391
+ expect(readSectionEntry({ agent: { a: { prompt: 'x' } } }, 'agents', 'a'))
392
+ .toEqual({ value: { prompt: 'x' }, key: 'agent', legacy: true });
393
+ expect(readSectionEntry({ agent: { a: 1 }, agents: { a: 2 } }, 'agents', 'a'))
394
+ .toEqual({ value: 2, key: 'agents', legacy: false });
395
+ });
396
+
397
+ it('rewrites a v1 entry into the v2 key and leaves siblings alone', () => {
398
+ const config = { agent: { a: { prompt: 'x' }, b: { prompt: 'y' } } };
399
+ writeSectionEntry(config, 'agents', 'a', { system: 'x' });
400
+ expect(config).toEqual({ agent: { b: { prompt: 'y' } }, agents: { a: { system: 'x' } } });
401
+ });
402
+
403
+ it('drops the v1 map entirely when its last entry moves', () => {
404
+ const config = { agent: { a: { prompt: 'x' } } };
405
+ writeSectionEntry(config, 'agents', 'a', { system: 'x' });
406
+ expect(config).toEqual({ agents: { a: { system: 'x' } } });
407
+ });
408
+
409
+ it('deletes an entry under either spelling', () => {
410
+ const config = { agent: { a: 1 }, agents: { a: 2 } };
411
+ expect(deleteSectionEntry(config, 'agents', 'a')).toBe(true);
412
+ expect(config).toEqual({});
413
+ });
414
+ });
415
+
416
+ describe('web search choice', () => {
417
+ it('accepts off, remove, random and a provider id, and nothing else', () => {
418
+ expect(parseWebSearchSelection(false)).toBe(false);
419
+ expect(parseWebSearchSelection(null)).toBe(null);
420
+ expect(parseWebSearchSelection('random')).toBe('random');
421
+ expect(parseWebSearchSelection(' exa ')).toBe('exa');
422
+ expect(parseWebSearchSelection('')).toBeUndefined();
423
+ expect(parseWebSearchSelection(true)).toBeUndefined();
424
+ expect(parseWebSearchSelection(undefined)).toBeUndefined();
425
+ expect(parseWebSearchSelection({ provider: 'exa' })).toBeUndefined();
426
+ });
427
+
428
+ it('writes the OpenCode shape and leaves other keys alone', () => {
429
+ const config = { model: 'openai/gpt-5' };
430
+ expect(writeWebSearchSelection(config, 'exa')).toBe(true);
431
+ expect(config).toEqual({ model: 'openai/gpt-5', websearch: { provider: 'exa' } });
432
+ expect(writeWebSearchSelection(config, 'exa')).toBe(false);
433
+ expect(writeWebSearchSelection(config, false)).toBe(true);
434
+ expect(config.websearch).toBe(false);
435
+ expect(writeWebSearchSelection(config, null)).toBe(true);
436
+ expect(config).toEqual({ model: 'openai/gpt-5' });
437
+ expect(writeWebSearchSelection(config, null)).toBe(false);
438
+ });
439
+ });
440
+
441
+ describe('findWebSearchProjectOverride', () => {
442
+ const userPath = '/home/u/.config/opencode/opencode.json';
443
+ const layers = (customConfig = {}) => ({ userConfig: { websearch: false }, projectConfig: {}, customConfig, paths: { userPath, projectPath: null } });
444
+
445
+ it('names the deepest project file that sets websearch', () => {
446
+ const files = [
447
+ { path: '/repo/app/.opencode/opencode.json', config: { plugins: [] } },
448
+ { path: '/repo/opencode.json', config: { websearch: false } },
449
+ ];
450
+ expect(findWebSearchProjectOverride(layers(), files)).toBe('/repo/opencode.json');
451
+ expect(findWebSearchProjectOverride(layers(), [{ path: '/repo/app/opencode.json', config: { websearch: { provider: 'exa' } } }, ...files])).toBe('/repo/app/opencode.json');
452
+ });
453
+
454
+ it('is null when no project file sets websearch', () => {
455
+ expect(findWebSearchProjectOverride(layers(), [{ path: '/repo/opencode.json', config: { model: 'x' } }])).toBeNull();
456
+ expect(findWebSearchProjectOverride(layers(), [])).toBeNull();
457
+ });
458
+
459
+ it('is null when OPENCODE_CONFIG sets websearch too, since it wins', () => {
460
+ expect(findWebSearchProjectOverride(layers({ websearch: { provider: 'exa' } }), [{ path: '/repo/opencode.json', config: { websearch: false } }])).toBeNull();
461
+ });
462
+
463
+ it('ignores the user config showing up among project files', () => {
464
+ expect(findWebSearchProjectOverride(layers(), [{ path: userPath, config: { websearch: false } }])).toBeNull();
465
+ });
466
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * READ-ONLY view of the credentials OpenCode 2.x keeps in its SQLite database.
3
+ *
4
+ * OpenCode 2.x imports the legacy `auth.json` once and from then on stores
5
+ * every credential in `<data>/opencode.db`, table `credential`, as plain JSON.
6
+ * There is no HTTP route that hands a key back, so the quota providers and
7
+ * voice keys would only ever see credentials that predate the upgrade. This
8
+ * module reads the table directly and projects each row into the legacy
9
+ * `auth.json` entry shape the rest of the server already understands, so a
10
+ * key added through OpenCode after the upgrade works like one that was
11
+ * imported. The database is opened read-only and never written.
12
+ *
13
+ * The table layout is OpenCode's private schema (verified against v2.0.x
14
+ * `packages/core/src/credential/sql.ts`); a schema change makes this reader
15
+ * return null and the caller falls back to `auth.json`.
16
+ *
17
+ * `node:sqlite` ships with Node 22.13+, `bun:sqlite` with Bun; a runtime
18
+ * without either also falls back to the file, silently.
19
+ */
20
+
21
+ import { createRequire } from 'node:module';
22
+
23
+ const DB_FILE_NAME = 'opencode.db';
24
+
25
+ /**
26
+ * A minimal read-only connection: `all(sql)` and `close()`. Node provides it
27
+ * through `node:sqlite`, Bun through `bun:sqlite`; both are built in. Neither
28
+ * being available yields null and the caller falls back to `auth.json`.
29
+ */
30
+ let openConnection;
31
+ const loadSqlite = () => {
32
+ if (openConnection !== undefined) return openConnection;
33
+ const require = createRequire(import.meta.url);
34
+ openConnection = null;
35
+ try {
36
+ const node = typeof process.getBuiltinModule === 'function' ? process.getBuiltinModule('node:sqlite') : null;
37
+ const { DatabaseSync } = node ?? require('node:sqlite');
38
+ if (typeof DatabaseSync === 'function') {
39
+ openConnection = (dbPath) => {
40
+ const db = new DatabaseSync(dbPath, { readOnly: true });
41
+ return { all: (sql) => db.prepare(sql).all(), close: () => db.close() };
42
+ };
43
+ return openConnection;
44
+ }
45
+ } catch {
46
+ // Not Node, or a Node without node:sqlite; try Bun next.
47
+ }
48
+ try {
49
+ const { Database } = require('bun:sqlite');
50
+ if (typeof Database === 'function') {
51
+ openConnection = (dbPath) => {
52
+ const db = new Database(dbPath, { readonly: true });
53
+ return { all: (sql) => db.query(sql).all(), close: () => db.close() };
54
+ };
55
+ }
56
+ } catch {
57
+ // No sqlite runtime at all.
58
+ }
59
+ return openConnection;
60
+ };
61
+
62
+ /**
63
+ * Where OpenCode keeps its database: `OPENCODE_DB` when set (absolute or
64
+ * relative to the data dir), else `opencode.db` in the data dir.
65
+ * @param {{ dataDir: string, env?: NodeJS.ProcessEnv, path: typeof import('node:path') }} options
66
+ */
67
+ export const resolveCredentialDbPath = ({ dataDir, env = process.env, path }) => {
68
+ const configured = (env.OPENCODE_DB ?? '').trim();
69
+ if (configured && configured !== ':memory:') return path.resolve(dataDir, configured);
70
+ return path.join(dataDir, DB_FILE_NAME);
71
+ };
72
+
73
+ /**
74
+ * Project one stored `credential.value` into the legacy `auth.json` entry
75
+ * shape (`{ type: 'api', key }` / `{ type: 'oauth', access, refresh, expires }`).
76
+ * Returns null for a value this reader does not understand.
77
+ */
78
+ export const projectCredentialValue = (raw) => {
79
+ let value;
80
+ try {
81
+ value = typeof raw === 'string' ? JSON.parse(raw) : raw;
82
+ } catch {
83
+ return null;
84
+ }
85
+ if (!value || typeof value !== 'object') return null;
86
+ if (value.type === 'key' && typeof value.key === 'string') {
87
+ const entry = { type: 'api', key: value.key };
88
+ if (value.metadata && typeof value.metadata === 'object') entry.metadata = value.metadata;
89
+ return entry;
90
+ }
91
+ if (value.type === 'oauth' && typeof value.access === 'string' && typeof value.refresh === 'string') {
92
+ const entry = { type: 'oauth', access: value.access, refresh: value.refresh, expires: Number(value.expires) || 0 };
93
+ const metadata = value.metadata && typeof value.metadata === 'object' ? value.metadata : {};
94
+ if (typeof metadata.accountID === 'string') entry.accountId = metadata.accountID;
95
+ if (typeof metadata.enterpriseUrl === 'string') entry.enterpriseUrl = metadata.enterpriseUrl;
96
+ return entry;
97
+ }
98
+ return null;
99
+ };
100
+
101
+ /**
102
+ * Read every integration's selected credential from the database.
103
+ *
104
+ * An integration may hold several credentials; the one OpenCode uses is the
105
+ * `active` row, else the most recently updated one, which mirrors how
106
+ * OpenCode itself picks. The result is keyed by integration id, which for
107
+ * providers is the provider id (`openai`, `github-copilot`, ...).
108
+ *
109
+ * @returns {Record<string, object> | null} null when the database or the
110
+ * sqlite runtime is unavailable, so the caller can tell "no credentials"
111
+ * from "could not look".
112
+ */
113
+ export const readCredentialsFromDb = ({ dbPath, fs }) => {
114
+ const open = loadSqlite();
115
+ if (!open) return null;
116
+ if (!fs.existsSync(dbPath)) return null;
117
+
118
+ let db;
119
+ try {
120
+ db = open(dbPath);
121
+ const rows = db.all(
122
+ 'SELECT integration_id, value, active, time_updated FROM credential ' +
123
+ 'WHERE integration_id IS NOT NULL ' +
124
+ 'ORDER BY integration_id, active DESC, time_updated DESC',
125
+ );
126
+ const result = {};
127
+ for (const row of rows) {
128
+ const id = typeof row.integration_id === 'string' ? row.integration_id : '';
129
+ if (!id || id in result) continue;
130
+ const entry = projectCredentialValue(row.value);
131
+ if (entry) result[id] = entry;
132
+ }
133
+ return result;
134
+ } catch (error) {
135
+ console.warn('Could not read OpenCode credentials database:', error instanceof Error ? error.message : error);
136
+ return null;
137
+ } finally {
138
+ try {
139
+ db?.close();
140
+ } catch {
141
+ // The connection is read-only; a failed close changes nothing.
142
+ }
143
+ }
144
+ };