@polderlabs/bizar 3.23.1 → 4.2.0
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 +83 -16
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +329 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +471 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +658 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +2063 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +246 -0
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +44 -47
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +25 -16
- package/cli/dev-link.mjs +17 -1
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +31 -39
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +1436 -0
- package/cli/service.mjs +5 -6
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +270 -65
- package/install.sh +59 -19
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1491 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1168 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +584 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/providers-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.0.0 — OpenCode providers and MCPs management.
|
|
5
|
+
*
|
|
6
|
+
* Reads / writes the opencode.json at ~/.config/opencode/opencode.json
|
|
7
|
+
* under the `provider` and `mcp` keys.
|
|
8
|
+
*
|
|
9
|
+
* v3.5.6 — `listAll()` falls back to inferring providers from agent
|
|
10
|
+
* `.md` frontmatter (the `model: provider/model-name` line) so the
|
|
11
|
+
* dashboard can surface the providers that are actually in use even
|
|
12
|
+
* when the user's opencode.json has no top-level `provider` key.
|
|
13
|
+
* Also tries the running `opencode serve` HTTP API for the canonical
|
|
14
|
+
* list. API keys are never echoed back in full — the response masks them.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
} from 'node:fs';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
|
|
27
|
+
const HOME = homedir();
|
|
28
|
+
const OPENCODE_JSON = join(HOME, '.config', 'opencode', 'opencode.json');
|
|
29
|
+
const OPENCODE_AGENTS_DIR = join(HOME, '.config', 'opencode', 'agents');
|
|
30
|
+
|
|
31
|
+
function safeReadJSON(file, fallback = {}) {
|
|
32
|
+
try {
|
|
33
|
+
if (!existsSync(file)) return fallback;
|
|
34
|
+
const text = readFileSync(file, 'utf8');
|
|
35
|
+
if (!text.trim()) return fallback;
|
|
36
|
+
return JSON.parse(text);
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Atomic JSON write: serialize to a sibling temp file, then rename into
|
|
43
|
+
// place. `rename` is atomic on POSIX (same filesystem), so a crash
|
|
44
|
+
// between write and rename never leaves a half-written / corrupt file.
|
|
45
|
+
function atomicWriteJson(filePath, data) {
|
|
46
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
47
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
48
|
+
renameSync(tmp, filePath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function loadConfig() {
|
|
52
|
+
return safeReadJSON(OPENCODE_JSON, {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function saveConfig(data) {
|
|
56
|
+
mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
|
|
57
|
+
atomicWriteJson(OPENCODE_JSON, data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mask(value) {
|
|
61
|
+
if (typeof value !== 'string' || !value) return '';
|
|
62
|
+
// Short keys (≤8 chars) get a distinct indicator so the UI can show
|
|
63
|
+
// "***short***" instead of looking like the field is empty.
|
|
64
|
+
if (value.length <= 8) return '***short***';
|
|
65
|
+
return value.slice(0, 2) + '...' + value.slice(-2);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unmask(stored, incoming) {
|
|
69
|
+
// Incoming is the user-typed value. If it's the masked form, keep stored.
|
|
70
|
+
if (typeof incoming === 'string' && incoming.startsWith('***') && incoming.endsWith('***')) {
|
|
71
|
+
return stored;
|
|
72
|
+
}
|
|
73
|
+
return incoming;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* v3.20.10 — Three-way merge for fields that may be missing from a
|
|
78
|
+
* patch object. Used by `update()` so an operator who submits
|
|
79
|
+
* `{ backupApiKey: 'new-key' }` (without re-supplying `apiKey`)
|
|
80
|
+
* doesn't accidentally clear the stored `apiKey` to undefined.
|
|
81
|
+
*
|
|
82
|
+
* stored — current on-disk value (string)
|
|
83
|
+
* incoming — patch value (string | undefined)
|
|
84
|
+
* Returns:
|
|
85
|
+
* - `stored` if `incoming` is undefined (field not in patch)
|
|
86
|
+
* - `stored` if `incoming` is the masked `***...***` placeholder
|
|
87
|
+
* - `incoming` otherwise (real value to replace)
|
|
88
|
+
*
|
|
89
|
+
* Replaces the previous `unmask(stored, patch.apiKey)` which returned
|
|
90
|
+
* undefined whenever the patch didn't include apiKey — that lost the
|
|
91
|
+
* stored key on every partial-update. Same issue applied to the new
|
|
92
|
+
* `backupApiKey` field, so the helper covers both.
|
|
93
|
+
*/
|
|
94
|
+
function preserveOrReplace(stored, incoming) {
|
|
95
|
+
if (incoming === undefined) return stored;
|
|
96
|
+
return unmask(stored, incoming);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const providersStore = {
|
|
100
|
+
OPENCODE_JSON,
|
|
101
|
+
|
|
102
|
+
list() {
|
|
103
|
+
const cfg = loadConfig();
|
|
104
|
+
const providers = cfg.provider || {};
|
|
105
|
+
return Object.entries(providers).map(([id, p]) => ({
|
|
106
|
+
id,
|
|
107
|
+
name: p.name || id,
|
|
108
|
+
baseURL: p.baseURL || p.options?.baseURL || '',
|
|
109
|
+
apiKey: mask(p.apiKey || p.options?.apiKey || ''),
|
|
110
|
+
backupApiKey: mask(p.backupApiKey || p.options?.backupApiKey || ''),
|
|
111
|
+
models: Array.isArray(p.models) ? p.models : [],
|
|
112
|
+
enabled: p.enabled !== false,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* v3.5.6 — Robust provider discovery. Tries three sources in order
|
|
118
|
+
* and merges them so the dashboard shows every provider that is
|
|
119
|
+
* actually in use, even when opencode.json has no `provider` key.
|
|
120
|
+
*
|
|
121
|
+
* 1. opencode.json `provider` / `providers` key — explicit config.
|
|
122
|
+
* 2. Agent `.md` frontmatter — every `model: provider/model` line
|
|
123
|
+
* implies a usable provider + model pair.
|
|
124
|
+
* 3. opencode serve HTTP `/api/providers` (best-effort, 1.5s timeout).
|
|
125
|
+
*
|
|
126
|
+
* Each source contributes providers; duplicates (by id) are merged so
|
|
127
|
+
* the explicit config wins on baseURL/apiKey and the inferred sources
|
|
128
|
+
* contribute their known models.
|
|
129
|
+
*
|
|
130
|
+
* Returns an array of:
|
|
131
|
+
* { id, name, baseURL, apiKey, models: [{id, name, source?}],
|
|
132
|
+
* source: 'config'|'agents'|'serve'|'config+agents' }
|
|
133
|
+
*/
|
|
134
|
+
async listAll() {
|
|
135
|
+
const byId = new Map();
|
|
136
|
+
|
|
137
|
+
const upsert = (id, patch, source) => {
|
|
138
|
+
const cur = byId.get(id) || {
|
|
139
|
+
id,
|
|
140
|
+
name: id,
|
|
141
|
+
baseURL: '',
|
|
142
|
+
apiKey: '',
|
|
143
|
+
backupApiKey: '',
|
|
144
|
+
models: [],
|
|
145
|
+
enabled: true,
|
|
146
|
+
source: '',
|
|
147
|
+
};
|
|
148
|
+
const next = { ...cur, ...patch };
|
|
149
|
+
// Merge models by id
|
|
150
|
+
const modelMap = new Map();
|
|
151
|
+
for (const m of cur.models) modelMap.set(m.id, m);
|
|
152
|
+
for (const m of patch.models || []) {
|
|
153
|
+
const existing = modelMap.get(m.id);
|
|
154
|
+
modelMap.set(m.id, existing ? { ...existing, ...m } : m);
|
|
155
|
+
}
|
|
156
|
+
next.models = Array.from(modelMap.values());
|
|
157
|
+
next.source = cur.source
|
|
158
|
+
? cur.source.includes(source)
|
|
159
|
+
? cur.source
|
|
160
|
+
: `${cur.source}+${source}`
|
|
161
|
+
: source;
|
|
162
|
+
byId.set(id, next);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Source 1: opencode.json provider/providers key
|
|
166
|
+
try {
|
|
167
|
+
const cfg = loadConfig();
|
|
168
|
+
const explicit = cfg.provider || cfg.providers || {};
|
|
169
|
+
for (const [id, p] of Object.entries(explicit)) {
|
|
170
|
+
if (!p || typeof p !== 'object') continue;
|
|
171
|
+
upsert(
|
|
172
|
+
id,
|
|
173
|
+
{
|
|
174
|
+
name: p.name || id,
|
|
175
|
+
baseURL: p.baseURL || p.options?.baseURL || '',
|
|
176
|
+
apiKey: mask(p.apiKey || p.options?.apiKey || ''),
|
|
177
|
+
backupApiKey: mask(p.backupApiKey || p.options?.backupApiKey || ''),
|
|
178
|
+
models: (Array.isArray(p.models) ? p.models : []).map((m) => ({
|
|
179
|
+
id: typeof m === 'string' ? m : m.id || m.name || String(m),
|
|
180
|
+
name: typeof m === 'string' ? m : m.name || m.id || String(m),
|
|
181
|
+
})),
|
|
182
|
+
enabled: p.enabled !== false,
|
|
183
|
+
},
|
|
184
|
+
'config',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
/* best-effort */
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Source 2: agent .md frontmatter
|
|
192
|
+
try {
|
|
193
|
+
if (existsSync(OPENCODE_AGENTS_DIR)) {
|
|
194
|
+
for (const file of readdirSync(OPENCODE_AGENTS_DIR)) {
|
|
195
|
+
if (!file.endsWith('.md')) continue;
|
|
196
|
+
const full = join(OPENCODE_AGENTS_DIR, file);
|
|
197
|
+
let raw;
|
|
198
|
+
try {
|
|
199
|
+
raw = readFileSync(full, 'utf8');
|
|
200
|
+
} catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const modelMatch = raw.match(/^model:\s*([^\s#]+)/m);
|
|
204
|
+
if (!modelMatch) continue;
|
|
205
|
+
const modelRef = modelMatch[1].trim();
|
|
206
|
+
const slashIdx = modelRef.indexOf('/');
|
|
207
|
+
if (slashIdx <= 0) continue;
|
|
208
|
+
const providerId = modelRef.slice(0, slashIdx);
|
|
209
|
+
const modelId = modelRef.slice(slashIdx + 1);
|
|
210
|
+
if (!providerId || !modelId) continue;
|
|
211
|
+
upsert(
|
|
212
|
+
providerId,
|
|
213
|
+
{
|
|
214
|
+
models: [{ id: modelId, name: modelId, source: `agents/${file.replace(/\.md$/, '')}` }],
|
|
215
|
+
},
|
|
216
|
+
'agents',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
/* best-effort */
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Source 3: opencode serve HTTP API
|
|
225
|
+
try {
|
|
226
|
+
const { readServeInfo } = await import('./serve-info.mjs');
|
|
227
|
+
const info = readServeInfo();
|
|
228
|
+
if (info && info.baseUrl) {
|
|
229
|
+
const auth = 'Basic ' + Buffer.from(`opencode:${info.password || ''}`).toString('base64');
|
|
230
|
+
const ctrl = new AbortController();
|
|
231
|
+
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
232
|
+
try {
|
|
233
|
+
const resp = await fetch(`${info.baseUrl}/api/providers`, {
|
|
234
|
+
headers: { Authorization: auth },
|
|
235
|
+
signal: ctrl.signal,
|
|
236
|
+
});
|
|
237
|
+
if (resp.ok) {
|
|
238
|
+
const body = await resp.json().catch(() => null);
|
|
239
|
+
const list = Array.isArray(body?.providers)
|
|
240
|
+
? body.providers
|
|
241
|
+
: Array.isArray(body?.data)
|
|
242
|
+
? body.data
|
|
243
|
+
: Array.isArray(body)
|
|
244
|
+
? body
|
|
245
|
+
: [];
|
|
246
|
+
for (const p of list) {
|
|
247
|
+
if (!p || typeof p !== 'object') continue;
|
|
248
|
+
const id = p.id || p.name;
|
|
249
|
+
if (!id) continue;
|
|
250
|
+
const models = (Array.isArray(p.models) ? p.models : []).map((m) => {
|
|
251
|
+
if (typeof m === 'string') return { id: m, name: m };
|
|
252
|
+
return {
|
|
253
|
+
id: m.id || m.name || String(m),
|
|
254
|
+
name: m.name || m.id || String(m),
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
upsert(
|
|
258
|
+
id,
|
|
259
|
+
{
|
|
260
|
+
name: p.name || id,
|
|
261
|
+
baseURL: p.baseURL || '',
|
|
262
|
+
models,
|
|
263
|
+
},
|
|
264
|
+
'serve',
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
/* serve unreachable — fall through */
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
/* best-effort */
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Array.from(byId.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* v3.5.6 — Return the currently configured default provider + model.
|
|
283
|
+
* Reads from opencode.json (`model`, `provider`, `small_model`).
|
|
284
|
+
* Falls back to scanning agents to find the default_agent's model.
|
|
285
|
+
*
|
|
286
|
+
* Returns: { providerId, modelId, source, agent?, smallModel? } or
|
|
287
|
+
* null if nothing can be determined.
|
|
288
|
+
*/
|
|
289
|
+
async getActive() {
|
|
290
|
+
let providerId = null;
|
|
291
|
+
let modelId = null;
|
|
292
|
+
let source = null;
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const cfg = loadConfig();
|
|
296
|
+
const m = typeof cfg.model === 'string' ? cfg.model : '';
|
|
297
|
+
if (m && m.includes('/')) {
|
|
298
|
+
const slashIdx = m.indexOf('/');
|
|
299
|
+
providerId = m.slice(0, slashIdx);
|
|
300
|
+
modelId = m.slice(slashIdx + 1);
|
|
301
|
+
source = 'opencode.json';
|
|
302
|
+
} else if (cfg.provider && typeof cfg.provider === 'object') {
|
|
303
|
+
const firstId = Object.keys(cfg.provider)[0];
|
|
304
|
+
if (firstId) {
|
|
305
|
+
providerId = firstId;
|
|
306
|
+
source = 'opencode.json:provider';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let smallModel = null;
|
|
311
|
+
if (typeof cfg.small_model === 'string' && cfg.small_model.includes('/')) {
|
|
312
|
+
smallModel = cfg.small_model;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let defaultAgent = typeof cfg.default_agent === 'string' ? cfg.default_agent : null;
|
|
316
|
+
|
|
317
|
+
// If we got a providerId but no modelId, look up the default_agent's model
|
|
318
|
+
let agentModel = null;
|
|
319
|
+
if (providerId && !modelId && defaultAgent && existsSync(OPENCODE_AGENTS_DIR)) {
|
|
320
|
+
try {
|
|
321
|
+
const file = join(OPENCODE_AGENTS_DIR, `${defaultAgent}.md`);
|
|
322
|
+
if (existsSync(file)) {
|
|
323
|
+
const raw = readFileSync(file, 'utf8');
|
|
324
|
+
const match = raw.match(/^model:\s*([^\s#]+)/m);
|
|
325
|
+
if (match) {
|
|
326
|
+
agentModel = match[1].trim();
|
|
327
|
+
if (agentModel.includes('/')) {
|
|
328
|
+
const slashIdx = agentModel.indexOf('/');
|
|
329
|
+
const inferredProvider = agentModel.slice(0, slashIdx);
|
|
330
|
+
if (!providerId) providerId = inferredProvider;
|
|
331
|
+
modelId = agentModel.slice(slashIdx + 1);
|
|
332
|
+
source = `agents/${defaultAgent}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
/* best-effort */
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (providerId) {
|
|
342
|
+
return {
|
|
343
|
+
providerId,
|
|
344
|
+
modelId: modelId || null,
|
|
345
|
+
source,
|
|
346
|
+
agent: defaultAgent || null,
|
|
347
|
+
smallModel,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
/* best-effort */
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Last-resort: scan all agents, pick the most-referenced provider/model
|
|
355
|
+
try {
|
|
356
|
+
if (existsSync(OPENCODE_AGENTS_DIR)) {
|
|
357
|
+
const counts = new Map(); // 'provider/model' -> count
|
|
358
|
+
for (const file of readdirSync(OPENCODE_AGENTS_DIR)) {
|
|
359
|
+
if (!file.endsWith('.md')) continue;
|
|
360
|
+
try {
|
|
361
|
+
const raw = readFileSync(join(OPENCODE_AGENTS_DIR, file), 'utf8');
|
|
362
|
+
const m = raw.match(/^model:\s*([^\s#]+)/m);
|
|
363
|
+
if (m) {
|
|
364
|
+
const ref = m[1].trim();
|
|
365
|
+
counts.set(ref, (counts.get(ref) || 0) + 1);
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
/* skip */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (counts.size > 0) {
|
|
372
|
+
const top = Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0];
|
|
373
|
+
const [ref] = top;
|
|
374
|
+
if (ref.includes('/')) {
|
|
375
|
+
const slashIdx = ref.indexOf('/');
|
|
376
|
+
return {
|
|
377
|
+
providerId: ref.slice(0, slashIdx),
|
|
378
|
+
modelId: ref.slice(slashIdx + 1),
|
|
379
|
+
source: 'agents:inferred',
|
|
380
|
+
agent: null,
|
|
381
|
+
smallModel: null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
/* best-effort */
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return null;
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
get(id) {
|
|
394
|
+
return this.list().find((p) => p.id === id) || null;
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
add(input) {
|
|
398
|
+
if (!input || typeof input !== 'object') throw new Error('input required');
|
|
399
|
+
// Auto-generate id from name if not provided
|
|
400
|
+
const id = input.id || (input.name ? input.name.toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/^-+|-+$/g, '').replace(/-+/g, '-') : `provider_${Date.now().toString(36)}`);
|
|
401
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(id)) {
|
|
402
|
+
throw new Error('invalid id');
|
|
403
|
+
}
|
|
404
|
+
const cfg = loadConfig();
|
|
405
|
+
cfg.provider = cfg.provider || {};
|
|
406
|
+
if (cfg.provider[id]) throw new Error(`provider "${id}" exists`);
|
|
407
|
+
cfg.provider[id] = {
|
|
408
|
+
name: input.name || id,
|
|
409
|
+
baseURL: input.baseURL || '',
|
|
410
|
+
apiKey: input.apiKey || '',
|
|
411
|
+
// v3.20.10 — backup key slot. Optional. Stored verbatim; masked
|
|
412
|
+
// by list() / listAll(). If both keys are present, the operator
|
|
413
|
+
// can swap them manually via the dashboard if the primary hits
|
|
414
|
+
// a rate limit or quota.
|
|
415
|
+
backupApiKey: input.backupApiKey || '',
|
|
416
|
+
models: Array.isArray(input.models) ? input.models : [],
|
|
417
|
+
enabled: input.enabled !== false,
|
|
418
|
+
};
|
|
419
|
+
saveConfig(cfg);
|
|
420
|
+
return this.get(id);
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
update(id, patch) {
|
|
424
|
+
if (!patch || typeof patch !== 'object') throw new Error('patch required');
|
|
425
|
+
const cfg = loadConfig();
|
|
426
|
+
cfg.provider = cfg.provider || {};
|
|
427
|
+
const cur = cfg.provider[id];
|
|
428
|
+
if (!cur) throw new Error(`provider "${id}" not found`);
|
|
429
|
+
cfg.provider[id] = {
|
|
430
|
+
...cur,
|
|
431
|
+
name: patch.name ?? cur.name,
|
|
432
|
+
baseURL: patch.baseURL ?? cur.baseURL,
|
|
433
|
+
// v3.20.10 — preserveOrReplace: don't lose the stored key when
|
|
434
|
+
// the patch omits the field. See preserveOrReplace() above.
|
|
435
|
+
apiKey: preserveOrReplace(cur.apiKey, patch.apiKey),
|
|
436
|
+
backupApiKey: preserveOrReplace(cur.backupApiKey, patch.backupApiKey),
|
|
437
|
+
models: Array.isArray(patch.models) ? patch.models : cur.models,
|
|
438
|
+
enabled: patch.enabled ?? cur.enabled,
|
|
439
|
+
};
|
|
440
|
+
saveConfig(cfg);
|
|
441
|
+
return this.get(id);
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
remove(id) {
|
|
445
|
+
const cfg = loadConfig();
|
|
446
|
+
if (!cfg.provider || !cfg.provider[id]) return false;
|
|
447
|
+
delete cfg.provider[id];
|
|
448
|
+
saveConfig(cfg);
|
|
449
|
+
return true;
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* v3.16.0 — Auto-detect providers from environment variables.
|
|
454
|
+
*
|
|
455
|
+
* Recognised API keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,
|
|
456
|
+
* GEMINI_API_KEY / GOOGLE_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY,
|
|
457
|
+
* COHERE_API_KEY, OPENROUTER_API_KEY, DEEPSEEK_API_KEY,
|
|
458
|
+
* MINIMAX_API_KEY.
|
|
459
|
+
*
|
|
460
|
+
* v3.20.10 — Backup keys: every KNOWN_PROVIDER also accepts a
|
|
461
|
+
* `<NAME>_BACKUP_API_KEY` env var (e.g. `MINIMAX_API_KEY_BACKUP`)
|
|
462
|
+
* and a `backupApiKey` field in the config. autoDetect() returns
|
|
463
|
+
* both, and the dashboard UI surfaces them in the Providers page
|
|
464
|
+
* so the operator knows "if the primary hits a rate limit, swap in
|
|
465
|
+
* the backup manually" without having to dig through shell history.
|
|
466
|
+
*
|
|
467
|
+
* Detection order (highest priority first):
|
|
468
|
+
* 1. config.provider.<id>.apiKey
|
|
469
|
+
* 2. config.provider.<id>.backupApiKey
|
|
470
|
+
* 3. process.env[envKey] (primary)
|
|
471
|
+
* 4. process.env[backupEnvKey] (backup)
|
|
472
|
+
*
|
|
473
|
+
* Each entry has a status:
|
|
474
|
+
* - 'configured' — key present AND format checks out
|
|
475
|
+
* - 'unknown' — env var set but format doesn't match known patterns
|
|
476
|
+
* - 'no-key' — provider known but no key in env or config
|
|
477
|
+
*
|
|
478
|
+
* Probes the canonical /models endpoint with a 1.5s timeout to confirm
|
|
479
|
+
* the key actually works. Probes are best-effort — failures don't
|
|
480
|
+
* downgrade status from 'configured' to 'no-key'.
|
|
481
|
+
*/
|
|
482
|
+
KNOWN_PROVIDERS: [
|
|
483
|
+
{
|
|
484
|
+
id: 'anthropic',
|
|
485
|
+
name: 'Anthropic',
|
|
486
|
+
envKeys: ['ANTHROPIC_API_KEY'],
|
|
487
|
+
backupEnvKeys: ['ANTHROPIC_API_KEY_BACKUP', 'ANTHROPIC_BACKUP_API_KEY'],
|
|
488
|
+
baseURL: 'https://api.anthropic.com/v1',
|
|
489
|
+
keyPattern: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'openai',
|
|
493
|
+
name: 'OpenAI',
|
|
494
|
+
envKeys: ['OPENAI_API_KEY'],
|
|
495
|
+
backupEnvKeys: ['OPENAI_API_KEY_BACKUP', 'OPENAI_BACKUP_API_KEY'],
|
|
496
|
+
baseURL: 'https://api.openai.com/v1',
|
|
497
|
+
keyPattern: /^sk-[A-Za-z0-9]{20,}$/,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 'google',
|
|
501
|
+
name: 'Google AI',
|
|
502
|
+
envKeys: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
503
|
+
backupEnvKeys: ['GEMINI_API_KEY_BACKUP', 'GOOGLE_API_KEY_BACKUP'],
|
|
504
|
+
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
|
|
505
|
+
keyPattern: /^AIza[A-Za-z0-9_-]{30,}$/,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'mistral',
|
|
509
|
+
name: 'Mistral',
|
|
510
|
+
envKeys: ['MISTRAL_API_KEY'],
|
|
511
|
+
backupEnvKeys: ['MISTRAL_API_KEY_BACKUP'],
|
|
512
|
+
baseURL: 'https://api.mistral.ai/v1',
|
|
513
|
+
keyPattern: /^[A-Za-z0-9]{20,}$/,
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
id: 'groq',
|
|
517
|
+
name: 'Groq',
|
|
518
|
+
envKeys: ['GROQ_API_KEY'],
|
|
519
|
+
backupEnvKeys: ['GROQ_API_KEY_BACKUP'],
|
|
520
|
+
baseURL: 'https://api.groq.com/openai/v1',
|
|
521
|
+
keyPattern: /^gsk_[A-Za-z0-9]{20,}$/,
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
id: 'cohere',
|
|
525
|
+
name: 'Cohere',
|
|
526
|
+
envKeys: ['COHERE_API_KEY'],
|
|
527
|
+
backupEnvKeys: ['COHERE_API_KEY_BACKUP'],
|
|
528
|
+
baseURL: 'https://api.cohere.com/v1',
|
|
529
|
+
keyPattern: /^[A-Za-z0-9]{20,}$/,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: 'openrouter',
|
|
533
|
+
name: 'OpenRouter',
|
|
534
|
+
envKeys: ['OPENROUTER_API_KEY'],
|
|
535
|
+
backupEnvKeys: ['OPENROUTER_API_KEY_BACKUP'],
|
|
536
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
537
|
+
keyPattern: /^sk-or-[A-Za-z0-9_-]{20,}$/,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: 'deepseek',
|
|
541
|
+
name: 'DeepSeek',
|
|
542
|
+
envKeys: ['DEEPSEEK_API_KEY'],
|
|
543
|
+
backupEnvKeys: ['DEEPSEEK_API_KEY_BACKUP'],
|
|
544
|
+
baseURL: 'https://api.deepseek.com/v1',
|
|
545
|
+
keyPattern: /^sk-[A-Za-z0-9]{20,}$/,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
// MiniMax accepts ANTHROPIC_API_KEY as a fallback because
|
|
549
|
+
// MiniMax's API is Anthropic-format-compatible and many users
|
|
550
|
+
// already have an Anthropic key configured. The backup key
|
|
551
|
+
// is the second slot — keep both as the most-resilient config.
|
|
552
|
+
id: 'minimax',
|
|
553
|
+
name: 'MiniMax',
|
|
554
|
+
envKeys: ['MINIMAX_API_KEY', 'ANTHROPIC_API_KEY'],
|
|
555
|
+
backupEnvKeys: ['MINIMAX_API_KEY_BACKUP', 'MINIMAX_BACKUP_API_KEY', 'ANTHROPIC_API_KEY_BACKUP'],
|
|
556
|
+
baseURL: 'https://api.minimax.chat/v1',
|
|
557
|
+
keyPattern: /^[A-Za-z0-9]{20,}$/,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
|
|
561
|
+
async autoDetect({ probe = true } = {}) {
|
|
562
|
+
const result = [];
|
|
563
|
+
for (const spec of this.KNOWN_PROVIDERS) {
|
|
564
|
+
const cfgInfo = (() => {
|
|
565
|
+
try {
|
|
566
|
+
const cfg = loadConfig();
|
|
567
|
+
const cfgProvider = cfg.provider?.[spec.id];
|
|
568
|
+
if (!cfgProvider) return { apiKey: '', backupApiKey: '' };
|
|
569
|
+
return {
|
|
570
|
+
apiKey: cfgProvider.apiKey || cfgProvider.options?.apiKey || '',
|
|
571
|
+
backupApiKey: cfgProvider.backupApiKey || cfgProvider.options?.backupApiKey || '',
|
|
572
|
+
};
|
|
573
|
+
} catch {
|
|
574
|
+
return { apiKey: '', backupApiKey: '' };
|
|
575
|
+
}
|
|
576
|
+
})();
|
|
577
|
+
|
|
578
|
+
// Primary: config > env
|
|
579
|
+
let apiKey = cfgInfo.apiKey;
|
|
580
|
+
let keySource = cfgInfo.apiKey ? 'config' : '';
|
|
581
|
+
if (!apiKey) {
|
|
582
|
+
for (const k of spec.envKeys) {
|
|
583
|
+
const v = process.env[k];
|
|
584
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
585
|
+
apiKey = v;
|
|
586
|
+
keySource = `env:${k}`;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Backup: config > env (independent from primary; you can have
|
|
593
|
+
// a backup key without a primary, e.g. if you rotate).
|
|
594
|
+
let backupApiKey = cfgInfo.backupApiKey;
|
|
595
|
+
let backupSource = cfgInfo.backupApiKey ? 'config' : '';
|
|
596
|
+
if (!backupApiKey) {
|
|
597
|
+
for (const k of spec.backupEnvKeys || []) {
|
|
598
|
+
const v = process.env[k];
|
|
599
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
600
|
+
backupApiKey = v;
|
|
601
|
+
backupSource = `env:${k}`;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const status = !apiKey
|
|
608
|
+
? 'no-key'
|
|
609
|
+
: spec.keyPattern && !spec.keyPattern.test(apiKey)
|
|
610
|
+
? 'unknown'
|
|
611
|
+
: 'configured';
|
|
612
|
+
const backupStatus = !backupApiKey
|
|
613
|
+
? 'no-key'
|
|
614
|
+
: spec.keyPattern && !spec.keyPattern.test(backupApiKey)
|
|
615
|
+
? 'unknown'
|
|
616
|
+
: 'configured';
|
|
617
|
+
|
|
618
|
+
// Probe primary
|
|
619
|
+
const probeResult = await this._probeKey(spec, apiKey, probe && status === 'configured');
|
|
620
|
+
const backupProbe = await this._probeKey(spec, backupApiKey, probe && backupStatus === 'configured');
|
|
621
|
+
|
|
622
|
+
result.push({
|
|
623
|
+
id: spec.id,
|
|
624
|
+
name: spec.name,
|
|
625
|
+
baseURL: spec.baseURL,
|
|
626
|
+
envKeys: spec.envKeys,
|
|
627
|
+
backupEnvKeys: spec.backupEnvKeys || [],
|
|
628
|
+
status,
|
|
629
|
+
keySource,
|
|
630
|
+
hasKey: !!apiKey,
|
|
631
|
+
probed: probeResult,
|
|
632
|
+
backup: {
|
|
633
|
+
status: backupStatus,
|
|
634
|
+
source: backupSource,
|
|
635
|
+
hasKey: !!backupApiKey,
|
|
636
|
+
probed: backupProbe,
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return result;
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* v3.20.10 — Probe a single key against the provider's /models endpoint.
|
|
645
|
+
* Returns { ok, status, modelCount? } or { ok: false, reason }.
|
|
646
|
+
* Exposed as a separate helper so primary + backup keys can be
|
|
647
|
+
* probed independently (each gets its own timeout + result).
|
|
648
|
+
*/
|
|
649
|
+
async _probeKey(spec, apiKey, shouldProbe) {
|
|
650
|
+
if (!shouldProbe || !apiKey || !spec.baseURL) return null;
|
|
651
|
+
try {
|
|
652
|
+
const ctrl = new AbortController();
|
|
653
|
+
const timer = setTimeout(() => ctrl.abort(), 1500);
|
|
654
|
+
const url = `${spec.baseURL}/models`;
|
|
655
|
+
const resp = await fetch(url, {
|
|
656
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
657
|
+
signal: ctrl.signal,
|
|
658
|
+
});
|
|
659
|
+
clearTimeout(timer);
|
|
660
|
+
const out = { ok: resp.ok, status: resp.status };
|
|
661
|
+
if (resp.ok) {
|
|
662
|
+
try {
|
|
663
|
+
const body = await resp.json();
|
|
664
|
+
if (Array.isArray(body?.data)) out.modelCount = body.data.length;
|
|
665
|
+
else if (Array.isArray(body)) out.modelCount = body.length;
|
|
666
|
+
} catch { /* ignore parse */ }
|
|
667
|
+
} else {
|
|
668
|
+
out.reason = `HTTP ${resp.status}`;
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
} catch (err) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
reason: err && err.name === 'AbortError' ? 'timeout' : 'network',
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
export const mcpsStore = {
|
|
681
|
+
OPENCODE_JSON,
|
|
682
|
+
|
|
683
|
+
list() {
|
|
684
|
+
const cfg = loadConfig();
|
|
685
|
+
const mcps = cfg.mcp || {};
|
|
686
|
+
return Object.entries(mcps).map(([id, m]) => {
|
|
687
|
+
const isRemote = m?.type === 'remote';
|
|
688
|
+
// Local MCP: command is an array in newer opencode.json. Older
|
|
689
|
+
// format had separate `command` (string) + `args` (array). Normalize.
|
|
690
|
+
const command = isRemote
|
|
691
|
+
? ''
|
|
692
|
+
: Array.isArray(m.command)
|
|
693
|
+
? m.command.join(' ')
|
|
694
|
+
: (m.command || '');
|
|
695
|
+
const args = isRemote
|
|
696
|
+
? []
|
|
697
|
+
: Array.isArray(m.command)
|
|
698
|
+
? m.command
|
|
699
|
+
: Array.isArray(m.args)
|
|
700
|
+
? m.args
|
|
701
|
+
: [];
|
|
702
|
+
return {
|
|
703
|
+
id,
|
|
704
|
+
type: isRemote ? 'remote' : 'local',
|
|
705
|
+
command,
|
|
706
|
+
args,
|
|
707
|
+
env: m.env || {},
|
|
708
|
+
url: m.url || '',
|
|
709
|
+
headers: m.headers || {},
|
|
710
|
+
oauth: !!m.oauth,
|
|
711
|
+
enabled: m.enabled !== false,
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
|
|
716
|
+
get(id) {
|
|
717
|
+
return this.list().find((m) => m.id === id) || null;
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
add(input) {
|
|
721
|
+
if (!input || !input.id) throw new Error('id is required');
|
|
722
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(input.id)) {
|
|
723
|
+
throw new Error('invalid id');
|
|
724
|
+
}
|
|
725
|
+
const cfg = loadConfig();
|
|
726
|
+
cfg.mcp = cfg.mcp || {};
|
|
727
|
+
if (cfg.mcp[input.id]) throw new Error(`mcp "${input.id}" exists`);
|
|
728
|
+
const isRemote = input.type === 'remote';
|
|
729
|
+
cfg.mcp[input.id] = isRemote
|
|
730
|
+
? {
|
|
731
|
+
type: 'remote',
|
|
732
|
+
url: input.url || '',
|
|
733
|
+
headers: input.headers || {},
|
|
734
|
+
oauth: !!input.oauth,
|
|
735
|
+
enabled: input.enabled !== false,
|
|
736
|
+
}
|
|
737
|
+
: {
|
|
738
|
+
type: 'local',
|
|
739
|
+
command: Array.isArray(input.args) && input.args.length > 0
|
|
740
|
+
? [input.command || '', ...input.args].filter(Boolean)
|
|
741
|
+
: (input.command || ''),
|
|
742
|
+
enabled: input.enabled !== false,
|
|
743
|
+
...(input.env && Object.keys(input.env).length > 0 ? { env: input.env } : {}),
|
|
744
|
+
};
|
|
745
|
+
saveConfig(cfg);
|
|
746
|
+
return this.get(input.id);
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
update(id, patch) {
|
|
750
|
+
if (!patch || typeof patch !== 'object') throw new Error('patch required');
|
|
751
|
+
const cfg = loadConfig();
|
|
752
|
+
cfg.mcp = cfg.mcp || {};
|
|
753
|
+
const cur = cfg.mcp[id];
|
|
754
|
+
if (!cur) throw new Error(`mcp "${id}" not found`);
|
|
755
|
+
const wasRemote = cur.type === 'remote';
|
|
756
|
+
const isRemote = patch.type ? patch.type === 'remote' : wasRemote;
|
|
757
|
+
if (isRemote) {
|
|
758
|
+
cfg.mcp[id] = {
|
|
759
|
+
type: 'remote',
|
|
760
|
+
url: patch.url ?? cur.url ?? '',
|
|
761
|
+
headers: patch.headers ?? cur.headers ?? {},
|
|
762
|
+
oauth: patch.oauth ?? cur.oauth ?? false,
|
|
763
|
+
enabled: patch.enabled ?? cur.enabled ?? true,
|
|
764
|
+
};
|
|
765
|
+
} else {
|
|
766
|
+
const nextCommand = Array.isArray(patch.args) && patch.args.length > 0
|
|
767
|
+
? [patch.command ?? cur.command ?? '', ...patch.args].filter(Boolean)
|
|
768
|
+
: (patch.command ?? cur.command ?? '');
|
|
769
|
+
cfg.mcp[id] = {
|
|
770
|
+
type: 'local',
|
|
771
|
+
command: nextCommand,
|
|
772
|
+
enabled: patch.enabled ?? cur.enabled ?? true,
|
|
773
|
+
...((patch.env && Object.keys(patch.env).length > 0) || cur.env
|
|
774
|
+
? { env: patch.env ?? cur.env ?? {} }
|
|
775
|
+
: {}),
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
saveConfig(cfg);
|
|
779
|
+
return this.get(id);
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
remove(id) {
|
|
783
|
+
const cfg = loadConfig();
|
|
784
|
+
if (!cfg.mcp || !cfg.mcp[id]) return false;
|
|
785
|
+
delete cfg.mcp[id];
|
|
786
|
+
saveConfig(cfg);
|
|
787
|
+
return true;
|
|
788
|
+
},
|
|
789
|
+
};
|