@vantran-se/openchamber-web 1.24.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -8
- package/bin/lib/commands-connect-url.js +3 -8
- package/bin/lib/commands-tunnel.js +3 -0
- package/dist/assets/{CodeMirrorCommentWidgets-B4a0JkUz.js → CodeMirrorCommentWidgets-ntXIbhV2.js} +1 -1
- package/dist/assets/DiffView-CjxUovbM.js +4 -0
- package/dist/assets/FilesView-DFWD0RQR.js +17 -0
- package/dist/assets/{GitView-Byu2DY7s.js → GitView-BWa7e5vw.js} +4 -4
- package/dist/assets/{GuestAttachDialog-B808E0-d.js → GuestAttachDialog-DdOuyZzp.js} +1 -1
- package/dist/assets/GuestSurfacePane-DDfzid3U.js +1 -0
- package/dist/assets/{JsonTreeView-VW_7czo3.js → JsonTreeView-3sGwqexh.js} +1 -1
- package/dist/assets/LinearIssuesView-C6dpIdjw.js +1 -0
- package/dist/assets/MarkdownImageGallery-Q7YR_tl4.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DxDV56bL.js +12 -0
- package/dist/assets/OnboardingScreen-rK4Stg__.js +1 -0
- package/dist/assets/{PatchDiff-DCjGhlYV.js → PatchDiff-DS-IMU1j.js} +1 -1
- package/dist/assets/{PierreDiffViewer-CEDZNkU2.js → PierreDiffViewer-DW23KWKZ.js} +1 -1
- package/dist/assets/PlanView-BCk_VobP.js +5 -0
- package/dist/assets/PluginPane-BPP1dbck.js +3 -0
- package/dist/assets/SettingsView-kMmrpn43.js +29 -0
- package/dist/assets/SettingsWindow-BpvD6IYQ.js +1 -0
- package/dist/assets/SubmoduleDiffSummary-BLttSS1X.js +1 -0
- package/dist/assets/ToolOutputDialog-CtqDqbyr.js +15 -0
- package/dist/assets/ToolPartDiffPreview-5FcKdyRy.js +8 -0
- package/dist/assets/{VirtualizedFileDiff-NU0FHFhT.js → VirtualizedFileDiff-C763hNCR.js} +4 -4
- package/dist/assets/{WalkthroughView-BZyjhqHv.js → WalkthroughView-lap5btCF.js} +2 -2
- package/dist/assets/appThemeRegistry-u4zw1l0B.js +1 -0
- package/dist/assets/{commitAndPush-BDftrjr6.js → commitAndPush-CwGJfJG-.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-DlWlEM3x.js → computeVirtualFileMetrics-DW5jjX4n.js} +1 -1
- package/dist/assets/de-CndEg67T.js +10 -0
- package/dist/assets/es-DglvPlaC.js +16 -0
- package/dist/assets/fr-DejPCpbS.js +14 -0
- package/dist/assets/index-C2r64QfD.js +1 -0
- package/dist/assets/index-CLcM8HKx.css +1 -0
- package/dist/assets/index-CvLgnuNI.js +41 -0
- package/dist/assets/{isDiffPlainText-CO4e5J_S.js → isDiffPlainText-D47a6hMP.js} +1 -1
- package/dist/assets/{isFilePlainText-ue6kCou7.js → isFilePlainText-B-mGgeKl.js} +1 -1
- package/dist/assets/ja-CVpQawCd.js +16 -0
- package/dist/assets/ko-Bw446h9j.js +16 -0
- package/dist/assets/main-Bz4WWXbr.js +2 -0
- package/dist/assets/main-oreqDsOV.js +896 -0
- package/dist/assets/{markdownCore-CK48LG0z.js → markdownCore-D2e5sp9y.js} +4 -4
- package/dist/assets/miniChat-D1v2gsBI.js +2 -0
- package/dist/assets/mobile-DBj3lCf2.js +2 -0
- package/dist/assets/{patchFileDiff-D7QVlovD.js → patchFileDiff-BpeUOV91.js} +3 -3
- package/dist/assets/pl-BYNlPdhm.js +16 -0
- package/dist/assets/pt-BR-CL-Yfv9H.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-DVtfEB2z.js +2 -0
- package/dist/assets/renderMobileApp-DFp9-xbG.js +25 -0
- package/dist/assets/runtimeEndpointReset-CsIpUep1.js +27 -0
- package/dist/assets/scheduledTasksApi-B5OHVpPM.js +1 -0
- package/dist/assets/{shikiHighlight-BPu3JiQs.js → shikiHighlight-DNm2Z_Jc.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-DWqRPo9A.js → textMateThemeFromAppTheme-DGF2kG7g.js} +1 -1
- package/dist/assets/tr-DzmbxLy-.js +7 -0
- package/dist/assets/uk-C7vQnwro.js +16 -0
- package/dist/assets/useAppFontEffects-Ci3FEaPf.js +7337 -0
- package/dist/assets/{useInlineCommentController-CgS0fnw_.js → useInlineCommentController-CuUuakrt.js} +2 -2
- package/dist/assets/useRootScrollLock-CQIsyERq.js +2 -0
- package/dist/assets/{vendor-codemirror-language-data-Bx2EHc1O.js → vendor-codemirror-language-data-CQa7_1cr.js} +2 -2
- package/dist/assets/vendor-cron-parser-CjvYVigk.js +2 -0
- package/dist/assets/vendor-opencode-client-_BAHQo4R.js +12 -0
- package/dist/assets/{vendor-remixicon-react-BDmGmFCA.js → vendor-remixicon-react-D6dZ0UwF.js} +1 -1
- package/dist/assets/{vendor-zod-CgAdXWZO.js → vendor-zod-Gea9uBlY.js} +1 -1
- package/dist/assets/{web-components-DxX8bZ1o.js → web-components-B6tKQyJN.js} +2 -2
- package/dist/assets/zh-CN-Cl0srS-z.js +16 -0
- package/dist/assets/zh-TW-DtII5bDb.js +16 -0
- package/dist/index.html +13 -7
- package/dist/mini-chat.html +5 -5
- package/dist/mobile.html +5 -5
- package/package.json +6 -5
- package/server/index.d.ts +1 -0
- package/server/index.js +246 -91
- package/server/lib/agent-tool/DOCUMENTATION.md +32 -15
- package/server/lib/agent-tool/runtime.js +135 -54
- package/server/lib/agent-tool/runtime.test.js +155 -87
- package/server/lib/browser-control/DOCUMENTATION.md +37 -2
- package/server/lib/browser-control/provider.js +217 -0
- package/server/lib/browser-control/provider.test.js +251 -0
- package/server/lib/context-obligatory/runtime.js +61 -50
- package/server/lib/context-obligatory/runtime.test.js +62 -162
- package/server/lib/event-stream/DOCUMENTATION.md +12 -1
- package/server/lib/event-stream/delta-coalescer.js +0 -0
- package/server/lib/event-stream/delta-coalescer.test.js +68 -57
- package/server/lib/event-stream/directory-ws-bridge.js +1 -1
- package/server/lib/event-stream/global-hub.js +10 -1
- package/server/lib/event-stream/global-hub.test.js +27 -26
- package/server/lib/event-stream/index.js +6 -0
- package/server/lib/event-stream/protocol.js +26 -19
- package/server/lib/event-stream/protocol.test.js +18 -7
- package/server/lib/event-stream/rebind.test.js +5 -5
- package/server/lib/event-stream/runtime.test.js +17 -15
- package/server/lib/event-stream/translate-v2.js +313 -0
- package/server/lib/event-stream/translate-v2.test.js +209 -0
- package/server/lib/event-stream/upstream-reader.test.js +16 -16
- package/server/lib/fs/DOCUMENTATION.md +4 -2
- package/server/lib/fs/byte-range.js +35 -0
- package/server/lib/fs/byte-range.test.js +31 -0
- package/server/lib/fs/routes.js +47 -42
- package/server/lib/fs/routes.test.js +123 -18
- package/server/lib/git/service.js +12 -0
- package/server/lib/github/DOCUMENTATION.md +1 -1
- package/server/lib/github/pr-status.js +22 -2
- package/server/lib/github/pr-status.test.js +27 -14
- package/server/lib/guests/DOCUMENTATION.md +13 -0
- package/server/lib/guests/catalog.js +12 -0
- package/server/lib/guests/catalog.test.js +33 -0
- package/server/lib/guests/routes.js +10 -1
- package/server/lib/guests/service.js +192 -14
- package/server/lib/guests/service.test.js +201 -1
- package/server/lib/guests/surface.js +630 -0
- package/server/lib/guests/surface.test.js +486 -0
- package/server/lib/markdown-image-grants/routes.js +18 -5
- package/server/lib/markdown-image-grants/routes.test.js +9 -6
- package/server/lib/message-queue/DOCUMENTATION.md +33 -22
- package/server/lib/message-queue/runtime.js +155 -100
- package/server/lib/message-queue/runtime.test.js +168 -82
- package/server/lib/notifications/DOCUMENTATION.md +28 -0
- package/server/lib/notifications/control-stream.http.test.js +68 -0
- package/server/lib/notifications/emitter-runtime.js +15 -0
- package/server/lib/notifications/emitter-runtime.test.js +32 -0
- package/server/lib/notifications/push-runtime.js +5 -1
- package/server/lib/notifications/push-runtime.test.js +2 -2
- package/server/lib/notifications/routes.js +1 -1
- package/server/lib/notifications/runtime.js +115 -26
- package/server/lib/notifications/template-runtime.js +13 -15
- package/server/lib/notifications/template-runtime.test.js +16 -9
- package/server/lib/notifications/trigger-runtime.test.js +187 -0
- package/server/lib/openchamber-control/DOCUMENTATION.md +10 -0
- package/server/lib/openchamber-control/actions.js +3 -2
- package/server/lib/openchamber-control/error.js +1 -1
- package/server/lib/openchamber-control/file-open.js +59 -0
- package/server/lib/openchamber-control/file-open.test.js +57 -0
- package/server/lib/openchamber-control/routes.test.js +12 -0
- package/server/lib/openchamber-control/service.js +102 -49
- package/server/lib/openchamber-control/service.test.js +84 -44
- package/server/lib/openchamber-sessions/archive-store.js +224 -0
- package/server/lib/openchamber-sessions/archive-store.test.js +187 -0
- package/server/lib/openchamber-sessions/opencode-client.js +22 -0
- package/server/lib/openchamber-sessions/routes.js +398 -328
- package/server/lib/openchamber-sessions/routes.test.js +777 -740
- package/server/lib/openchamber-sessions/session-metadata-store.js +285 -0
- package/server/lib/openchamber-sessions/session-metadata-store.test.js +259 -0
- package/server/lib/opencode/DOCUMENTATION.md +449 -29
- package/server/lib/opencode/agents.js +233 -327
- package/server/lib/opencode/auth.js +41 -44
- package/server/lib/opencode/auth.test.js +92 -0
- package/server/lib/opencode/cli-upgrade.d.ts +4 -0
- package/server/lib/opencode/cli-upgrade.js +18 -0
- package/server/lib/opencode/cli-upgrade.test.js +36 -0
- package/server/lib/opencode/commands.js +164 -145
- package/server/lib/opencode/compatibility.d.ts +16 -0
- package/server/lib/opencode/compatibility.js +88 -0
- package/server/lib/opencode/compatibility.test.js +62 -0
- package/server/lib/opencode/config-entity-routes.js +58 -29
- package/server/lib/opencode/config-entity-v2.test.js +290 -0
- package/server/lib/opencode/config-mutation-response.js +17 -7
- package/server/lib/opencode/config-mutation-response.test.js +4 -7
- package/server/lib/opencode/config-paths.test.js +2 -2
- package/server/lib/opencode/config-v2.d.ts +183 -0
- package/server/lib/opencode/config-v2.js +801 -0
- package/server/lib/opencode/config-v2.test.js +466 -0
- package/server/lib/opencode/credential-db.js +144 -0
- package/server/lib/opencode/credential-db.test.js +104 -0
- package/server/lib/opencode/env-runtime.js +48 -9
- package/server/lib/opencode/env-runtime.test.js +45 -0
- package/server/lib/opencode/feature-routes-runtime.js +15 -5
- package/server/lib/opencode/lifecycle-process.test.js +7 -4
- package/server/lib/opencode/lifecycle.js +85 -30
- package/server/lib/opencode/lifecycle.test.js +80 -8
- package/server/lib/opencode/login-shell-env.js +27 -0
- package/server/lib/opencode/managed-config-file.js +119 -0
- package/server/lib/opencode/managed-config-file.test.js +152 -0
- package/server/lib/opencode/managed-plugin-config.js +33 -13
- package/server/lib/opencode/mcp.js +43 -149
- package/server/lib/opencode/network-runtime.js +6 -7
- package/server/lib/opencode/openchamber-routes.test.js +2 -5
- package/server/lib/opencode/plugin-routes.js +3 -3
- package/server/lib/opencode/plugin-routes.test.js +6 -24
- package/server/lib/opencode/plugins.js +91 -40
- package/server/lib/opencode/plugins.test.js +28 -15
- package/server/lib/opencode/providers.js +63 -83
- package/server/lib/opencode/providers.test.js +128 -59
- package/server/lib/opencode/proxy-agent-wiring.test.js +4 -2
- package/server/lib/opencode/proxy.js +179 -129
- package/server/lib/opencode/pwa-manifest-routes.js +4 -2
- package/server/lib/opencode/pwa-manifest-routes.test.js +3 -2
- package/server/lib/opencode/response-envelope.js +14 -0
- package/server/lib/opencode/response-envelope.test.js +23 -0
- package/server/lib/opencode/routes-upgrade.test.js +73 -116
- package/server/lib/opencode/routes.js +121 -428
- package/server/lib/opencode/server-utils-runtime.js +10 -4
- package/server/lib/opencode/session-activity.js +78 -0
- package/server/lib/opencode/session-runtime.js +21 -10
- package/server/lib/opencode/session-runtime.test.js +9 -9
- package/server/lib/opencode/settings-helpers.js +9 -9
- package/server/lib/opencode/settings-helpers.test.js +7 -12
- package/server/lib/opencode/settings-registry.json +6 -8
- package/server/lib/opencode/settings-runtime.js +18 -1
- package/server/lib/opencode/settings-runtime.test.js +31 -2
- package/server/lib/opencode/shared.js +54 -22
- package/server/lib/opencode/shared.test.js +8 -4
- package/server/lib/opencode/shutdown-runtime.js +54 -7
- package/server/lib/opencode/shutdown-runtime.test.js +224 -30
- package/server/lib/opencode/skill-routes.js +23 -24
- package/server/lib/opencode/skills.js +7 -5
- package/server/lib/opencode/tunnel-wiring-runtime.js +2 -1
- package/server/lib/opencode/upgrade-capability.test.js +1 -1
- package/server/lib/opencode/v1-migration-topup.js +288 -0
- package/server/lib/opencode/v1-migration-topup.test.js +266 -0
- package/server/lib/opencode/v2-install.d.ts +8 -0
- package/server/lib/opencode/v2-install.js +158 -0
- package/server/lib/opencode/v2-install.test.js +124 -0
- package/server/lib/opencode/watcher.js +10 -3
- package/server/lib/opencode/watcher.test.js +17 -21
- package/server/lib/opencode/websearch-config.js +66 -0
- package/server/lib/package-manager.js +55 -68
- package/server/lib/package-manager.test.js +241 -75
- package/server/lib/permission-auto-accept/runtime.js +38 -22
- package/server/lib/permission-auto-accept/runtime.test.js +39 -17
- package/server/lib/quota/DOCUMENTATION.md +4 -0
- package/server/lib/quota/providers/xai.js +4 -4
- package/server/lib/relay/tunnel-host.js +4 -1
- package/server/lib/relay/tunnel-host.test.js +7 -0
- package/server/lib/routing/DOCUMENTATION.md +52 -21
- package/server/lib/routing/defaults.js +23 -6
- package/server/lib/routing/jev.js +17 -7
- package/server/lib/routing/routes.http.test.js +68 -33
- package/server/lib/routing/routes.js +65 -22
- package/server/lib/routing/runtime.js +145 -48
- package/server/lib/routing/runtime.test.js +79 -55
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +4 -0
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +51 -46
- package/server/lib/scheduled-tasks/runtime.js +81 -74
- package/server/lib/scheduled-tasks/runtime.test.js +53 -1
- package/server/lib/session-assist/DOCUMENTATION.md +8 -3
- package/server/lib/session-assist/context.js +122 -46
- package/server/lib/session-assist/context.test.js +117 -49
- package/server/lib/session-assist/runtime.js +52 -22
- package/server/lib/session-assist/runtime.test.js +145 -173
- package/server/lib/session-goal/DOCUMENTATION.md +23 -15
- package/server/lib/session-goal/create.js +9 -12
- package/server/lib/session-goal/create.test.js +27 -28
- package/server/lib/session-goal/runtime.js +164 -77
- package/server/lib/session-goal/runtime.test.js +271 -519
- package/server/lib/session-knowledge/DOCUMENTATION.md +5 -0
- package/server/lib/session-knowledge/runtime.js +59 -41
- package/server/lib/session-knowledge/runtime.test.js +90 -20
- package/server/lib/small-model/DOCUMENTATION.md +154 -208
- package/server/lib/small-model/client.js +113 -0
- package/server/lib/small-model/index.js +296 -158
- package/server/lib/small-model/index.test.js +423 -256
- package/server/lib/small-model/routes.js +1 -1
- package/server/lib/spaces/DOCUMENTATION.md +807 -0
- package/server/lib/spaces/code-in-bait.js +185 -0
- package/server/lib/spaces/code-in.js +641 -0
- package/server/lib/spaces/code-in.test.js +1048 -0
- package/server/lib/spaces/errors.js +8 -0
- package/server/lib/spaces/exec-http.js +94 -0
- package/server/lib/spaces/gatekeeper-channel.js +202 -0
- package/server/lib/spaces/gatekeeper-channel.test.js +215 -0
- package/server/lib/spaces/gatekeeper-program.cjs +914 -0
- package/server/lib/spaces/gatekeeper-program.test.js +1391 -0
- package/server/lib/spaces/hardening.js +457 -0
- package/server/lib/spaces/hardening.test.js +368 -0
- package/server/lib/spaces/host-git.js +127 -0
- package/server/lib/spaces/host-git.test.js +137 -0
- package/server/lib/spaces/labels.js +234 -0
- package/server/lib/spaces/labels.test.js +219 -0
- package/server/lib/spaces/layout.js +184 -0
- package/server/lib/spaces/layout.test.js +30 -0
- package/server/lib/spaces/manager.js +82 -0
- package/server/lib/spaces/manager.test.js +114 -0
- package/server/lib/spaces/places/code-in.docker.live.test.js +216 -0
- package/server/lib/spaces/places/contract-suite.js +156 -0
- package/server/lib/spaces/places/contract.docker.live.test.js +7 -0
- package/server/lib/spaces/places/contract.test.js +6 -0
- package/server/lib/spaces/places/docker-engine.js +126 -0
- package/server/lib/spaces/places/docker-live-support.js +202 -0
- package/server/lib/spaces/places/docker-tools.js +186 -0
- package/server/lib/spaces/places/docker.js +587 -0
- package/server/lib/spaces/places/docker.test.js +1610 -0
- package/server/lib/spaces/places/escape-suite.js +1018 -0
- package/server/lib/spaces/places/escape.docker.live.test.js +7 -0
- package/server/lib/spaces/places/fake-docker.js +375 -0
- package/server/lib/spaces/places/memory-place.js +103 -0
- package/server/lib/spaces/places/packed.docker.live.test.js +77 -0
- package/server/lib/spaces/places/registry.js +43 -0
- package/server/lib/spaces/places/registry.test.js +50 -0
- package/server/lib/spaces/places/server.docker.live.test.js +225 -0
- package/server/lib/spaces/run-command.js +261 -0
- package/server/lib/spaces/run-command.test.js +396 -0
- package/server/lib/spaces/space-server.js +190 -0
- package/server/lib/spaces/space-server.test.js +335 -0
- package/server/lib/spaces/tools-filler.js +82 -0
- package/server/lib/spaces/tools-filler.test.js +148 -0
- package/server/lib/spaces/tools-pack.js +49 -0
- package/server/lib/spaces/tools-pack.test.js +57 -0
- package/server/lib/spaces/tools.js +138 -0
- package/server/lib/spaces/tools.test.js +153 -0
- package/server/lib/tts/openai-sdk.js +11 -0
- package/server/lib/tts/service.js +7 -5
- package/server/lib/tts/stt.js +3 -2
- package/server/lib/tunnels/DOCUMENTATION.md +2 -0
- package/server/lib/tunnels/routes.js +7 -0
- package/server/lib/tunnels/routes.test.js +55 -0
- package/server/lib/ui-auth/ui-auth.js +38 -2
- package/server/lib/ui-auth/ui-auth.test.js +42 -0
- package/server/lib/ui-auth/ui-passkeys.js +12 -10
- package/server/lib/walkthrough/DOCUMENTATION.md +10 -1
- package/server/lib/walkthrough/pull-request.js +61 -8
- package/server/lib/walkthrough/pull-request.test.js +54 -1
- package/server/lib/walkthrough/routes.js +20 -0
- package/server/lib/walkthrough/routes.test.js +19 -0
- package/server/opencode-proxy.test.js +190 -166
- package/server/opencode-worktree-readiness.test.js +5 -4
- package/dist/assets/DiffView-B2CD9wC8.js +0 -4
- package/dist/assets/FilesView-C4vMFclc.js +0 -13
- package/dist/assets/LinearIssuesView-EzGNCGrd.js +0 -1
- package/dist/assets/MarkdownImageGallery-B2YUDFaB.js +0 -1
- package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +0 -12
- package/dist/assets/OnboardingScreen-CQHBSTEi.js +0 -1
- package/dist/assets/PlanView-D3x-pTkZ.js +0 -5
- package/dist/assets/PluginPane-DEcgDQ9h.js +0 -3
- package/dist/assets/SettingsView-CSinuH-1.js +0 -29
- package/dist/assets/SettingsWindow-B5CHoJFZ.js +0 -1
- package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +0 -1
- package/dist/assets/ToolOutputDialog-tLNWmfcW.js +0 -15
- package/dist/assets/ToolPartDiffPreview-CflzmK-x.js +0 -8
- package/dist/assets/appThemeRegistry-DLicw4dH.js +0 -1
- package/dist/assets/de-Bro6_zyl.js +0 -10
- package/dist/assets/es-Bb9XQZvd.js +0 -16
- package/dist/assets/fr-Dgfzn-IL.js +0 -14
- package/dist/assets/index-CtQNmqUL.js +0 -1
- package/dist/assets/index-DqaMWNrC.css +0 -1
- package/dist/assets/index-Dqc6USl3.js +0 -40
- package/dist/assets/ja-BYsakGFh.js +0 -16
- package/dist/assets/ko-BSaQz9aN.js +0 -16
- package/dist/assets/main-B62_pjL5.js +0 -2
- package/dist/assets/main-BEdcOI2P.js +0 -896
- package/dist/assets/miniChat-LT8Rqofr.js +0 -2
- package/dist/assets/mobile-CqarEynh.js +0 -2
- package/dist/assets/pl-OIviPdoj.js +0 -16
- package/dist/assets/pt-BR-Cltg-E6s.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +0 -2
- package/dist/assets/renderMobileApp-BC1MHg6g.js +0 -25
- package/dist/assets/runtimeEndpointReset-DdrazOVH.js +0 -25
- package/dist/assets/scheduledTasksApi-B2SWMGEU.js +0 -1
- package/dist/assets/tr-CQI-KPre.js +0 -7
- package/dist/assets/uk-CqW7R4DA.js +0 -16
- package/dist/assets/useAppFontEffects-ne84JBVL.js +0 -7333
- package/dist/assets/useRootScrollLock-Cpf1qoGp.js +0 -2
- package/dist/assets/vendor-cron-parser-CUcP7OfX.js +0 -2
- package/dist/assets/vendor-opencode-sdk-BJ73u-bH.js +0 -7
- package/dist/assets/zh-CN-CVZG_o_J.js +0 -16
- package/dist/assets/zh-TW-D5ZxzCZp.js +0 -16
- package/server/lib/opencode/mcp-oauth-callback.test.js +0 -111
- package/server/lib/routing/feature-flag.js +0 -11
- package/server/lib/small-model/call.js +0 -851
- package/server/lib/small-model/call.test.js +0 -1292
- package/server/lib/small-model/catalog.js +0 -13
- package/server/lib/small-model/resolve.js +0 -131
- package/server/lib/small-model/resolve.test.js +0 -197
- package/server/lib/small-model/runtime-providers.js +0 -164
- package/server/lib/small-model/runtime-providers.test.js +0 -118
- package/server/lib/system-prompt/DOCUMENTATION.md +0 -36
- package/server/lib/system-prompt/runtime.js +0 -49
- package/server/lib/system-prompt/runtime.test.js +0 -76
- 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
|
+
};
|