@openchambery/web 1.19.22-beta.1 → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +179 -0
- package/dist/assets/{AgentAvatar-CRaBJXKs.js → AgentAvatar-CzTtVMnI.js} +1 -1
- package/dist/assets/AssistantView-Cg1qQvVe.js +3 -0
- package/dist/assets/{DiagramView-D1LGK_s2.js → DiagramView-hOi72Wdx.js} +1 -1
- package/dist/assets/{FadeInOnReveal-B0kJo9Ii.js → FadeInOnReveal-DCqrQkkz.js} +1 -1
- package/dist/assets/Icon-B8fSTLdP.js +1 -0
- package/dist/assets/MarkdownRenderer-Cw74oVxV.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-DfoCJ1Mq.js → MarkdownRendererImpl-YM7iRmzK.js} +3 -3
- package/dist/assets/MarkstreamRendererImpl-BDy_oGJQ.js +3 -0
- package/dist/assets/MobileDetailNavigation-iX_hb_Xt.js +1 -0
- package/dist/assets/{MobileShareBridge-2Yjn6wXg.js → MobileShareBridge-CAMUupwR.js} +2 -2
- package/dist/assets/MobileSurface-CHbdPcLH.js +2 -0
- package/dist/assets/{MultiRunWindow-tTbMFowS.js → MultiRunWindow-Ci1ODFFX.js} +1 -1
- package/dist/assets/{OnboardingScreen-CD2KtlF3.js → OnboardingScreen-O4cGdz5p.js} +1 -1
- package/dist/assets/OverlayScrollbar-CRfGtcmG.js +2 -0
- package/dist/assets/SettingsView-Dg8BtTV1.js +25 -0
- package/dist/assets/{SettingsWindow-zIa76s4t.js → SettingsWindow-B8d8qIQ7.js} +1 -1
- package/dist/assets/TerminalView-CMiw3s_c.js +8 -0
- package/dist/assets/{ToolOutputDialog-DNqA4uBl.js → ToolOutputDialog-ByaQYRTp.js} +2 -2
- package/dist/assets/appThemeRegistry-BGGHio8T.js +1 -0
- package/dist/assets/{assistant-attachment-cache-C0aRnFkj.js → assistant-attachment-cache-DphJleHr.js} +1 -1
- package/dist/assets/{button-CI7Q-Zw_.js → button-DIIrsMjW.js} +1 -1
- package/dist/assets/{collapsible-D5Phaykh.js → collapsible-CnvUeYst.js} +1 -1
- package/dist/assets/{context-menu-CubSDW2g.js → context-menu-BGBUpZEQ.js} +1 -1
- package/dist/assets/{desktopHosts-B1ixEylr.js → desktopHosts-JGFqhlaM.js} +1 -1
- package/dist/assets/{desktopRelayRestore-4JmFUw8r.js → desktopRelayRestore-CHEwrI93.js} +1 -1
- package/dist/assets/{diagram-DdLO8HZ9.js → diagram-rvn7_qBM.js} +1 -1
- package/dist/assets/{dialog-B6pnIYmp.js → dialog-CQ63sc9E.js} +1 -1
- package/dist/assets/es-Dsmc0A_3.js +15 -0
- package/dist/assets/fileMentionTouchSelection-BzzcWJCi.js +3 -0
- package/dist/assets/{fileQueries-DQc8EJUf.js → fileQueries-DN6gbLhM.js} +1 -1
- package/dist/assets/fr-C_ZlZDI2.js +13 -0
- package/dist/assets/gitApi-BF8vuoZc.js +10 -0
- package/dist/assets/{imageSource-BlAJJUEI.js → imageSource-BMztRu0c.js} +1 -1
- package/dist/assets/{input-D-__fwRi.js → input-BVu-iykl.js} +1 -1
- package/dist/assets/insertionBoundaries-EcAfGT2D.js +1 -0
- package/dist/assets/ja-BqqTaWTi.js +15 -0
- package/dist/assets/ko-DP2XPM37.js +15 -0
- package/dist/assets/main-B7t9lavz.js +2 -0
- package/dist/assets/main-D2BzpnUt.css +1 -0
- package/dist/assets/main-DWP-prNB.js +169 -0
- package/dist/assets/markdown-worker-bS5OgGO3.js +2 -0
- package/dist/assets/{markdownHeightCache-BhxqJOvf.js → markdownHeightCache-D9-B1wdY.js} +1 -1
- package/dist/assets/markdownTheme-DM5h1i18.js +1 -0
- package/dist/assets/mcpQueries-CfXrAkuU.js +1 -0
- package/dist/assets/miniChat-DgOrEQbN.js +2 -0
- package/dist/assets/mobile-Co_ZOHkZ.js +2 -0
- package/dist/assets/multirun-DelyUO6k.js +4 -0
- package/dist/assets/persistence-D0y7226-.js +3 -0
- package/dist/assets/pl-BCf5uLlK.js +15 -0
- package/dist/assets/platform-CurqO28D.js +6 -0
- package/dist/assets/{projectMeta-S1hJ7oBp.js → projectMeta-CPElzJ0n.js} +2 -2
- package/dist/assets/pt-BR-W09Q2pvn.js +15 -0
- package/dist/assets/{relay-image-stream-DM7atqJs.js → relay-image-stream-DrdVj99c.js} +1 -1
- package/dist/assets/renderElectronMiniChatApp-pOE93w86.js +2 -0
- package/dist/assets/renderMobileApp-F7ZSLcw8.js +28 -0
- package/dist/assets/runtime-fetch-Fs9ZV2V8.js +1 -0
- package/dist/assets/{runtimeConfig-CMkwoQ_X.js → runtimeConfig-DSS0KxG3.js} +1 -1
- package/dist/assets/runtimeEndpointReset-x2Wgx57R.js +149 -0
- package/dist/assets/sessionChangeSummary-BUGSYcHp.js +2 -0
- package/dist/assets/{sessionLookup-BrU3rFHJ.js → sessionLookup-Bss8M20i.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-Fnfhf3K9.js → sortable-tabs-strip-CE7W7lJ9.js} +1 -1
- package/dist/assets/src-lS1keDVV.css +2 -0
- package/dist/assets/store-jaP79mqI.js +15 -0
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-mDR2jXTo.js} +1 -1
- package/dist/assets/textarea-XD2oX_Ee.js +1 -0
- package/dist/assets/{tooltip-0T5jv0cu.js → tooltip-DYeQ_Hvy.js} +1 -1
- package/dist/assets/uk-BgFzz8m2.js +15 -0
- package/dist/assets/{url-_z7evzdK.js → url-CGbJbiJq.js} +1 -1
- package/dist/assets/{useAppFontEffects-C02YFyAv.js → useAppFontEffects-BAWHZ-t0.js} +64 -45
- package/dist/assets/{useAssistantUIStore-CMutVh9B.js → useAssistantUIStore-Bz4tRflF.js} +1 -1
- package/dist/assets/useDesktopWindowControlsLayout-hN_nLb7T.js +1 -0
- package/dist/assets/useEffectiveDirectory-Do9DQ453.js +1 -0
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-Bjx8zkGM.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BsY_gi_K.js +414 -0
- package/dist/assets/useSessionAutoCleanup-C4g5KmDf.js +2 -0
- package/dist/assets/useUIStore-syVmbuDA.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-C9z0ML1z.js → useWorkerHighlightedLines-B5QXxBok.js} +2 -2
- package/dist/assets/vendor-opencode-client-C12gfjag.js +12 -0
- package/dist/assets/{vendor-reactuses-core-BnW6GxJt.js → vendor-reactuses-core-Dq5ocTF8.js} +1 -1
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-BhqSLgUY.js +15 -0
- package/dist/assets/zh-TW-BsaBgY-p.js +15 -0
- package/dist/index.html +12 -12
- package/dist/mini-chat.html +12 -12
- package/dist/mobile.html +12 -12
- package/package.json +3 -2
- package/server/index.js +423 -20
- package/server/lib/assistants/DOCUMENTATION.md +26 -21
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/contracts.js +11 -1
- package/server/lib/assistants/projection-history.test.js +454 -0
- package/server/lib/assistants/routes.js +11 -5
- package/server/lib/assistants/service.js +850 -294
- package/server/lib/assistants/service.test.js +588 -274
- package/server/lib/browser-provider/DOCUMENTATION.md +141 -0
- package/server/lib/browser-provider/contract.js +312 -0
- package/server/lib/browser-provider/host.js +421 -0
- package/server/lib/browser-provider/host.test.js +304 -0
- package/server/lib/browser-provider/routes.js +122 -0
- package/server/lib/browser-provider/routes.test.js +120 -0
- package/server/lib/browser-provider/selection-store.js +43 -0
- package/server/lib/browser-provider/selection-store.test.js +35 -0
- package/server/lib/conversations/DOCUMENTATION.md +5 -5
- package/server/lib/conversations/service.js +87 -57
- package/server/lib/conversations/service.test.js +132 -100
- package/server/lib/conversations/validation.js +11 -1
- package/server/lib/event-stream/DOCUMENTATION.md +3 -1
- package/server/lib/event-stream/directory-ws-bridge.js +13 -2
- package/server/lib/event-stream/global-hub.js +51 -5
- package/server/lib/event-stream/reasoning-projection.js +16 -1
- package/server/lib/event-stream/reasoning-projection.test.js +15 -0
- package/server/lib/event-stream/runtime.js +57 -20
- package/server/lib/event-stream/runtime.test.js +112 -0
- package/server/lib/llm/DOCUMENTATION.md +95 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +41 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +516 -453
- package/server/lib/llm/generate.test.js +705 -484
- package/server/lib/llm/routes.js +8 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.js +15 -2
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/notifications/DOCUMENTATION.md +3 -1
- package/server/lib/notifications/runtime.js +133 -31
- package/server/lib/notifications/runtime.test.js +103 -0
- package/server/lib/notifications/template-runtime.js +130 -38
- package/server/lib/notifications/template-runtime.test.js +96 -0
- package/server/lib/opencode/DOCUMENTATION.md +111 -16
- package/server/lib/opencode/agent-document.d.ts +28 -0
- package/server/lib/opencode/agent-document.js +280 -0
- package/server/lib/opencode/agent-document.test.js +137 -0
- package/server/lib/opencode/agents.js +96 -0
- package/server/lib/opencode/auth-state-runtime.js +5 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/config-entity-routes.js +117 -103
- package/server/lib/opencode/config-entity-routes.test.js +148 -10
- package/server/lib/opencode/config-hot-reload.integration.test.js +105 -0
- package/server/lib/opencode/ensure-cli.d.ts +8 -0
- package/server/lib/opencode/ensure-cli.js +238 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +208 -111
- package/server/lib/opencode/env-runtime.test.js +211 -14
- package/server/lib/opencode/feature-routes-runtime.js +90 -5
- package/server/lib/opencode/feature-routes-runtime.test.js +25 -0
- package/server/lib/opencode/http-server-connections.js +98 -0
- package/server/lib/opencode/http-server-connections.test.js +321 -0
- package/server/lib/opencode/lifecycle.js +539 -95
- package/server/lib/opencode/lifecycle.test.js +698 -6
- package/server/lib/opencode/managed-capabilities-runtime.js +15 -2
- package/server/lib/opencode/managed-capabilities-runtime.test.js +1 -0
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/openchamber-routes.js +7 -1
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +88 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +326 -0
- package/server/lib/opencode/owned-runtime-upgrade.js +431 -0
- package/server/lib/opencode/owned-runtime-upgrade.test.js +160 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugin-routes.js +3 -18
- package/server/lib/opencode/plugin-routes.test.js +9 -9
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/provider-catalog.js +58 -12
- package/server/lib/opencode/provider-catalog.test.js +48 -0
- package/server/lib/opencode/proxy.js +373 -28
- package/server/lib/opencode/proxy.test.js +353 -0
- package/server/lib/opencode/routes.js +333 -76
- package/server/lib/opencode/routes.test.js +315 -50
- package/server/lib/opencode/runtime-contract.js +401 -0
- package/server/lib/opencode/runtime-contract.test.js +214 -0
- package/server/lib/opencode/server-opencode-fetch.js +242 -0
- package/server/lib/opencode/server-opencode-fetch.test.js +130 -0
- package/server/lib/opencode/server-utils-runtime.js +35 -0
- package/server/lib/opencode/shared-service.integration.test.js +66 -0
- package/server/lib/opencode/shared-service.js +147 -0
- package/server/lib/opencode/shared-service.test.js +93 -0
- package/server/lib/opencode/shared.js +3 -3
- package/server/lib/opencode/shutdown-runtime.js +15 -6
- package/server/lib/opencode/shutdown-runtime.test.js +49 -1
- package/server/lib/opencode/skill-routes.js +26 -40
- package/server/lib/opencode/skill-routes.test.js +71 -7
- package/server/lib/opencode/skills.test.js +15 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +4 -0
- package/server/lib/opencode/static-routes-runtime.js +3 -1
- package/server/lib/opencode/static-routes-runtime.test.js +33 -0
- package/server/lib/opencode/upgrade-screen.d.ts +30 -0
- package/server/lib/opencode/upgrade-screen.js +299 -0
- package/server/lib/opencode/upgrade-screen.test.js +196 -0
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +60 -0
- package/server/lib/opencode/v2-plugin-host-shim/index.mjs +155 -0
- package/server/lib/opencode/v2-plugin-host-shim/package.json +4 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/permission-auto-accept/DOCUMENTATION.md +5 -5
- package/server/lib/permission-auto-accept/runtime.js +14 -7
- package/server/lib/permission-auto-accept/runtime.test.js +27 -12
- package/server/lib/relay/reasoning-projection.e2e.test.ts +22 -7
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +79 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +381 -310
- package/server/lib/scheduled-tasks/runtime.test.js +600 -309
- package/server/lib/session-goal/DOCUMENTATION.md +173 -22
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +240 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +1617 -113
- package/server/lib/session-goal/runtime.test.js +1500 -70
- package/server/lib/session-index/DOCUMENTATION.md +18 -15
- package/server/lib/session-index/event-ingest.js +89 -16
- package/server/lib/session-index/event-ingest.test.js +73 -0
- package/server/lib/session-index/host-authority-snapshot.js +92 -0
- package/server/lib/session-index/routes.host-archive.test.js +201 -0
- package/server/lib/session-index/routes.js +63 -10
- package/server/lib/session-index/routes.test.js +8 -8
- package/server/lib/session-index/service.js +7 -1
- package/server/lib/session-index/sync-runtime.js +171 -41
- package/server/lib/session-index/sync-runtime.test.js +164 -9
- package/server/lib/session-metadata/DOCUMENTATION.md +96 -0
- package/server/lib/session-metadata/opencode-session-record.d.ts +57 -0
- package/server/lib/session-metadata/opencode-session-record.js +316 -0
- package/server/lib/session-metadata/opencode-session-record.test.js +242 -0
- package/server/lib/session-metadata/routes.js +197 -0
- package/server/lib/session-metadata/routes.test.js +156 -0
- package/server/lib/session-metadata/session-archive.d.ts +73 -0
- package/server/lib/session-metadata/session-archive.js +654 -0
- package/server/lib/session-metadata/session-archive.test.js +359 -0
- package/server/lib/session-metadata/session-metadata-store.d.ts +38 -0
- package/server/lib/session-metadata/session-metadata-store.js +422 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +379 -0
- package/server/lib/session-metadata/session-projection.d.ts +41 -0
- package/server/lib/session-metadata/session-projection.js +279 -0
- package/server/lib/session-metadata/session-projection.test.js +64 -0
- package/server/lib/session-metadata/system-session.js +55 -0
- package/server/lib/session-metadata/system-session.test.js +55 -0
- package/server/lib/session-title/DOCUMENTATION.md +40 -7
- package/server/lib/session-title/native-runtime.test.js +167 -0
- package/server/lib/session-title/runtime.js +69 -44
- package/server/lib/session-title/session-access.js +46 -0
- package/server/lib/session-title/session-access.test.js +91 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +12 -11
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +123 -75
- package/server/lib/session-turn-pages/routes.test.js +566 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/session-turn-pages/session-message-projection.d.ts +13 -0
- package/server/lib/session-turn-pages/session-message-projection.js +281 -0
- package/server/lib/small-model/DOCUMENTATION.md +101 -146
- package/server/lib/small-model/catalog.js +84 -54
- package/server/lib/small-model/catalog.test.js +111 -9
- package/server/lib/small-model/custom-api.js +68 -0
- package/server/lib/small-model/custom-api.test.js +64 -0
- package/server/lib/small-model/index.js +125 -241
- package/server/lib/small-model/resolve.js +35 -120
- package/server/lib/small-model/resolve.test.js +20 -110
- package/server/lib/small-model/routes.js +1 -3
- package/server/lib/small-model/summary-settings.test.js +157 -253
- package/server/opencode-proxy.test.js +63 -12
- package/dist/assets/AssistantView-BUveAgc4.js +0 -3
- package/dist/assets/Icon-BgdfPhNs.js +0 -1
- package/dist/assets/MarkdownRenderer-BbrbDH6-.js +0 -2
- package/dist/assets/MarkstreamRendererImpl-D2dsW5Lg.js +0 -3
- package/dist/assets/MobileDetailNavigation-B0aqKXd7.js +0 -1
- package/dist/assets/MobileSurface-DW7WfPsy.js +0 -2
- package/dist/assets/OverlayScrollbar-DrA0Vsg5.js +0 -2
- package/dist/assets/SettingsView-PkXIavr7.js +0 -25
- package/dist/assets/TerminalView-CDdzF4yi.js +0 -8
- package/dist/assets/appThemeRegistry-D5Yxg4YF.js +0 -1
- package/dist/assets/es-DA3MqOo0.js +0 -15
- package/dist/assets/fileMentionTouchSelection-DNeiqns-.js +0 -4
- package/dist/assets/fr-Dbz0CdIA.js +0 -13
- package/dist/assets/gitApi-DXyC4MiV.js +0 -7
- package/dist/assets/insertionBoundaries-KlOVrHHt.js +0 -1
- package/dist/assets/ja-DumanVQA.js +0 -15
- package/dist/assets/ko-C0IkiUjQ.js +0 -15
- package/dist/assets/main-BPS5ylib.js +0 -2
- package/dist/assets/main-Cpd9VWYs.js +0 -169
- package/dist/assets/main-qSeeJRW1.css +0 -1
- package/dist/assets/markdown-worker-CptoICmq.js +0 -2
- package/dist/assets/markdownTheme-DOfGg-A7.js +0 -1
- package/dist/assets/miniChat-B8_KLWiY.js +0 -2
- package/dist/assets/mobile-CtktKrXn.js +0 -2
- package/dist/assets/multirun-CYfP9Cfm.js +0 -4
- package/dist/assets/persistence-Ce3AnpEr.js +0 -3
- package/dist/assets/pl-B9No6MQp.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/pt-BR-DyQ0WTDh.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-iK14LWm8.js +0 -2
- package/dist/assets/renderMobileApp-DORLPWnb.js +0 -28
- package/dist/assets/runtime-fetch-zIalUnbb.js +0 -1
- package/dist/assets/runtimeEndpointReset-ChOxt-J7.js +0 -147
- package/dist/assets/sessionChangeSummary-NnxcqCTI.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/store-5B_LDnLM.js +0 -15
- package/dist/assets/textarea-bHYxZtfH.js +0 -1
- package/dist/assets/uk-C_1KkqyP.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-Dnr-jX0C.js +0 -1
- package/dist/assets/useEffectiveDirectory-CZLLyarc.js +0 -1
- package/dist/assets/useMobileNavigationStore-CoCiqNgm.js +0 -413
- package/dist/assets/useSessionAutoCleanup-xQyETQkG.js +0 -2
- package/dist/assets/useUIStore-Bxv3f23A.js +0 -1
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-uWILgBdt.js +0 -15
- package/dist/assets/zh-TW-MYfdvhbD.js +0 -15
- package/server/lib/small-model/call.js +0 -427
- package/server/lib/small-model/call.test.js +0 -382
- package/server/lib/small-model/opencode-session.js +0 -339
- package/server/lib/small-model/opencode-session.test.js +0 -177
|
@@ -2,9 +2,62 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { parse as parseJsonc } from 'jsonc-parser';
|
|
5
|
-
import {
|
|
5
|
+
import { OpenCode } from '@opencode/client';
|
|
6
6
|
import { projectProviderCatalog } from './provider-catalog.js';
|
|
7
7
|
|
|
8
|
+
// Compose official v2 provider.list + model.list (+ optional default) into the
|
|
9
|
+
// catalog shape consumed by projectProviderCatalog. Missing arrays are failure.
|
|
10
|
+
const composeV2ProviderCatalogSource = (providers, models, defaultModel) => {
|
|
11
|
+
if (!Array.isArray(providers) || !Array.isArray(models)) return null;
|
|
12
|
+
|
|
13
|
+
const modelsByProvider = new Map();
|
|
14
|
+
for (const model of models) {
|
|
15
|
+
if (!model || typeof model !== 'object') continue;
|
|
16
|
+
const providerID = typeof model.providerID === 'string' ? model.providerID : '';
|
|
17
|
+
// ModelInfo.id is the external model id used in generate/session refs.
|
|
18
|
+
// ModelInfo.modelID is a separate internal field and must not be preferred.
|
|
19
|
+
const modelID = typeof model.id === 'string' && model.id
|
|
20
|
+
? model.id
|
|
21
|
+
: (typeof model.modelID === 'string' ? model.modelID : '');
|
|
22
|
+
if (!providerID || !modelID) continue;
|
|
23
|
+
if (!modelsByProvider.has(providerID)) modelsByProvider.set(providerID, Object.create(null));
|
|
24
|
+
const entry = { id: modelID };
|
|
25
|
+
if (typeof model.name === 'string' && model.name) entry.name = model.name;
|
|
26
|
+
if (typeof model.family === 'string' && model.family) entry.family = model.family;
|
|
27
|
+
if (model.limit && typeof model.limit === 'object') entry.limit = model.limit;
|
|
28
|
+
if (model.capabilities) entry.capabilities = model.capabilities;
|
|
29
|
+
// v2 cost is a tier array; v2 variants are `{ id }[]`. Projection owns the safe shape.
|
|
30
|
+
if (model.cost && typeof model.cost === 'object') entry.cost = model.cost;
|
|
31
|
+
if (model.variants && typeof model.variants === 'object') entry.variants = model.variants;
|
|
32
|
+
if (typeof model.time?.released === 'number' && Number.isFinite(model.time.released)) {
|
|
33
|
+
entry.release_date = new Date(model.time.released).toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
modelsByProvider.get(providerID)[modelID] = entry;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const list = [];
|
|
39
|
+
for (const provider of providers) {
|
|
40
|
+
if (!provider || typeof provider.id !== 'string' || !provider.id) continue;
|
|
41
|
+
list.push({
|
|
42
|
+
id: provider.id,
|
|
43
|
+
name: typeof provider.name === 'string' && provider.name ? provider.name : provider.id,
|
|
44
|
+
models: modelsByProvider.get(provider.id) || Object.create(null),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const defaults = Object.create(null);
|
|
49
|
+
const defaultModelID = typeof defaultModel?.id === 'string' && defaultModel.id
|
|
50
|
+
? defaultModel.id
|
|
51
|
+
: (typeof defaultModel?.modelID === 'string' ? defaultModel.modelID : '');
|
|
52
|
+
if (defaultModel && typeof defaultModel.providerID === 'string' && defaultModelID) {
|
|
53
|
+
defaults[defaultModel.providerID] = defaultModelID;
|
|
54
|
+
}
|
|
55
|
+
return { providers: list, default: defaults };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createOpenCodeClient = ({ baseUrl, headers, fetch: fetchImpl }) =>
|
|
59
|
+
OpenCode.make({ baseUrl, headers, fetch: fetchImpl });
|
|
60
|
+
|
|
8
61
|
const MAX_GLOBAL_CONFIG_SIZE = 2 * 1024 * 1024;
|
|
9
62
|
const GLOBAL_CONFIG_FILES = {
|
|
10
63
|
opencode: ['opencode.json', 'opencode.jsonc'],
|
|
@@ -81,10 +134,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
81
134
|
const {
|
|
82
135
|
resolveProjectDirectory,
|
|
83
136
|
resolveOptionalProjectDirectory,
|
|
84
|
-
|
|
85
|
-
clientReloadDelayMs,
|
|
137
|
+
waitForOpenCodeReady,
|
|
86
138
|
getAgentSources,
|
|
87
139
|
getAgentConfig,
|
|
140
|
+
listDisabledAgentOverrides,
|
|
88
141
|
createAgent,
|
|
89
142
|
updateAgent,
|
|
90
143
|
deleteAgent,
|
|
@@ -115,25 +168,43 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
115
168
|
if (!directory) {
|
|
116
169
|
return res.status(400).json({ error });
|
|
117
170
|
}
|
|
118
|
-
|
|
171
|
+
// Gate on managed-OpenCode readiness like the generic proxy: without this
|
|
172
|
+
// the SDK hits an upstream that is still starting (or a stale orphan from
|
|
173
|
+
// a nodemon restart) and gets HTML/401 back, surfacing as a spurious 502.
|
|
174
|
+
if (typeof waitForOpenCodeReady === 'function') {
|
|
175
|
+
await waitForOpenCodeReady(20_000, 200);
|
|
176
|
+
}
|
|
177
|
+
const client = createOpenCodeClient({
|
|
119
178
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
120
|
-
directory,
|
|
121
179
|
headers: getOpenCodeAuthHeaders(),
|
|
122
|
-
|
|
180
|
+
// Forward the full init: the SDK carries Authorization in init.headers.
|
|
181
|
+
// Dropping init (the old `(request) => fetch(request, { signal })` shape)
|
|
182
|
+
// silently stripped auth and produced spurious upstream 401s.
|
|
183
|
+
fetch: (request, init) => fetch(request, { ...init, signal: AbortSignal.timeout(8_000) }),
|
|
123
184
|
});
|
|
124
|
-
const
|
|
125
|
-
|
|
185
|
+
const location = { directory };
|
|
186
|
+
const [providersResult, modelsResult, defaultResult] = await Promise.all([
|
|
187
|
+
client.provider.list({ location }),
|
|
188
|
+
client.model.list({ location }),
|
|
189
|
+
client.model.default({ location }),
|
|
190
|
+
]);
|
|
191
|
+
const source = composeV2ProviderCatalogSource(
|
|
192
|
+
providersResult?.data,
|
|
193
|
+
modelsResult?.data,
|
|
194
|
+
defaultResult?.data,
|
|
195
|
+
);
|
|
196
|
+
if (!source) {
|
|
126
197
|
console.error('Provider catalog upstream response failed');
|
|
127
198
|
return res.status(502).json({ error: 'Provider catalog is unavailable' });
|
|
128
199
|
}
|
|
129
|
-
const catalog = projectProviderCatalog(
|
|
200
|
+
const catalog = projectProviderCatalog(source);
|
|
130
201
|
if (!catalog.ok) {
|
|
131
202
|
console.error('Provider catalog upstream response is malformed');
|
|
132
203
|
return res.status(502).json({ error: 'Provider catalog is unavailable' });
|
|
133
204
|
}
|
|
134
205
|
return res.json(catalog.value);
|
|
135
|
-
} catch {
|
|
136
|
-
console.error('Provider catalog request failed');
|
|
206
|
+
} catch (catalogError) {
|
|
207
|
+
console.error('Provider catalog request failed', catalogError?.reason ? `(${catalogError.reason})` : catalogError?.message ?? catalogError);
|
|
137
208
|
return res.status(502).json({ error: 'Provider catalog is unavailable' });
|
|
138
209
|
}
|
|
139
210
|
});
|
|
@@ -191,7 +262,8 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
191
262
|
target: req.params.target,
|
|
192
263
|
fileName: target.fileName,
|
|
193
264
|
content,
|
|
194
|
-
requiresManualRestart:
|
|
265
|
+
requiresManualRestart: req.params.target !== 'opencode',
|
|
266
|
+
application: req.params.target === 'opencode' ? 'watch' : 'manual',
|
|
195
267
|
});
|
|
196
268
|
} catch (error) {
|
|
197
269
|
console.error('Failed to write global configuration:', error);
|
|
@@ -199,29 +271,9 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
199
271
|
}
|
|
200
272
|
});
|
|
201
273
|
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
// server will not serve it until the user restarts that server. We must not
|
|
206
|
-
// report a clean "reloading" success in that case, otherwise the UI silently
|
|
207
|
-
// reverts the edit to the stale value on the next refresh.
|
|
208
|
-
const buildConfigMutationResponse = (refreshResult, { liveMessage, manualRestartMessage }) => {
|
|
209
|
-
if (refreshResult && refreshResult.external) {
|
|
210
|
-
return {
|
|
211
|
-
success: true,
|
|
212
|
-
requiresReload: false,
|
|
213
|
-
requiresManualRestart: true,
|
|
214
|
-
message: manualRestartMessage,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
success: true,
|
|
220
|
-
requiresReload: true,
|
|
221
|
-
message: liveMessage,
|
|
222
|
-
reloadDelayMs: clientReloadDelayMs,
|
|
223
|
-
};
|
|
224
|
-
};
|
|
274
|
+
// A receipt confirms persistence, not runtime activation. V2 watches local
|
|
275
|
+
// configuration; domain events confirm changes without rebuilding locations.
|
|
276
|
+
const configSaved = () => ({ success: true, requiresReload: false, application: 'watch' });
|
|
225
277
|
|
|
226
278
|
const readMutationBody = (req) => (
|
|
227
279
|
req.body && typeof req.body === 'object' && !Array.isArray(req.body) ? req.body : {}
|
|
@@ -230,24 +282,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
230
282
|
const completeMcpMutation = async (res, action, name, applyChange) => {
|
|
231
283
|
applyChange();
|
|
232
284
|
|
|
233
|
-
|
|
234
|
-
await refreshOpenCodeAfterConfigChange(`mcp ${action}`);
|
|
235
|
-
return res.json({
|
|
236
|
-
success: true,
|
|
237
|
-
requiresReload: true,
|
|
238
|
-
message: `MCP server "${name}" ${action}d. Reloading interface…`,
|
|
239
|
-
reloadDelayMs: clientReloadDelayMs,
|
|
240
|
-
});
|
|
241
|
-
} catch (error) {
|
|
242
|
-
console.error(`[API:MCP ${action}] Reload failed after config write:`, error);
|
|
243
|
-
return res.json({
|
|
244
|
-
success: true,
|
|
245
|
-
requiresReload: false,
|
|
246
|
-
reloadFailed: true,
|
|
247
|
-
message: `MCP server "${name}" ${action}d, but OpenCode reload failed.`,
|
|
248
|
-
warning: error.message || 'OpenCode reload failed after the MCP configuration changed',
|
|
249
|
-
});
|
|
250
|
-
}
|
|
285
|
+
return res.json({ ...configSaved(), message: `MCP server "${name}" ${action}d.` });
|
|
251
286
|
};
|
|
252
287
|
|
|
253
288
|
app.post('/api/config/agents/metadata', async (req, res) => {
|
|
@@ -277,7 +312,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
277
312
|
sources,
|
|
278
313
|
};
|
|
279
314
|
}
|
|
280
|
-
|
|
315
|
+
const disabled = typeof listDisabledAgentOverrides === 'function'
|
|
316
|
+
? listDisabledAgentOverrides(directory)
|
|
317
|
+
: [];
|
|
318
|
+
return res.json(disabled.length > 0 ? { agents, disabled } : { agents });
|
|
281
319
|
} catch (error) {
|
|
282
320
|
console.error('Failed to get agent metadata batch:', error);
|
|
283
321
|
return res.status(500).json({ error: 'Failed to get agent configuration metadata' });
|
|
@@ -337,15 +375,11 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
337
375
|
console.log('[Server] Creating agent');
|
|
338
376
|
|
|
339
377
|
createAgent(agentName, config, directory, scope);
|
|
340
|
-
|
|
341
|
-
agentName
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
res.json(buildConfigMutationResponse(refreshResult, {
|
|
345
|
-
liveMessage: `Agent ${agentName} created successfully. Reloading interface…`,
|
|
346
|
-
manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
|
|
347
|
-
}));
|
|
378
|
+
res.json(configSaved());
|
|
348
379
|
} catch (error) {
|
|
380
|
+
if (error?.code === 'drop-confirmation') {
|
|
381
|
+
return res.status(409).json({ code: 'drop-confirmation', dropped: error.dropped, error: error.message });
|
|
382
|
+
}
|
|
349
383
|
console.error('Failed to create agent');
|
|
350
384
|
res.status(500).json({ error: error.message || 'Failed to create agent' });
|
|
351
385
|
}
|
|
@@ -363,15 +397,14 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
363
397
|
console.log('[Server] Updating agent');
|
|
364
398
|
|
|
365
399
|
updateAgent(agentName, updates, directory);
|
|
366
|
-
const refreshResult = await refreshOpenCodeAfterConfigChange('agent update');
|
|
367
400
|
|
|
368
401
|
console.log('[Server] Agent updated successfully');
|
|
369
402
|
|
|
370
|
-
res.json(
|
|
371
|
-
liveMessage: `Agent ${agentName} updated successfully. Reloading interface…`,
|
|
372
|
-
manualRestartMessage: `Agent ${agentName} saved. Restart your connected OpenCode server to apply the change.`,
|
|
373
|
-
}));
|
|
403
|
+
res.json(configSaved());
|
|
374
404
|
} catch (error) {
|
|
405
|
+
if (error?.code === 'drop-confirmation') {
|
|
406
|
+
return res.status(409).json({ code: 'drop-confirmation', dropped: error.dropped, error: error.message });
|
|
407
|
+
}
|
|
375
408
|
console.error('[Server] Failed to update agent');
|
|
376
409
|
res.status(500).json({ error: error.message || 'Failed to update agent' });
|
|
377
410
|
}
|
|
@@ -387,12 +420,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
387
420
|
|
|
388
421
|
const scope = req.body?.scope;
|
|
389
422
|
deleteAgent(agentName, directory, scope);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
res.json(buildConfigMutationResponse(refreshResult, {
|
|
393
|
-
liveMessage: `Agent ${agentName} deleted successfully. Reloading interface…`,
|
|
394
|
-
manualRestartMessage: `Agent ${agentName} deleted. Restart your connected OpenCode server to apply the change.`,
|
|
395
|
-
}));
|
|
423
|
+
res.json(configSaved());
|
|
396
424
|
} catch (error) {
|
|
397
425
|
console.error('Failed to delete agent');
|
|
398
426
|
res.status(500).json({ error: error.message || 'Failed to delete agent' });
|
|
@@ -500,14 +528,22 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
500
528
|
if (!getOpenCodePort()) {
|
|
501
529
|
return res.json({ commands: [] });
|
|
502
530
|
}
|
|
503
|
-
const client =
|
|
531
|
+
const client = createOpenCodeClient({
|
|
504
532
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
505
|
-
directory,
|
|
506
533
|
headers: getOpenCodeAuthHeaders(),
|
|
507
|
-
fetch: (request) => fetch(request, {
|
|
534
|
+
fetch: (request, init) => fetch(request, {
|
|
535
|
+
...init,
|
|
536
|
+
signal: init?.signal
|
|
537
|
+
? AbortSignal.any([init.signal, AbortSignal.timeout(8_000)])
|
|
538
|
+
: AbortSignal.timeout(8_000),
|
|
539
|
+
}),
|
|
508
540
|
});
|
|
509
|
-
const response = await client.command.list({ directory });
|
|
510
|
-
|
|
541
|
+
const response = await client.command.list({ location: { directory } });
|
|
542
|
+
if (!Array.isArray(response?.data)) {
|
|
543
|
+
console.error('Command catalog upstream response failed');
|
|
544
|
+
return res.status(502).json({ error: 'Command catalog is unavailable' });
|
|
545
|
+
}
|
|
546
|
+
const commands = response.data;
|
|
511
547
|
return res.json({
|
|
512
548
|
commands: commands
|
|
513
549
|
.filter((command) => command?.source !== 'skill')
|
|
@@ -598,16 +634,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
598
634
|
console.log('[Server] Creating command');
|
|
599
635
|
|
|
600
636
|
createCommand(commandName, config, directory, scope);
|
|
601
|
-
|
|
602
|
-
commandName
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
res.json({
|
|
606
|
-
success: true,
|
|
607
|
-
requiresReload: true,
|
|
608
|
-
message: `Command ${commandName} created successfully. Reloading interface…`,
|
|
609
|
-
reloadDelayMs: clientReloadDelayMs,
|
|
610
|
-
});
|
|
637
|
+
res.json(configSaved());
|
|
611
638
|
} catch (error) {
|
|
612
639
|
console.error('Failed to create command');
|
|
613
640
|
res.status(500).json({ error: error.message || 'Failed to create command' });
|
|
@@ -626,16 +653,10 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
626
653
|
console.log('[Server] Updating command');
|
|
627
654
|
|
|
628
655
|
updateCommand(commandName, updates, directory);
|
|
629
|
-
await refreshOpenCodeAfterConfigChange('command update');
|
|
630
656
|
|
|
631
657
|
console.log('[Server] Command updated successfully');
|
|
632
658
|
|
|
633
|
-
res.json(
|
|
634
|
-
success: true,
|
|
635
|
-
requiresReload: true,
|
|
636
|
-
message: `Command ${commandName} updated successfully. Reloading interface…`,
|
|
637
|
-
reloadDelayMs: clientReloadDelayMs,
|
|
638
|
-
});
|
|
659
|
+
res.json(configSaved());
|
|
639
660
|
} catch (error) {
|
|
640
661
|
console.error('[Server] Failed to update command');
|
|
641
662
|
res.status(500).json({ error: error.message || 'Failed to update command' });
|
|
@@ -651,14 +672,7 @@ export const registerConfigEntityRoutes = (app, dependencies) => {
|
|
|
651
672
|
}
|
|
652
673
|
|
|
653
674
|
deleteCommand(commandName, directory);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
res.json({
|
|
657
|
-
success: true,
|
|
658
|
-
requiresReload: true,
|
|
659
|
-
message: `Command ${commandName} deleted successfully. Reloading interface…`,
|
|
660
|
-
reloadDelayMs: clientReloadDelayMs,
|
|
661
|
-
});
|
|
675
|
+
res.json(configSaved());
|
|
662
676
|
} catch (error) {
|
|
663
677
|
console.error('Failed to delete command');
|
|
664
678
|
res.status(500).json({ error: error.message || 'Failed to delete command' });
|
|
@@ -5,10 +5,11 @@ import fs from 'node:fs/promises';
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
|
|
8
|
-
vi.mock('@opencode
|
|
8
|
+
vi.mock('@opencode/client', () => ({ OpenCode: { make: vi.fn() } }));
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { OpenCode } = await import('@opencode/client');
|
|
11
11
|
const { registerConfigEntityRoutes } = await import('./config-entity-routes.js');
|
|
12
|
+
const createOpencodeClient = OpenCode.make;
|
|
12
13
|
|
|
13
14
|
beforeEach(() => {
|
|
14
15
|
createOpencodeClient.mockReset();
|
|
@@ -49,7 +50,50 @@ const createDependencies = (getCommandSources, configDirectory, getAgentSources
|
|
|
49
50
|
getOpenCodePort: () => 4096,
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
describe('V2 configuration persistence', () => {
|
|
54
|
+
it.each(['agents', 'commands', 'mcp'])('%s CRUD never restarts or reloads OpenCode', async (entity) => {
|
|
55
|
+
const dependencies = createDependencies(vi.fn());
|
|
56
|
+
dependencies.refreshOpenCodeAfterConfigChange.mockRejectedValue(new Error('must not manage the service'));
|
|
57
|
+
const app = express();
|
|
58
|
+
app.use(express.json());
|
|
59
|
+
registerConfigEntityRoutes(app, dependencies);
|
|
60
|
+
for (const method of ['post', 'patch', 'delete']) {
|
|
61
|
+
const response = await request(app)[method](`/api/config/${entity}/example?directory=%2Frepo`)
|
|
62
|
+
.send({ name: 'example', scope: 'project', type: 'local', command: ['example'] });
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
expect(response.body).toMatchObject({ success: true, requiresReload: false, application: 'watch' });
|
|
65
|
+
expect(response.body.requiresManualRestart).not.toBe(true);
|
|
66
|
+
}
|
|
67
|
+
expect(dependencies.refreshOpenCodeAfterConfigChange).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
52
71
|
describe('config entity command metadata route', () => {
|
|
72
|
+
it('preserves real SDK authentication and request options when fetching the catalog', async () => {
|
|
73
|
+
const { OpenCode: realOpenCode } = await vi.importActual('@opencode/client');
|
|
74
|
+
createOpencodeClient.mockImplementation(realOpenCode.make);
|
|
75
|
+
const upstream = vi.spyOn(globalThis, 'fetch').mockImplementation(async (_input, init) => {
|
|
76
|
+
if (new Headers(init?.headers).get('Authorization') !== 'Basic example') {
|
|
77
|
+
return new Response('Unauthorized', { status: 401 });
|
|
78
|
+
}
|
|
79
|
+
return Response.json({ data: [{ name: 'review', description: 'Review changes' }] });
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
const app = express();
|
|
83
|
+
app.use(express.json());
|
|
84
|
+
registerConfigEntityRoutes(app, createDependencies(() => ({ md: { exists: false }, json: { exists: false } })));
|
|
85
|
+
const response = await request(app).post('/api/config/commands/metadata').send({ catalog: true });
|
|
86
|
+
expect(response.status).toBe(200);
|
|
87
|
+
expect(response.body.commands).toEqual([expect.objectContaining({ name: 'review' })]);
|
|
88
|
+
const [url, init] = upstream.mock.calls[0];
|
|
89
|
+
expect(new URL(url).pathname).toBe('/api/command');
|
|
90
|
+
expect(init.method).toBe('GET');
|
|
91
|
+
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
92
|
+
} finally {
|
|
93
|
+
upstream.mockRestore();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
53
97
|
it('returns metadata for many commands through one request', async () => {
|
|
54
98
|
const app = express();
|
|
55
99
|
app.use(express.json());
|
|
@@ -100,9 +144,9 @@ describe('config entity command metadata route', () => {
|
|
|
100
144
|
|
|
101
145
|
expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
|
|
102
146
|
baseUrl: 'http://opencode-upstream:4096',
|
|
103
|
-
directory: '/repo',
|
|
104
147
|
headers: { Authorization: 'Basic example' },
|
|
105
148
|
}));
|
|
149
|
+
expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
|
|
106
150
|
expect(response.body.commands).toEqual([
|
|
107
151
|
expect.objectContaining({ name: 'markdown', reference: markdownPath, description: `${'😀'.repeat(160)}…`, scope: 'project', isBuiltIn: false }),
|
|
108
152
|
expect.objectContaining({ name: 'json', reference: 'json', description: 'JSON command', scope: 'project', isBuiltIn: false }),
|
|
@@ -149,14 +193,35 @@ describe('config entity command metadata route', () => {
|
|
|
149
193
|
{ name: 'long-path', reference: 'long-path' },
|
|
150
194
|
]);
|
|
151
195
|
});
|
|
196
|
+
|
|
197
|
+
it('returns 502 when the command catalog is not an array instead of an empty success', async () => {
|
|
198
|
+
const app = express();
|
|
199
|
+
app.use(express.json());
|
|
200
|
+
createOpencodeClient.mockReturnValue({
|
|
201
|
+
command: { list: vi.fn(async () => ({ data: { commands: [] } })) },
|
|
202
|
+
});
|
|
203
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
204
|
+
|
|
205
|
+
const response = await request(app)
|
|
206
|
+
.post('/api/config/commands/metadata?directory=%2Frepo')
|
|
207
|
+
.send({ catalog: true })
|
|
208
|
+
.expect(502);
|
|
209
|
+
expect(response.body).toEqual({ error: 'Command catalog is unavailable' });
|
|
210
|
+
});
|
|
152
211
|
});
|
|
153
212
|
|
|
154
213
|
describe('provider catalog route', () => {
|
|
155
214
|
it('uses the resolved directory, OpenCode auth, SDK providers call, and explicit catalog route', async () => {
|
|
156
215
|
const app = express();
|
|
157
216
|
const resolveProjectDirectory = vi.fn(async () => ({ directory: '/project/from-header' }));
|
|
158
|
-
const
|
|
159
|
-
|
|
217
|
+
const location = { directory: '/project/from-header' };
|
|
218
|
+
const providers = vi.fn(async () => ({ data: [] }));
|
|
219
|
+
const models = vi.fn(async () => ({ data: [] }));
|
|
220
|
+
const modelDefault = vi.fn(async () => ({ data: null }));
|
|
221
|
+
createOpencodeClient.mockReturnValue({
|
|
222
|
+
provider: { list: providers },
|
|
223
|
+
model: { list: models, default: modelDefault },
|
|
224
|
+
});
|
|
160
225
|
registerConfigEntityRoutes(app, {
|
|
161
226
|
...createDependencies(vi.fn()),
|
|
162
227
|
resolveProjectDirectory,
|
|
@@ -171,22 +236,94 @@ describe('provider catalog route', () => {
|
|
|
171
236
|
expect(resolveProjectDirectory).toHaveBeenCalledWith(expect.objectContaining({ query: { directory: '/repo' } }));
|
|
172
237
|
expect(createOpencodeClient).toHaveBeenCalledWith(expect.objectContaining({
|
|
173
238
|
baseUrl: 'http://opencode-upstream:4096',
|
|
174
|
-
directory: '/project/from-header',
|
|
175
239
|
headers: { Authorization: 'Basic example' },
|
|
176
240
|
fetch: expect.any(Function),
|
|
177
241
|
}));
|
|
178
|
-
expect(
|
|
242
|
+
expect(createOpencodeClient.mock.calls[0][0].directory).toBeUndefined();
|
|
243
|
+
expect(providers).toHaveBeenCalledWith({ location });
|
|
244
|
+
expect(models).toHaveBeenCalledWith({ location });
|
|
245
|
+
expect(modelDefault).toHaveBeenCalledWith({ location });
|
|
179
246
|
expect(response.body).toEqual({ schemaVersion: 1, providers: [], default: {}, partial: false });
|
|
180
247
|
});
|
|
181
248
|
|
|
249
|
+
it('projects ModelInfo.id as the catalog model id and default', async () => {
|
|
250
|
+
const app = express();
|
|
251
|
+
createOpencodeClient.mockReturnValue({
|
|
252
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
253
|
+
model: {
|
|
254
|
+
list: vi.fn(async () => ({
|
|
255
|
+
data: [{
|
|
256
|
+
id: 'gpt-5.4-mini',
|
|
257
|
+
modelID: 'internal-pack',
|
|
258
|
+
providerID: 'openai',
|
|
259
|
+
name: 'GPT-5.4 Mini',
|
|
260
|
+
}],
|
|
261
|
+
})),
|
|
262
|
+
default: vi.fn(async () => ({
|
|
263
|
+
data: { id: 'gpt-5.4-mini', modelID: 'internal-pack', providerID: 'openai', name: 'GPT-5.4 Mini' },
|
|
264
|
+
})),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
268
|
+
|
|
269
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
270
|
+
expect(response.body.providers).toEqual([{
|
|
271
|
+
id: 'openai',
|
|
272
|
+
name: 'OpenAI',
|
|
273
|
+
models: { 'gpt-5.4-mini': { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' } },
|
|
274
|
+
}]);
|
|
275
|
+
expect(response.body.default).toEqual({ openai: 'gpt-5.4-mini' });
|
|
276
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('projects v2 thinking variants into the provider catalog and strips request payloads', async () => {
|
|
280
|
+
const app = express();
|
|
281
|
+
createOpencodeClient.mockReturnValue({
|
|
282
|
+
provider: { list: vi.fn(async () => ({ data: [{ id: 'openai', name: 'OpenAI' }] })) },
|
|
283
|
+
model: {
|
|
284
|
+
list: vi.fn(async () => ({
|
|
285
|
+
data: [{
|
|
286
|
+
id: 'gpt-5.4',
|
|
287
|
+
modelID: 'internal-pack',
|
|
288
|
+
providerID: 'openai',
|
|
289
|
+
name: 'GPT 5.4',
|
|
290
|
+
capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
|
|
291
|
+
variants: [
|
|
292
|
+
{ id: 'low', settings: { reasoningEffort: 'low', apiKey: 'variant-settings-sentinel' } },
|
|
293
|
+
{ id: 'high', headers: { Authorization: 'variant-headers-sentinel' }, body: { store: false } },
|
|
294
|
+
],
|
|
295
|
+
}],
|
|
296
|
+
})),
|
|
297
|
+
default: vi.fn(async () => ({ data: null })),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
301
|
+
|
|
302
|
+
const response = await request(app).get('/api/config/catalog/providers').expect(200);
|
|
303
|
+
expect(response.body.providers[0].models['gpt-5.4'].variants).toEqual({ low: {}, high: {} });
|
|
304
|
+
expect(response.body.providers[0].models['gpt-5.4'].capabilities).toEqual({
|
|
305
|
+
toolcall: true,
|
|
306
|
+
input: { text: true, image: true },
|
|
307
|
+
output: { text: true },
|
|
308
|
+
});
|
|
309
|
+
expect(JSON.stringify(response.body)).not.toContain('sentinel');
|
|
310
|
+
expect(JSON.stringify(response.body)).not.toContain('internal-pack');
|
|
311
|
+
});
|
|
312
|
+
|
|
182
313
|
it('returns 502 for SDK failure and malformed catalog responses', async () => {
|
|
183
314
|
const app = express();
|
|
184
|
-
createOpencodeClient.mockReturnValue({
|
|
315
|
+
createOpencodeClient.mockReturnValue({
|
|
316
|
+
provider: { list: vi.fn(async () => ({ data: {} })) },
|
|
317
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
318
|
+
});
|
|
185
319
|
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
186
320
|
await request(app).get('/api/config/catalog/providers').expect(502);
|
|
187
321
|
|
|
188
322
|
const failingApp = express();
|
|
189
|
-
createOpencodeClient.mockReturnValue({
|
|
323
|
+
createOpencodeClient.mockReturnValue({
|
|
324
|
+
provider: { list: vi.fn(async () => { throw new Error('upstream unavailable'); }) },
|
|
325
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
326
|
+
});
|
|
190
327
|
registerConfigEntityRoutes(failingApp, createDependencies(vi.fn()));
|
|
191
328
|
await request(failingApp).get('/api/config/catalog/providers').expect(502);
|
|
192
329
|
});
|
|
@@ -194,7 +331,8 @@ describe('provider catalog route', () => {
|
|
|
194
331
|
it('returns 502 for SDK error envelopes that include data', async () => {
|
|
195
332
|
const app = express();
|
|
196
333
|
createOpencodeClient.mockReturnValue({
|
|
197
|
-
|
|
334
|
+
provider: { list: vi.fn(async () => { throw new Error('upstream sentinel'); }) },
|
|
335
|
+
model: { list: vi.fn(async () => ({ data: [] })), default: vi.fn(async () => ({ data: null })) },
|
|
198
336
|
});
|
|
199
337
|
registerConfigEntityRoutes(app, createDependencies(vi.fn()));
|
|
200
338
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { OpenCode } from '@opencode/client';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { once } from 'node:events';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// Opt-in, isolated real V2 server. No user config, credentials, model calls,
|
|
11
|
+
// shared service registration, or npm plugin installs are used.
|
|
12
|
+
test.skipIf(!process.env.OPENCHAMBER_TEST_OPENCODE_BINARY)('V2 applies credentials, global config and plugin changes in the same process', async () => {
|
|
13
|
+
const temporary = path.join(os.tmpdir(), 'opencode');
|
|
14
|
+
await fs.mkdir(temporary, { recursive: true });
|
|
15
|
+
const root = await fs.mkdtemp(path.join(temporary, 'config-hot-reload-'));
|
|
16
|
+
const config = path.join(root, 'config', 'opencode');
|
|
17
|
+
const directory = path.join(root, 'project');
|
|
18
|
+
await fs.mkdir(config, { recursive: true });
|
|
19
|
+
await fs.mkdir(directory, { recursive: true });
|
|
20
|
+
const password = randomBytes(24).toString('hex');
|
|
21
|
+
const child = spawn(process.env.OPENCHAMBER_TEST_OPENCODE_BINARY, ['serve', '--hostname', '127.0.0.1', '--port', '0'], {
|
|
22
|
+
cwd: directory,
|
|
23
|
+
env: {
|
|
24
|
+
PATH: process.env.PATH,
|
|
25
|
+
HOME: root,
|
|
26
|
+
TMPDIR: temporary,
|
|
27
|
+
XDG_CONFIG_HOME: path.join(root, 'config'),
|
|
28
|
+
XDG_DATA_HOME: path.join(root, 'data'),
|
|
29
|
+
XDG_STATE_HOME: path.join(root, 'state'),
|
|
30
|
+
XDG_CACHE_HOME: path.join(root, 'cache'),
|
|
31
|
+
OPENCODE_CONFIG_DIR: config,
|
|
32
|
+
OPENCODE_SERVER_PASSWORD: password,
|
|
33
|
+
OPENCODE_DISABLE_MODELS_FETCH: '1',
|
|
34
|
+
},
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
let output = '';
|
|
38
|
+
let spawnError;
|
|
39
|
+
child.on('error', (error) => { spawnError = error; });
|
|
40
|
+
child.stdout.on('data', (data) => { output = (output + data).slice(-8192); });
|
|
41
|
+
child.stderr.on('data', () => undefined);
|
|
42
|
+
const closed = once(child, 'close').catch(() => undefined);
|
|
43
|
+
const until = async (read, accepts, label) => {
|
|
44
|
+
const deadline = Date.now() + 20_000;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
if (spawnError) throw spawnError;
|
|
47
|
+
if (child.exitCode !== null) throw new Error(`Isolated OpenCode exited during ${label}`);
|
|
48
|
+
const value = await read();
|
|
49
|
+
if (accepts(value)) return value;
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Timed out waiting for ${label}`);
|
|
53
|
+
};
|
|
54
|
+
const write = async (info) => {
|
|
55
|
+
const file = path.join(config, 'opencode.json');
|
|
56
|
+
await fs.writeFile(`${file}.tmp`, JSON.stringify(info));
|
|
57
|
+
await fs.rename(`${file}.tmp`, file);
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const origin = await until(
|
|
61
|
+
async () => output.match(/(?:server listening on|opencode server listening on) (http:\/\/[^\s]+)/)?.[1],
|
|
62
|
+
Boolean, 'listener',
|
|
63
|
+
);
|
|
64
|
+
const client = OpenCode.make({
|
|
65
|
+
baseUrl: origin,
|
|
66
|
+
headers: { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString('base64')}` },
|
|
67
|
+
fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(15_000) }),
|
|
68
|
+
});
|
|
69
|
+
const location = { directory };
|
|
70
|
+
await client.command.list({ location });
|
|
71
|
+
const pid = child.pid;
|
|
72
|
+
const commands = { 'hot-reload-probe': { template: 'Do not execute anything.', description: 'one' } };
|
|
73
|
+
await write({ commands });
|
|
74
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'global command create');
|
|
75
|
+
await write({ commands: { 'hot-reload-probe': { ...commands['hot-reload-probe'], description: 'two' } } });
|
|
76
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe' && entry.description === 'two'), 'command update');
|
|
77
|
+
|
|
78
|
+
await write({ commands, plugins: ['*', '-opencode.config.command'] });
|
|
79
|
+
await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin disable');
|
|
80
|
+
await write({ commands });
|
|
81
|
+
await until(() => client.command.list({ location }), (result) => result.data.some((entry) => entry.name === 'hot-reload-probe'), 'plugin re-enable');
|
|
82
|
+
|
|
83
|
+
await client.integration.connect.key({ integrationID: 'anthropic', key: 'isolated-test-placeholder', location });
|
|
84
|
+
const integration = await until(() => client.integration.get({ integrationID: 'anthropic', location }),
|
|
85
|
+
(result) => result.data.connections.some((connection) => connection.type === 'credential'), 'credential connection');
|
|
86
|
+
for (const connection of integration.data.connections) {
|
|
87
|
+
if (connection.type === 'credential') await client.credential.remove({ credentialID: connection.id });
|
|
88
|
+
}
|
|
89
|
+
await until(() => client.integration.get({ integrationID: 'anthropic', location }),
|
|
90
|
+
(result) => result.data.connections.length === 0, 'credential removal');
|
|
91
|
+
await write({ commands, mcp: { servers: { 'hot-reload-mcp': { type: 'local', command: ['unused-test-command'], disabled: true } } } });
|
|
92
|
+
await until(() => client.mcp.list({ location }), (result) => result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config add');
|
|
93
|
+
await write({});
|
|
94
|
+
await until(() => client.mcp.list({ location }), (result) => !result.data.some((server) => server.name === 'hot-reload-mcp'), 'MCP config removal');
|
|
95
|
+
await until(() => client.command.list({ location }), (result) => !result.data.some((entry) => entry.name === 'hot-reload-probe'), 'command removal');
|
|
96
|
+
expect(child.pid).toBe(pid);
|
|
97
|
+
expect(child.exitCode).toBeNull();
|
|
98
|
+
} finally {
|
|
99
|
+
child.kill('SIGTERM');
|
|
100
|
+
const kill = setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
101
|
+
await closed;
|
|
102
|
+
clearTimeout(kill);
|
|
103
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}, 120_000);
|