@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,239 @@
1
+ /**
2
+ * src/server/routes/obsidian.mjs
3
+ *
4
+ * v2 — Backward-compatible rewrite. When `.bizar/memory.json` exists, this
5
+ * router delegates to memory-store.mjs. When memory is not configured, it falls
6
+ * back to the original obsidian-store.mjs behaviour.
7
+ *
8
+ * CRITICAL: Preserve legacy response shapes exactly. Do not change the JSON
9
+ * structure returned by any endpoint.
10
+ */
11
+
12
+ import { Router } from 'express';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
17
+
18
+ // Lazy imports so circular deps are avoided and we only load memory-store.mjs
19
+ // when it is actually needed.
20
+ let _memoryStore = null;
21
+ let _obsidianStore = null;
22
+ let _wrap = null;
23
+
24
+ async function getMemoryStore() {
25
+ if (!_memoryStore) {
26
+ _memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) => m);
27
+ }
28
+ return _memoryStore;
29
+ }
30
+
31
+ async function getObsidianStore() {
32
+ if (!_obsidianStore) {
33
+ _obsidianStore = await import(`${SERVER_ROOT}/obsidian-store.mjs`).then((m) => m);
34
+ }
35
+ return _obsidianStore;
36
+ }
37
+
38
+ async function getShared() {
39
+ if (!_wrap) {
40
+ const mod = await import('./_shared.mjs').then((m) => m);
41
+ _wrap = mod.wrap;
42
+ }
43
+ return { wrap: _wrap };
44
+ }
45
+
46
+ /**
47
+ * Returns true if the project has opted into the memory service via
48
+ * `.bizar/memory.json`.
49
+ */
50
+ async function hasMemoryConfig(projectRoot) {
51
+ const { safeReadJSON } = await import(`${SERVER_ROOT}/yaml.mjs`).then((m) => m);
52
+ // Use safeReadJSON from yaml.mjs is expensive — use simpler check
53
+ const { existsSync } = await import('node:fs');
54
+ const configPath = join(projectRoot, '.bizar', 'memory.json');
55
+ return existsSync(configPath);
56
+ }
57
+
58
+ export async function createObsidianRouter({ projectRoot }) {
59
+ const router = Router();
60
+ const { wrap } = await getShared();
61
+ const memoryEnabled = await hasMemoryConfig(projectRoot);
62
+
63
+ if (memoryEnabled) {
64
+ // ── Memory service is active — delegate to memory-store.mjs ──────────────
65
+ const ms = await getMemoryStore();
66
+
67
+ // GET /api/obsidian — vault stats
68
+ router.get('/obsidian', wrap(async (_req, res) => {
69
+ const { vaultRoot, mode } = ms.resolveVault(projectRoot);
70
+ const { existsSync, readdirSync, statSync, readFileSync } = await import('node:fs');
71
+ if (!existsSync(vaultRoot)) {
72
+ res.json({ exists: false, vaultDir: vaultRoot, noteCount: 0, totalSize: 0 });
73
+ return;
74
+ }
75
+ const notes = ms.listNotes(projectRoot);
76
+ const totalSize = notes.reduce((acc, n) => acc + n.size, 0);
77
+ const folders = new Set(notes.map((n) => n.relPath.split('/')[0]));
78
+ res.json({
79
+ exists: true,
80
+ vaultDir: vaultRoot,
81
+ noteCount: notes.length,
82
+ totalSize,
83
+ folderCount: folders.size,
84
+ folders: [...folders].sort(),
85
+ lastModified: notes[0]?.mtime || null,
86
+ });
87
+ }));
88
+
89
+ // POST /api/obsidian — init vault
90
+ router.post('/obsidian', wrap(async (_req, res) => {
91
+ const result = ms.initVault(projectRoot);
92
+ res.json({ vaultDir: result.vaultRoot, created: result.created });
93
+ }));
94
+
95
+ // GET /api/obsidian/notes — legacy shape: { notes: [{ path, relPath, mtime, size }] }
96
+ router.get('/obsidian/notes', wrap(async (_req, res) => {
97
+ const notes = ms.listNotes(projectRoot);
98
+ // Return legacy shape
99
+ const legacyNotes = notes.map((n) => ({
100
+ path: n.relPath,
101
+ relPath: n.relPath,
102
+ mtime: n.mtime,
103
+ size: n.size,
104
+ }));
105
+ res.json({ notes: legacyNotes });
106
+ }));
107
+
108
+ // POST /api/obsidian/notes — write note
109
+ // CRITICAL: returns 201 with FULL note shape { relPath, frontmatter, body, raw, mtime, size }
110
+ router.post('/obsidian/notes', wrap(async (req, res) => {
111
+ const { path: relPath, frontmatter, body } = req.body || {};
112
+ if (!relPath || typeof relPath !== 'string') {
113
+ res.status(400).json({ error: 'bad_request', message: 'path is required' });
114
+ return;
115
+ }
116
+ if (!relPath.endsWith('.md')) {
117
+ res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
118
+ return;
119
+ }
120
+ try {
121
+ const note = ms.writeNote(projectRoot, relPath, { frontmatter: frontmatter || {}, body: body || '' });
122
+ // Return FULL note shape (legacy behavior)
123
+ res.status(201).json(note);
124
+ } catch (err) {
125
+ res.status(400).json({ error: 'bad_request', message: err.message });
126
+ }
127
+ }));
128
+
129
+ // GET /api/obsidian/notes/:path — single note
130
+ router.get('/obsidian/notes/*', wrap(async (req, res) => {
131
+ const relPath = req.params[0];
132
+ const note = ms.readNote(projectRoot, relPath);
133
+ if (!note) {
134
+ res.status(404).json({ error: 'not_found' });
135
+ return;
136
+ }
137
+ res.json(note);
138
+ }));
139
+
140
+ // DELETE /api/obsidian/notes/:path
141
+ router.delete('/obsidian/notes/*', wrap(async (req, res) => {
142
+ const relPath = req.params[0];
143
+ const ok = ms.deleteNote(projectRoot, relPath);
144
+ if (!ok) {
145
+ res.status(404).json({ error: 'not_found' });
146
+ return;
147
+ }
148
+ res.status(204).end();
149
+ }));
150
+
151
+ // GET /api/obsidian/search?q=...
152
+ router.get('/obsidian/search', wrap(async (req, res) => {
153
+ const q = req.query.q || '';
154
+ const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
155
+ const results = ms.searchVault(projectRoot, q, { limit });
156
+ res.json({ query: q, results });
157
+ }));
158
+
159
+ // POST /api/obsidian/index — rebuild index
160
+ router.post('/obsidian/index', wrap(async (_req, res) => {
161
+ // No-op in memory mode (INDEX.md is not maintained)
162
+ res.json({ ok: true, indexPath: null });
163
+ }));
164
+
165
+ } else {
166
+ // ── Legacy obsidian-store.mjs behaviour ───────────────────────────────────
167
+ const os = await getObsidianStore();
168
+
169
+ router.get('/obsidian', wrap(async (_req, res) => {
170
+ res.json(os.vaultStats(projectRoot));
171
+ }));
172
+
173
+ router.post('/obsidian', wrap(async (_req, res) => {
174
+ const result = os.initObsidianVault(projectRoot);
175
+ res.json(result);
176
+ }));
177
+
178
+ router.get('/obsidian/notes', wrap(async (_req, res) => {
179
+ const notes = os.listVaultNotes(projectRoot);
180
+ res.json({ notes });
181
+ }));
182
+
183
+ router.post('/obsidian/notes', wrap(async (req, res) => {
184
+ const { path: relPath, frontmatter, body } = req.body || {};
185
+ if (!relPath || typeof relPath !== 'string') {
186
+ res.status(400).json({ error: 'bad_request', message: 'path is required' });
187
+ return;
188
+ }
189
+ if (!relPath.endsWith('.md')) {
190
+ res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
191
+ return;
192
+ }
193
+ try {
194
+ const note = os.writeVaultNote(projectRoot, relPath, { frontmatter, body });
195
+ res.status(201).json(note);
196
+ } catch (err) {
197
+ res.status(400).json({ error: 'bad_request', message: err.message });
198
+ }
199
+ }));
200
+
201
+ router.get('/obsidian/notes/*', wrap(async (req, res) => {
202
+ const relPath = req.params[0];
203
+ const note = os.readVaultNote(projectRoot, relPath);
204
+ if (!note) {
205
+ res.status(404).json({ error: 'not_found' });
206
+ return;
207
+ }
208
+ res.json(note);
209
+ }));
210
+
211
+ router.delete('/obsidian/notes/*', wrap(async (req, res) => {
212
+ const relPath = req.params[0];
213
+ const ok = os.deleteVaultNote(projectRoot, relPath);
214
+ if (!ok) {
215
+ res.status(404).json({ error: 'not_found' });
216
+ return;
217
+ }
218
+ res.status(204).end();
219
+ }));
220
+
221
+ router.get('/obsidian/search', wrap(async (req, res) => {
222
+ const q = req.query.q;
223
+ const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
224
+ const results = os.searchVault(projectRoot, q, { limit });
225
+ res.json({ query: q, results });
226
+ }));
227
+
228
+ router.post('/obsidian/index', wrap(async (_req, res) => {
229
+ const indexPath = os.rebuildIndex(projectRoot);
230
+ if (!indexPath) {
231
+ res.status(404).json({ error: 'not_initialized' });
232
+ return;
233
+ }
234
+ res.json({ ok: true, indexPath });
235
+ }));
236
+ }
237
+
238
+ return router;
239
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * src/server/routes/opencode-sessions.mjs
3
+ *
4
+ * /api/opencode-sessions — reads active sessions from the opencode
5
+ * SQLite database (~/.local/share/opencode/opencode.db) and returns
6
+ * them in a format compatible with the dashboard's session list.
7
+ *
8
+ * Sessions are filtered to non-archived (time_archived IS NULL) and
9
+ * ordered by most recently updated.
10
+ */
11
+ import { Router } from 'express';
12
+ import Database from 'better-sqlite3';
13
+ import { join } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { wrap } from './_shared.mjs';
16
+
17
+ const DB_PATH = join(homedir(), '.local', 'share', 'opencode', 'opencode.db');
18
+
19
+ /**
20
+ * @returns {import('express').Router}
21
+ */
22
+ export function createOpencodeSessionsRouter() {
23
+ const router = Router();
24
+
25
+ router.get('/opencode-sessions', wrap(async (_req, res) => {
26
+ let db;
27
+ try {
28
+ db = new Database(DB_PATH, { readonly: true, fileMustExist: true });
29
+ const rows = db.prepare(`
30
+ SELECT id, title, project_id, time_created, time_updated
31
+ FROM session
32
+ WHERE time_archived IS NULL
33
+ ORDER BY time_updated DESC
34
+ LIMIT 200
35
+ `).all();
36
+ return res.json({
37
+ sessions: rows.map((r) => ({
38
+ id: r.id,
39
+ title: r.title,
40
+ project: r.project_id,
41
+ created: r.time_created,
42
+ updated: r.time_updated,
43
+ mtime: r.time_updated,
44
+ })),
45
+ });
46
+ } catch (err) {
47
+ // DB might not exist yet, or permissions issue — return empty list
48
+ return res.json({ sessions: [], error: err.message });
49
+ } finally {
50
+ if (db) db.close();
51
+ }
52
+ }));
53
+
54
+ return router;
55
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * src/server/routes/overview.mjs
3
+ *
4
+ * /api/snapshot — full snapshot (everything the Overview card needs)
5
+ * /api/overview — overview-only
6
+ * /api/health — { ok: true, ts }
7
+ * /api/restart (POST) — self-respawn
8
+ */
9
+ import { Router } from 'express';
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { projectsStore } from '../projects-store.mjs';
13
+ import { tasksStore } from '../tasks-store.mjs';
14
+ import { agentsStore } from '../agents-store.mjs';
15
+ import { schedulesStore } from '../schedules-store.mjs';
16
+ import { providersStore, mcpsStore } from '../providers-store.mjs';
17
+ import { modsLoader } from '../mods-loader.mjs';
18
+ import {
19
+ OPENCODE_JSON,
20
+ readSettings,
21
+ safeReadJSON,
22
+ wrap,
23
+ } from './_shared.mjs';
24
+
25
+ /**
26
+ * @param {object} deps
27
+ * @param {object} deps.state
28
+ * @returns {import('express').Router}
29
+ */
30
+ export function createOverviewRouter({ state }) {
31
+ const router = Router();
32
+
33
+ router.get('/snapshot', wrap(async (_req, res) => {
34
+ res.json(buildSnapshot());
35
+ }));
36
+
37
+ router.get('/overview', wrap(async (_req, res) => {
38
+ res.json(state.getOverview());
39
+ }));
40
+
41
+ router.get('/health', (_req, res) => res.json({ ok: true, ts: Date.now() }));
42
+
43
+ // v3.5.3 — Self-respawn: starts a new dashboard process in the
44
+ // background and exits this one. The BIZAR_AUTO_RESPAWN=1 env var
45
+ // marks it as an automated restart (vs a user-initiated one).
46
+ router.post('/restart', (req, res) => {
47
+ res.json({ restarting: true });
48
+ setTimeout(() => {
49
+ const child = spawn(
50
+ process.execPath,
51
+ [process.argv[1], ...process.argv.slice(2)],
52
+ {
53
+ detached: true,
54
+ stdio: 'ignore',
55
+ env: { ...process.env, BIZAR_AUTO_RESPAWN: '1' },
56
+ },
57
+ );
58
+ child.on('error', (err) => {
59
+ console.error('[dashboard] restart spawn failed:', err.message);
60
+ });
61
+ child.unref();
62
+ process.exit(0);
63
+ }, 500);
64
+ });
65
+
66
+ function buildSnapshot() {
67
+ const cfg = safeReadJSON(OPENCODE_JSON, null);
68
+ const active = projectsStore.active();
69
+ return {
70
+ overview: state.getOverview(),
71
+ agents: agentsStore.list(),
72
+ artifacts: state.getArtifacts(),
73
+ projects: projectsStore.list().projects,
74
+ activeProject: active,
75
+ config: {
76
+ path: OPENCODE_JSON,
77
+ data: cfg,
78
+ raw: cfg === null ? '' : JSON.stringify(cfg, null, 2),
79
+ exists: existsSync(OPENCODE_JSON),
80
+ },
81
+ settings: readSettings(),
82
+ tasks: active ? tasksStore.loadTasks(active.id) : [],
83
+ mods: modsLoader.list(),
84
+ schedules: active ? schedulesStore.list(active.id) : [],
85
+ providers: providersStore.list(),
86
+ mcps: mcpsStore.list(),
87
+ };
88
+ }
89
+
90
+ return router;
91
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * src/server/routes/pair.mjs
3
+ *
4
+ * Companion-app pairing (v3.5.2).
5
+ *
6
+ * /api/pair/start (POST) — mint a short-lived token, return QR payload
7
+ * /api/pair/verify (GET) — companion confirms a token before persisting
8
+ * /api/pair/status (GET) — count of currently active pair tokens
9
+ *
10
+ * The pair token is unrelated to the v3.6.0 dashboard auth token;
11
+ * it's a separate short-lived bearer that's minted on demand for
12
+ * the companion QR scan flow.
13
+ */
14
+ import { Router } from 'express';
15
+ import { pairStore } from '../pair-store.mjs';
16
+ import { wrap } from './_shared.mjs';
17
+
18
+ /**
19
+ * @param {object} deps
20
+ * @param {object} deps.state
21
+ * @param {Function} deps.broadcast
22
+ * @returns {import('express').Router}
23
+ */
24
+ export function createPairRouter({ state, broadcast }) {
25
+ const router = Router();
26
+
27
+ router.post('/pair/start', wrap(async (req, res) => {
28
+ const port = req.app?.get('port') || (req.socket?.server?.address()?.port) || 4321;
29
+ const publicUrl = pairStore.detectPublicUrl(req, port);
30
+ const ttlMs = Math.max(30_000, Math.min(15 * 60 * 1000, Number(req.body?.ttlMs) || 5 * 60 * 1000));
31
+ const entry = pairStore.mint(publicUrl, ttlMs);
32
+ state.appendActivity({ kind: 'pair.start', publicUrl: entry.publicUrl, expiresAt: entry.expiresAt });
33
+ broadcast({ type: 'pair:change', publicUrl: entry.publicUrl, expiresAt: entry.expiresAt });
34
+ res.json({
35
+ token: entry.token,
36
+ qrPayload: entry.qrPayload,
37
+ publicUrl: entry.publicUrl,
38
+ expiresAt: entry.expiresAt,
39
+ });
40
+ }));
41
+
42
+ // Used by the companion right after scanning the QR to confirm the token
43
+ // works before persisting it. Returns { valid: true } on success, 401
44
+ // { error: 'expired' | 'no_token' } on failure.
45
+ router.get('/pair/verify', wrap(async (req, res) => {
46
+ const auth = req.headers.authorization || '';
47
+ if (!auth.startsWith('Bearer ')) {
48
+ res.status(401).json({ error: 'no_token', message: 'missing Authorization: Bearer' });
49
+ return;
50
+ }
51
+ const token = auth.slice(7).trim();
52
+ const entry = pairStore.verify(token);
53
+ if (!entry) {
54
+ res.status(401).json({ error: 'expired', message: 'pair token invalid or expired' });
55
+ return;
56
+ }
57
+ res.json({ valid: true, publicUrl: entry.publicUrl, expiresAt: entry.expiresAt });
58
+ }));
59
+
60
+ // Lightweight introspection — useful for the Settings card and the TUI
61
+ // to show "X tokens active". Always returns { active: <count> }.
62
+ router.get('/pair/status', wrap(async (_req, res) => {
63
+ res.json({ active: pairStore._size() });
64
+ }));
65
+
66
+ return router;
67
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * src/server/routes/projects.mjs
3
+ *
4
+ * /api/projects — list
5
+ * /api/projects (POST) — add by path
6
+ * /api/projects/:id/activate (POST) — set active
7
+ * /api/projects/:id (DELETE) — remove
8
+ * /api/projects/refresh (POST) — re-scan projects.json
9
+ * /api/projects/auto-detect (POST) — auto-add the server's cwd
10
+ * /api/projects/active/tasks — tasks for the active project
11
+ * /api/projects/active/schedules — schedules for the active project
12
+ * /api/projects/active/mods — mods for the active project
13
+ * /api/projects/active/state — state.json for the active project
14
+ */
15
+ import { Router } from 'express';
16
+ import { join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { projectsStore } from '../projects-store.mjs';
19
+ import { tasksStore } from '../tasks-store.mjs';
20
+ import { schedulesStore } from '../schedules-store.mjs';
21
+ import { readActiveProjectId, safeReadJSON, wrap, readSettings } from './_shared.mjs';
22
+ import { buildAllowedRootsFromSettings, resolveSafePath } from '../lib/path-safe.mjs';
23
+
24
+ /**
25
+ * @param {object} deps
26
+ * @param {object} deps.state
27
+ * @param {Function} deps.broadcast
28
+ * @param {string} deps.projectRoot
29
+ * @returns {import('express').Router}
30
+ */
31
+ export function createProjectsRouter({ state, broadcast, projectRoot }) {
32
+ const router = Router();
33
+
34
+ router.get('/projects', wrap(async (_req, res) => {
35
+ res.json(projectsStore.list());
36
+ }));
37
+
38
+ router.post('/projects', wrap(async (req, res) => {
39
+ const path = (req.body?.path || '').trim();
40
+ const name = req.body?.name || null;
41
+ if (!path) {
42
+ res.status(400).json({ error: 'bad_request', message: 'path is required' });
43
+ return;
44
+ }
45
+ const project = projectsStore.add(path, name);
46
+ state.appendActivity({ kind: 'project.add', id: project.id, path: project.path });
47
+ res.status(201).json(project);
48
+ }));
49
+
50
+ router.post('/projects/:id/activate', wrap(async (req, res) => {
51
+ const activated = projectsStore.activate(req.params.id);
52
+ if (!activated) {
53
+ res.status(404).json({ error: 'not_found' });
54
+ return;
55
+ }
56
+ state.appendActivity({ kind: 'project.activate', id: activated.id });
57
+ broadcast({ type: 'project:change', project: activated });
58
+ res.json(activated);
59
+ }));
60
+
61
+ router.delete('/projects/:id', wrap(async (req, res) => {
62
+ projectsStore.remove(req.params.id);
63
+ broadcast({ type: 'project:change' });
64
+ res.status(204).end();
65
+ }));
66
+
67
+ router.post('/projects/refresh', wrap(async (_req, res) => {
68
+ res.json(projectsStore.list());
69
+ }));
70
+
71
+ // v3.0.4 — Manual auto-detect trigger for the "Use current directory"
72
+ // button on the Overview. Auto-detects the server's `projectRoot` and
73
+ // returns the (possibly updated) registry. Idempotent.
74
+ router.post('/projects/auto-detect', wrap(async (_req, res) => {
75
+ const detected = projectsStore.autoDetect({ cwd: projectRoot });
76
+ if (detected) {
77
+ state.appendActivity({ kind: 'project.auto-detect', id: detected.id, path: detected.path });
78
+ broadcast({ type: 'project:change', project: detected });
79
+ }
80
+ res.json(projectsStore.list());
81
+ }));
82
+
83
+ // v3.6.0 — Scan a configured `dashboard.projectsDirectory` for
84
+ // project roots and add any newly-detected ones to the registry.
85
+ // Idempotent: already-registered paths are skipped silently.
86
+ //
87
+ // v3.11.0 — The allow-list now also includes every entry in
88
+ // `dashboard.allowedRoots` (in addition to home and
89
+ // `projectsDirectory`). The configured `projectsDirectory` is
90
+ // re-validated against the full allow-list before scanning — a
91
+ // tampered settings file can't widen the boundary.
92
+ router.post('/projects/scan', wrap(async (_req, res) => {
93
+ const settings = readSettings().data || {};
94
+ const configured = settings.dashboard?.projectsDirectory;
95
+ if (typeof configured !== 'string' || !configured.trim()) {
96
+ res.status(400).json({
97
+ error: 'bad_request',
98
+ message: 'dashboard.projectsDirectory is not configured',
99
+ });
100
+ return;
101
+ }
102
+ const allowedRoots = buildAllowedRootsFromSettings({ settings, home: homedir() });
103
+ const safeRoot = resolveSafePath(configured, allowedRoots);
104
+ if (!safeRoot) {
105
+ res.status(403).json({
106
+ error: 'forbidden',
107
+ message: 'projectsDirectory is outside the allowed roots',
108
+ });
109
+ return;
110
+ }
111
+ let result;
112
+ try {
113
+ result = await projectsStore.scanDirectory(safeRoot);
114
+ } catch (err) {
115
+ res.status(500).json({ error: 'scan_failed', message: err?.message || String(err) });
116
+ return;
117
+ }
118
+ if (result.error) {
119
+ // scanDirectory returns `{ error }` on a soft failure (bad
120
+ // root, etc.). Surface as 400 so the client can show it.
121
+ res.status(400).json({ error: 'scan_failed', message: result.error });
122
+ return;
123
+ }
124
+ // Broadcast each newly added project so connected clients refresh.
125
+ for (const p of result.added) {
126
+ state.appendActivity({ kind: 'project.scan', id: p.id, path: p.path });
127
+ broadcast({ type: 'project:change', kind: 'added', project: p });
128
+ }
129
+ res.json({
130
+ added: result.added,
131
+ skipped: result.skipped,
132
+ scanned: result.scanned,
133
+ });
134
+ }));
135
+
136
+ // ── /api/projects/active/<entity> ──────────────────────────────────────
137
+ router.get('/projects/active/tasks', wrap(async (_req, res) => {
138
+ const active = projectsStore.active();
139
+ if (!active) {
140
+ res.json({ tasks: [], projectId: null });
141
+ return;
142
+ }
143
+ res.json({ tasks: tasksStore.loadTasks(active.id), projectId: active.id });
144
+ }));
145
+
146
+ router.get('/projects/active/schedules', wrap(async (_req, res) => {
147
+ const active = projectsStore.active();
148
+ if (!active) {
149
+ res.json({ schedules: [], projectId: null });
150
+ return;
151
+ }
152
+ res.json({ schedules: schedulesStore.list(active.id), projectId: active.id });
153
+ }));
154
+
155
+ router.get('/projects/active/mods', wrap(async (_req, res) => {
156
+ const active = projectsStore.active();
157
+ if (!active) {
158
+ res.json({ mods: [], projectId: null });
159
+ return;
160
+ }
161
+ res.json({ mods: [], projectId: active.id });
162
+ }));
163
+
164
+ router.get('/projects/active/state', wrap(async (_req, res) => {
165
+ const active = projectsStore.active();
166
+ if (!active) {
167
+ res.json({ state: {}, projectId: null });
168
+ return;
169
+ }
170
+ const stateFile = join(projectsStore.projectDir(active.id), 'state.json');
171
+ res.json({ state: safeReadJSON(stateFile, {}) || {}, projectId: active.id });
172
+ }));
173
+
174
+ return router;
175
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * src/server/routes/providers.mjs
3
+ *
4
+ * /api/providers — aggregated list (opencode.json + agent frontmatter + serve HTTP)
5
+ * /api/providers/active — current default provider + model
6
+ *
7
+ * The dashboard's Overview card uses these to show what the user
8
+ * can run. The /api/config/providers surface (which CRUDs the
9
+ * registry store) lives in config.mjs.
10
+ */
11
+ import { Router } from 'express';
12
+ import { providersStore } from '../providers-store.mjs';
13
+ import { wrap } from './_shared.mjs';
14
+
15
+ /**
16
+ * @returns {import('express').Router}
17
+ */
18
+ export function createProvidersRouter() {
19
+ const router = Router();
20
+
21
+ // Surface-everything endpoint the dashboard uses to populate the
22
+ // Providers card on the Overview tab. Reads from opencode.json +
23
+ // agent frontmatter + (best-effort) the running opencode serve HTTP
24
+ // API, so the list is non-empty even on installs that don't declare
25
+ // a top-level `provider` key in opencode.json.
26
+ router.get('/providers', wrap(async (_req, res) => {
27
+ const providers = await providersStore.listAll();
28
+ res.json({ providers, count: providers.length });
29
+ }));
30
+
31
+ // Active default provider + model. null when nothing is configured.
32
+ router.get('/providers/active', wrap(async (_req, res) => {
33
+ const active = await providersStore.getActive();
34
+ res.json(active || { providerId: null, modelId: null, source: null });
35
+ }));
36
+
37
+ // v3.16.0 — Auto-detect providers from env + config. Best-effort;
38
+ // a timeout/failure on one provider does not block the others.
39
+ // Query: ?probe=0 to skip the /models probe (faster, less informative).
40
+ router.get('/providers/auto-detect', wrap(async (req, res) => {
41
+ const probe = req.query.probe !== '0';
42
+ const results = await providersStore.autoDetect({ probe });
43
+ res.json({ providers: results, count: results.length });
44
+ }));
45
+
46
+ return router;
47
+ }