@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.
Files changed (361) hide show
  1. package/README.md +83 -16
  2. package/bizar-dash/.bizar/activity.log +3 -0
  3. package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
  4. package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
  5. package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
  6. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
  7. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
  8. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
  9. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
  10. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
  11. package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
  12. package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
  13. package/bizar-dash/.bizar/graph/graph.html +307 -0
  14. package/bizar-dash/.bizar/graph/graph.json +40267 -0
  15. package/bizar-dash/.bizar/graph/manifest.json +762 -0
  16. package/bizar-dash/.graphifyignore +40 -0
  17. package/bizar-dash/.obsidian/README.md +14 -0
  18. package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
  19. package/bizar-dash/.obsidian/vault.json +7 -0
  20. package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
  21. package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
  22. package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
  23. package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
  24. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
  25. package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
  26. package/bizar-dash/CHANGELOG.md +474 -0
  27. package/bizar-dash/DESIGN.md +136 -0
  28. package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
  29. package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
  30. package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
  31. package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
  32. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
  33. package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
  34. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
  35. package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
  36. package/bizar-dash/dist/index.html +40 -0
  37. package/bizar-dash/dist/mobile.html +22 -0
  38. package/bizar-dash/plans/debug-plan/comments.json +1 -0
  39. package/bizar-dash/plans/debug-plan/meta.json +10 -0
  40. package/bizar-dash/plans/debug-plan/plan.json +174 -0
  41. package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
  42. package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
  43. package/bizar-dash/src/cli.mjs +522 -0
  44. package/bizar-dash/src/server/activity-log.mjs +174 -0
  45. package/bizar-dash/src/server/agents-store.mjs +465 -0
  46. package/bizar-dash/src/server/api.mjs +138 -0
  47. package/bizar-dash/src/server/artifact-template.html +810 -0
  48. package/bizar-dash/src/server/artifacts-store.mjs +733 -0
  49. package/bizar-dash/src/server/auth.mjs +381 -0
  50. package/bizar-dash/src/server/background-store.mjs +447 -0
  51. package/bizar-dash/src/server/bg-poller.mjs +305 -0
  52. package/bizar-dash/src/server/bg-retry.mjs +574 -0
  53. package/bizar-dash/src/server/browser.mjs +40 -0
  54. package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
  55. package/bizar-dash/src/server/dialog-poller.mjs +105 -0
  56. package/bizar-dash/src/server/dialog-store.mjs +177 -0
  57. package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
  58. package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
  59. package/bizar-dash/src/server/memory-git.mjs +329 -0
  60. package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
  61. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  62. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  63. package/bizar-dash/src/server/memory-store.mjs +471 -0
  64. package/bizar-dash/src/server/mod-security.mjs +398 -0
  65. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  66. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  67. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  68. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  69. package/bizar-dash/src/server/pair-store.mjs +138 -0
  70. package/bizar-dash/src/server/projects-store.mjs +364 -0
  71. package/bizar-dash/src/server/providers-store.mjs +789 -0
  72. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  73. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  74. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  75. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  76. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  77. package/bizar-dash/src/server/routes/background.mjs +165 -0
  78. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  79. package/bizar-dash/src/server/routes/config.mjs +129 -0
  80. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  81. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  82. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  83. package/bizar-dash/src/server/routes/history.mjs +78 -0
  84. package/bizar-dash/src/server/routes/memory.mjs +658 -0
  85. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  86. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  87. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  88. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  89. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  90. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  91. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  92. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  93. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  94. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  95. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  96. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  97. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  98. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  99. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  100. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  101. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  102. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  103. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  104. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  105. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  106. package/bizar-dash/src/server/search-store.mjs +220 -0
  107. package/bizar-dash/src/server/serve-info.mjs +684 -0
  108. package/bizar-dash/src/server/server.mjs +796 -0
  109. package/bizar-dash/src/server/settings-store.mjs +45 -0
  110. package/bizar-dash/src/server/skills-store.mjs +300 -0
  111. package/bizar-dash/src/server/state.mjs +437 -0
  112. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  113. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  114. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  115. package/bizar-dash/src/server/tui.mjs +923 -0
  116. package/bizar-dash/src/server/update-store.mjs +168 -0
  117. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  118. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  119. package/bizar-dash/src/server/watcher.mjs +81 -0
  120. package/bizar-dash/src/server/yaml.mjs +238 -0
  121. package/bizar-dash/src/web/App.tsx +718 -0
  122. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  123. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  124. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  125. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  126. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  127. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  128. package/bizar-dash/src/web/components/Button.tsx +56 -0
  129. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  130. package/bizar-dash/src/web/components/Card.tsx +40 -0
  131. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  132. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  133. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  134. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  135. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  136. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  137. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  138. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  139. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  140. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  141. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  142. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  143. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  144. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  145. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  146. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  147. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  148. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  149. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  150. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  151. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  152. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  153. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  154. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  155. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  156. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  157. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  158. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  159. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  160. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  161. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  162. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  163. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  164. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  165. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  166. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  167. package/bizar-dash/src/web/index.html +37 -0
  168. package/bizar-dash/src/web/lib/api.ts +193 -0
  169. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  170. package/bizar-dash/src/web/lib/types.ts +668 -0
  171. package/bizar-dash/src/web/lib/utils.ts +114 -0
  172. package/bizar-dash/src/web/lib/ws.ts +176 -0
  173. package/bizar-dash/src/web/main.tsx +34 -0
  174. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  175. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  176. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  177. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  178. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  179. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  180. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  181. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  183. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  184. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  185. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  186. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  188. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  189. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  190. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  194. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  195. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  196. package/bizar-dash/src/web/mobile.html +20 -0
  197. package/bizar-dash/src/web/mobile.tsx +13 -0
  198. package/bizar-dash/src/web/styles/chat.css +198 -0
  199. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  200. package/bizar-dash/src/web/styles/main.css +8855 -0
  201. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  202. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  203. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  204. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  205. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  206. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  207. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  208. package/bizar-dash/src/web/views/Config.tsx +2063 -0
  209. package/bizar-dash/src/web/views/History.tsx +335 -0
  210. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  211. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  212. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  213. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  214. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  215. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  216. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  217. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  218. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  219. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  220. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  221. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  222. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  223. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  224. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  225. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  226. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  227. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  228. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  229. package/bizar-dash/tests/memory-cli.test.mjs +542 -0
  230. package/bizar-dash/tests/memory-config.test.mjs +422 -0
  231. package/bizar-dash/tests/memory-git.test.mjs +246 -0
  232. package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
  233. package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
  234. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  235. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  236. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  237. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  238. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  239. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  240. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  241. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  242. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  243. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  244. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  245. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  246. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  247. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  248. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  249. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  250. package/bizar-dash/tests/yaml.test.mjs +128 -0
  251. package/bizar-dash/tsconfig.json +23 -0
  252. package/bizar-dash/vite.config.ts +27 -0
  253. package/cli/atomic.mjs +73 -0
  254. package/cli/banner.mjs +1 -1
  255. package/cli/bin.mjs +44 -47
  256. package/cli/bootstrap.mjs +1 -1
  257. package/cli/copy.mjs +25 -16
  258. package/cli/dev-link.mjs +17 -1
  259. package/cli/doctor.mjs +4 -4
  260. package/cli/doctor.test.mjs +2 -2
  261. package/cli/init.mjs +135 -1
  262. package/cli/install.mjs +31 -39
  263. package/cli/memory-constants.mjs +59 -0
  264. package/cli/memory.mjs +1436 -0
  265. package/cli/service.mjs +5 -6
  266. package/cli/utils.mjs +6 -3
  267. package/config/AGENTS.md +7 -7
  268. package/config/agents/_shared/AGENT_BASELINE.md +59 -61
  269. package/config/opencode.json +13 -38
  270. package/config/skills/memory-protocol/SKILL.md +105 -0
  271. package/config/skills/obsidian/SKILL.md +270 -65
  272. package/install.sh +59 -19
  273. package/package.json +49 -14
  274. package/packages/sdk/LICENSE +21 -0
  275. package/packages/sdk/README.md +80 -0
  276. package/packages/sdk/src/client.ts +183 -0
  277. package/packages/sdk/src/errors.ts +120 -0
  278. package/packages/sdk/src/events.ts +153 -0
  279. package/packages/sdk/src/index.ts +63 -0
  280. package/packages/sdk/src/types.ts +176 -0
  281. package/packages/sdk/src/version.ts +4 -0
  282. package/packages/sdk/tests/client.test.ts +217 -0
  283. package/packages/sdk/tests/errors.test.ts +108 -0
  284. package/packages/sdk/tests/events.test.ts +139 -0
  285. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  286. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  287. package/packages/sdk/tsconfig.json +25 -0
  288. package/packages/sdk/vitest.config.ts +9 -0
  289. package/plugins/bizar/LICENSE +21 -0
  290. package/plugins/bizar/README.md +448 -0
  291. package/plugins/bizar/dist/index.js +29901 -0
  292. package/plugins/bizar/index.ts +1491 -0
  293. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  294. package/plugins/bizar/src/background-state.ts +551 -0
  295. package/plugins/bizar/src/background.ts +1250 -0
  296. package/plugins/bizar/src/commands-impl.ts +464 -0
  297. package/plugins/bizar/src/commands.ts +1168 -0
  298. package/plugins/bizar/src/dashboard-client.ts +233 -0
  299. package/plugins/bizar/src/event-stream.ts +606 -0
  300. package/plugins/bizar/src/fingerprint.ts +120 -0
  301. package/plugins/bizar/src/handoff.ts +79 -0
  302. package/plugins/bizar/src/http-client.ts +467 -0
  303. package/plugins/bizar/src/key-rotation.ts +218 -0
  304. package/plugins/bizar/src/logger.ts +144 -0
  305. package/plugins/bizar/src/loop.ts +176 -0
  306. package/plugins/bizar/src/opencode-runner.ts +390 -0
  307. package/plugins/bizar/src/options.ts +421 -0
  308. package/plugins/bizar/src/plan-fs.ts +323 -0
  309. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  310. package/plugins/bizar/src/report.ts +178 -0
  311. package/plugins/bizar/src/research-prompt.ts +35 -0
  312. package/plugins/bizar/src/serve-info.ts +228 -0
  313. package/plugins/bizar/src/serve.ts +496 -0
  314. package/plugins/bizar/src/settings.ts +349 -0
  315. package/plugins/bizar/src/state.ts +298 -0
  316. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  317. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  318. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  319. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  320. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  321. package/plugins/bizar/src/tools/open-kb.ts +191 -0
  322. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  323. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  324. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  325. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  326. package/plugins/bizar/tests/background-state.test.ts +277 -0
  327. package/plugins/bizar/tests/background.test.ts +402 -0
  328. package/plugins/bizar/tests/block.test.ts +195 -0
  329. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  330. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  331. package/plugins/bizar/tests/commands.test.ts +584 -0
  332. package/plugins/bizar/tests/config.test.ts +122 -0
  333. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  334. package/plugins/bizar/tests/dispose.test.ts +336 -0
  335. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  336. package/plugins/bizar/tests/event.test.ts +262 -0
  337. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  338. package/plugins/bizar/tests/http-client.test.ts +404 -0
  339. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  340. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  341. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  342. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  343. package/plugins/bizar/tests/loop.test.ts +397 -0
  344. package/plugins/bizar/tests/options.test.ts +276 -0
  345. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  346. package/plugins/bizar/tests/serve.test.ts +339 -0
  347. package/plugins/bizar/tests/settings.test.ts +351 -0
  348. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  349. package/plugins/bizar/tests/state.test.ts +276 -0
  350. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  351. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  352. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  353. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  354. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  355. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  356. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  357. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  358. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  359. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  360. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  361. 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
+ };