@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,415 @@
1
+ /**
2
+ * src/server/routes/_shared.mjs
3
+ *
4
+ * v3.6.0 — Shared utilities for the split route modules.
5
+ *
6
+ * The original api.mjs had everything in one 2,395-line file. After
7
+ * the split, this module holds the cross-route helpers: file I/O,
8
+ * settings read/write, the `wrap` async error handler, and the
9
+ * "active project id" resolver.
10
+ *
11
+ * Keeping these in one place (rather than duplicating per route file)
12
+ * means a single change here propagates to every router.
13
+ */
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
15
+ import { join, dirname, resolve as pathResolve, sep as pathSep } from 'node:path';
16
+ import { homedir } from 'node:os';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { projectsStore } from '../projects-store.mjs';
19
+
20
+ const DASH_PACKAGE_JSON = join(
21
+ dirname(fileURLToPath(import.meta.url)),
22
+ '..',
23
+ '..',
24
+ '..',
25
+ 'package.json',
26
+ );
27
+
28
+ function readDashboardVersion() {
29
+ try {
30
+ const raw = JSON.parse(readFileSync(DASH_PACKAGE_JSON, 'utf8'));
31
+ return raw?.version || '0.0.0';
32
+ } catch {
33
+ return '0.0.0';
34
+ }
35
+ }
36
+
37
+ const DASHBOARD_VERSION = readDashboardVersion();
38
+
39
+ /** Home dir, cached at module load (homedir() doesn't change mid-process). */
40
+ export const HOME = homedir();
41
+
42
+ /** Opencode config dir. Same convention used by api.mjs. */
43
+ export const OPENCODE_DIR = join(HOME, '.config', 'opencode');
44
+ export const OPENCODE_JSON = join(OPENCODE_DIR, 'opencode.json');
45
+
46
+ /** Bizar home + settings file path. */
47
+ export const BIZAR_HOME = join(HOME, '.config', 'bizar');
48
+ export const SETTINGS_FILE = join(BIZAR_HOME, 'settings.json');
49
+
50
+ /**
51
+ * Safe JSON read with a default. Catches every failure mode
52
+ * (missing file, empty file, malformed JSON, permission error) and
53
+ * returns `fallback`.
54
+ *
55
+ * @template T
56
+ * @param {string} file
57
+ * @param {T} fallback
58
+ * @returns {T | null}
59
+ */
60
+ export function safeReadJSON(file, fallback = null) {
61
+ try {
62
+ if (!existsSync(file)) return fallback;
63
+ const text = readFileSync(file, 'utf8');
64
+ if (!text.trim()) return fallback;
65
+ return JSON.parse(text);
66
+ } catch {
67
+ return fallback;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Safe text read with a fallback for unreadable / missing files.
73
+ *
74
+ * @param {string} file
75
+ * @param {string} fallback
76
+ * @returns {string}
77
+ */
78
+ export function safeReadText(file, fallback = '') {
79
+ try {
80
+ if (!existsSync(file)) return fallback;
81
+ return readFileSync(file, 'utf8');
82
+ } catch {
83
+ return fallback;
84
+ }
85
+ }
86
+
87
+ export function atomicWriteJson(filePath, data) {
88
+ const tmp = `${filePath}.tmp.${process.pid}`;
89
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
90
+ renameSync(tmp, filePath);
91
+ }
92
+
93
+ /**
94
+ * The v3 settings shape. Keep this in sync with the `DEFAULT_SETTINGS`
95
+ * constant that used to live inline in api.mjs. The frontend mirrors
96
+ * the same shape in web/lib/types.ts → Settings.
97
+ */
98
+ export const DEFAULT_SETTINGS = {
99
+ theme: {
100
+ mode: 'dark',
101
+ accent: '#8b5cf6',
102
+ success: '#3fb950',
103
+ warning: '#d29922',
104
+ error: '#f85149',
105
+ info: '#58a6ff',
106
+ fontFamily: 'Inter',
107
+ fontSize: 14,
108
+ compactMode: false,
109
+ animations: true,
110
+ },
111
+ ui: {
112
+ layout: 'topnav',
113
+ showHeader: true,
114
+ showStatusBar: true,
115
+ defaultTab: 'overview',
116
+ },
117
+ defaultAgent: 'odin',
118
+ defaultModel: '',
119
+ notifications: { onAgentComplete: true, onPlanApproval: true },
120
+ dashboard: { autoLaunchWeb: true, projectsDirectory: '', allowedRoots: [] },
121
+ service: { enabled: true, autostart: false },
122
+ about: {
123
+ version: DASHBOARD_VERSION,
124
+ homepage: 'https://github.com/DrB0rk/BizarHarness',
125
+ license: 'MIT',
126
+ },
127
+ agents: {
128
+ maxParallel: 6,
129
+ stuckThresholdMs: 600000,
130
+ autoRestart: true,
131
+ },
132
+ systemLlm: {
133
+ enabled: true,
134
+ provider: 'opencode',
135
+ model: 'opencode/deepseek-v4-flash-free',
136
+ },
137
+ };
138
+
139
+ /**
140
+ * Merge a user-supplied settings object onto the defaults, preserving
141
+ * unknown keys but always overwriting the `about.version` so the
142
+ * package's actual version wins (defends against a user hand-editing
143
+ * settings.json to claim a different version).
144
+ *
145
+ * @param {Record<string, unknown> | null | undefined} existing
146
+ * @returns {Record<string, unknown>}
147
+ */
148
+ export function mergeSettings(existing) {
149
+ if (!existing || typeof existing !== 'object') return DEFAULT_SETTINGS;
150
+ const merged = { ...DEFAULT_SETTINGS, ...existing };
151
+ merged.theme = { ...DEFAULT_SETTINGS.theme, ...(existing.theme || {}) };
152
+ merged.ui = { ...DEFAULT_SETTINGS.ui, ...(existing.ui || {}) };
153
+ merged.notifications = {
154
+ ...DEFAULT_SETTINGS.notifications,
155
+ ...(existing.notifications || {}),
156
+ };
157
+ merged.dashboard = { ...DEFAULT_SETTINGS.dashboard, ...(existing.dashboard || {}) };
158
+ merged.service = { ...DEFAULT_SETTINGS.service, ...(existing.service || {}) };
159
+ merged.about = { ...DEFAULT_SETTINGS.about, ...(existing.about || {}) };
160
+ merged.agents = { ...DEFAULT_SETTINGS.agents, ...(existing.agents || {}) };
161
+ merged.systemLlm = { ...DEFAULT_SETTINGS.systemLlm, ...(existing.systemLlm || {}) };
162
+ // Always use the package version — never let user settings override it
163
+ merged.about.version = DEFAULT_SETTINGS.about.version;
164
+ return merged;
165
+ }
166
+
167
+ /**
168
+ * Read settings.json from disk. Returns `{ path, data, exists }`.
169
+ *
170
+ * @returns {{ path: string, data: Record<string, unknown>, exists: boolean }}
171
+ */
172
+ export function readSettings() {
173
+ const raw = safeReadJSON(SETTINGS_FILE, null);
174
+ return {
175
+ path: SETTINGS_FILE,
176
+ data: mergeSettings(raw),
177
+ exists: existsSync(SETTINGS_FILE),
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Write settings.json (merged with defaults). Returns the new
183
+ * readSettings() shape so callers can broadcast the canonical data.
184
+ *
185
+ * Validates the merged `dashboard` sub-object via
186
+ * `validateDashboardSettings` before persisting. Invalid payloads
187
+ * throw a structured `Error` (`.status = 400`, `.code`,
188
+ * `.message`) which the route's `wrap()` translates into a 400 JSON
189
+ * response.
190
+ *
191
+ * @param {Record<string, unknown>} data
192
+ * @returns {{ path: string, data: Record<string, unknown>, exists: boolean }}
193
+ */
194
+ export function writeSettings(data) {
195
+ mkdirSync(dirname(SETTINGS_FILE), { recursive: true });
196
+ const merged = mergeSettings(data);
197
+ // Validate BEFORE atomic write so a bad payload never lands on disk.
198
+ validateDashboardSettings(merged.dashboard);
199
+ atomicWriteJson(SETTINGS_FILE, merged);
200
+ return readSettings();
201
+ }
202
+
203
+ /**
204
+ * Maximum number of entries permitted in `dashboard.allowedRoots`.
205
+ * Prevents a malicious or accidental payload from widening the
206
+ * filesystem boundary to thousands of roots, which would make the
207
+ * allow-list itself a DoS vector at request time.
208
+ */
209
+ const MAX_ALLOWED_ROOTS = 50;
210
+
211
+ /**
212
+ * Validate one path string in the dashboard sub-object. Returns the
213
+ * trimmed path if it is safe, or throws a structured Error.
214
+ *
215
+ * Rules applied:
216
+ * - must be a non-empty string
217
+ * - must NOT contain NUL (`\0`)
218
+ * - must NOT contain a backslash (POSIX paths have no backslash;
219
+ * a backslash is a strong signal of a path-escape attempt)
220
+ * - must be an absolute path (starts with `/` on POSIX)
221
+ * - must resolve under `os.homedir()` — the dashboard's filesystem
222
+ * boundary. A configured path that escapes home is rejected
223
+ * outright so a typo can't silently widen the boundary.
224
+ *
225
+ * @param {unknown} value
226
+ * @param {string} fieldName — used in the error message
227
+ * @returns {string} — the trimmed path
228
+ */
229
+ function validateDashboardPath(value, fieldName) {
230
+ if (typeof value !== 'string') {
231
+ const err = new Error(`${fieldName} must be a string`);
232
+ err.status = 400;
233
+ err.code = 'bad_request';
234
+ throw err;
235
+ }
236
+ const trimmed = value.trim();
237
+ if (!trimmed) {
238
+ // Empty is allowed for `projectsDirectory` (means "unset");
239
+ // higher-level validation already decided whether empty is OK
240
+ // for the calling field. Here we just hand back the trimmed
241
+ // empty string so the caller can short-circuit.
242
+ return trimmed;
243
+ }
244
+ if (trimmed.includes('\0')) {
245
+ const err = new Error(`${fieldName} must not contain NUL bytes`);
246
+ err.status = 400;
247
+ err.code = 'bad_request';
248
+ throw err;
249
+ }
250
+ if (trimmed.includes('\\')) {
251
+ const err = new Error(`${fieldName} must not contain backslashes`);
252
+ err.status = 400;
253
+ err.code = 'bad_request';
254
+ throw err;
255
+ }
256
+ if (!trimmed.startsWith('/')) {
257
+ const err = new Error(`${fieldName} must be an absolute path (start with /)`);
258
+ err.status = 400;
259
+ err.code = 'bad_request';
260
+ throw err;
261
+ }
262
+ const home = homedir();
263
+ const homeResolved = pathResolve(home);
264
+ const candidateResolved = pathResolve(trimmed);
265
+ if (candidateResolved !== homeResolved &&
266
+ !candidateResolved.startsWith(homeResolved + pathSep)) {
267
+ const err = new Error(
268
+ `${fieldName} must live under the user's home directory (${home})`,
269
+ );
270
+ err.status = 400;
271
+ err.code = 'bad_request';
272
+ throw err;
273
+ }
274
+ return trimmed;
275
+ }
276
+
277
+ /**
278
+ * Validate the `dashboard.*` sub-object on PUT /api/settings. Throws
279
+ * a structured Error (`.status = 400`, `.code = 'bad_request'`,
280
+ * `.message`) when the payload is unsafe. Returns normally on
281
+ * success.
282
+ *
283
+ * Rules:
284
+ * - `dashboard.projectsDirectory` must be empty, OR a non-empty
285
+ * string path that is absolute and lives under `os.homedir()`.
286
+ * Rationale: the projects scanner is gated to home, so a
287
+ * `projectsDirectory` that escapes home would be silently
288
+ * unusable; rejecting at write time surfaces the mistake
289
+ * instead of letting it rot in settings.json.
290
+ * - `dashboard.allowedRoots` must be an array of strings. Each
291
+ * entry must be an absolute path under `os.homedir()`. An empty
292
+ * array is allowed (means "no extras"). Entries that fail
293
+ * validation are rejected here (NOT silently dropped) so the
294
+ * operator sees the bad value. Runtime allow-list building
295
+ * (`buildAllowedRootsFromSettings`) drops silently for the
296
+ * "tampered-settings" defense-in-depth path.
297
+ * - No path may contain NUL bytes or backslashes (POSIX-path
298
+ * escape attempts).
299
+ * - The total number of allowedRoots entries is capped at
300
+ * `MAX_ALLOWED_ROOTS` to keep the per-request allow-list
301
+ * bounded.
302
+ *
303
+ * @param {Record<string, unknown> | null | undefined} dashboard
304
+ */
305
+ export function validateDashboardSettings(dashboard) {
306
+ if (dashboard == null) return;
307
+ if (typeof dashboard !== 'object' || Array.isArray(dashboard)) {
308
+ const err = new Error('dashboard settings must be an object');
309
+ err.status = 400;
310
+ err.code = 'bad_request';
311
+ throw err;
312
+ }
313
+
314
+ // projectsDirectory
315
+ const proj = dashboard.projectsDirectory;
316
+ if (proj !== undefined && proj !== null && proj !== '') {
317
+ validateDashboardPath(proj, 'dashboard.projectsDirectory');
318
+ } else if (proj !== undefined && proj !== null && typeof proj !== 'string') {
319
+ const err = new Error('dashboard.projectsDirectory must be a string');
320
+ err.status = 400;
321
+ err.code = 'bad_request';
322
+ throw err;
323
+ }
324
+
325
+ // allowedRoots
326
+ const roots = dashboard.allowedRoots;
327
+ if (roots === undefined || roots === null) {
328
+ // Field omitted — mergeSettings fills in the default `[]`. Fine.
329
+ } else if (!Array.isArray(roots)) {
330
+ const err = new Error('dashboard.allowedRoots must be an array');
331
+ err.status = 400;
332
+ err.code = 'bad_request';
333
+ throw err;
334
+ } else {
335
+ if (roots.length > MAX_ALLOWED_ROOTS) {
336
+ const err = new Error(
337
+ `dashboard.allowedRoots may not exceed ${MAX_ALLOWED_ROOTS} entries`,
338
+ );
339
+ err.status = 400;
340
+ err.code = 'bad_request';
341
+ throw err;
342
+ }
343
+ roots.forEach((entry, i) => {
344
+ validateDashboardPath(entry, `dashboard.allowedRoots[${i}]`);
345
+ });
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Tiny frontmatter parser. Used by the mods routes that need to
351
+ * read SKILL.md / command / agent YAML frontmatter. We only support
352
+ * scalar values (string / number) — no nested objects — because the
353
+ * dashboard never edits frontmatter, only displays it.
354
+ *
355
+ * @param {string} raw
356
+ * @returns {{ frontmatter: Record<string, string>, body: string }}
357
+ */
358
+ export function parseFrontmatter(raw) {
359
+ if (!raw.startsWith('---')) return { frontmatter: {}, body: raw };
360
+ const end = raw.indexOf('\n---', 3);
361
+ if (end === -1) return { frontmatter: {}, body: raw };
362
+ const fmBlock = raw.slice(3, end).trim();
363
+ const body = raw.slice(end + 4).replace(/^\s+/, '');
364
+ const frontmatter = {};
365
+ for (const line of fmBlock.split(/\r?\n/)) {
366
+ const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
367
+ if (!m) continue;
368
+ const key = m[1];
369
+ let val = m[2].trim();
370
+ if (
371
+ (val.startsWith('"') && val.endsWith('"')) ||
372
+ (val.startsWith("'") && val.endsWith("'"))
373
+ ) {
374
+ val = val.slice(1, -1);
375
+ }
376
+ frontmatter[key] = val;
377
+ }
378
+ return { frontmatter, body };
379
+ }
380
+
381
+ /**
382
+ * Resolve the current active project id (or null if none). This is
383
+ * the per-request default that task/schedule/state lookups fall back
384
+ * to when the client didn't pass an explicit projectId.
385
+ *
386
+ * @returns {string | null}
387
+ */
388
+ export function readActiveProjectId() {
389
+ return projectsStore.active()?.id || null;
390
+ }
391
+
392
+ /**
393
+ * Async-error wrapper used by every route handler. A throw inside the
394
+ * handler becomes a JSON `{ error, message }` response with the
395
+ * right status, instead of crashing the server.
396
+ *
397
+ * Usage: `router.get('/foo', wrap(async (req, res) => { ... }))`
398
+ *
399
+ * @template T
400
+ * @param {(req: import('express').Request, res: import('express').Response, next: import('express').NextFunction) => Promise<T>} fn
401
+ * @returns {import('express').RequestHandler}
402
+ */
403
+ export function wrap(fn) {
404
+ return async (req, res, ...rest) => {
405
+ try {
406
+ await fn(req, res, ...rest);
407
+ } catch (err) {
408
+ const status = err?.status || 500;
409
+ res.status(status).json({
410
+ error: err?.code || 'internal_error',
411
+ message: err?.message || String(err),
412
+ });
413
+ }
414
+ };
415
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * src/server/routes/activity.mjs
3
+ *
4
+ * /api/activity — full activity log (newest first)
5
+ * /api/activity/hidden — list of hidden event keys (for overview hide)
6
+ * /api/activity/hide — POST { keys: [...] } add to hidden
7
+ * /api/activity/hide — DELETE clear all hidden
8
+ * /api/activity/hide/:key — DELETE unhide one
9
+ *
10
+ * "Hidden" events are NOT deleted — they're just excluded from the
11
+ * Overview feed. The full log is still queryable at /api/activity.
12
+ *
13
+ * Storage: ~/.cache/bizar/activity-hidden.json
14
+ * Shape: { hidden: string[] }
15
+ *
16
+ * Event key = sha1(kind|ts|slug) — stable across reloads because it's
17
+ * derived from event content, not a synthetic id.
18
+ */
19
+ import { Router } from 'express';
20
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { homedir } from 'node:os';
23
+ import { createHash } from 'node:crypto';
24
+ import { wrap } from './_shared.mjs';
25
+
26
+ const HIDDEN_PATH = join(homedir(), '.cache', 'bizar', 'activity-hidden.json');
27
+
28
+ function readHidden() {
29
+ if (!existsSync(HIDDEN_PATH)) return { hidden: [] };
30
+ try {
31
+ return JSON.parse(readFileSync(HIDDEN_PATH, 'utf8'));
32
+ } catch {
33
+ return { hidden: [] };
34
+ }
35
+ }
36
+
37
+ function writeHidden(data) {
38
+ mkdirSync(join(homedir(), '.cache', 'bizar'), { recursive: true });
39
+ writeFileSync(HIDDEN_PATH, JSON.stringify(data, null, 2), 'utf8');
40
+ }
41
+
42
+ /** Compute a stable key for an activity event. */
43
+ export function activityKey(item) {
44
+ const h = createHash('sha1');
45
+ h.update(String(item.kind || ''));
46
+ h.update('|');
47
+ h.update(String(item.ts || ''));
48
+ h.update('|');
49
+ h.update(String(item.slug || item.title || ''));
50
+ return h.digest('hex').slice(0, 16);
51
+ }
52
+
53
+ /**
54
+ * @param {object} deps
55
+ * @param {object} deps.state
56
+ * @returns {import('express').Router}
57
+ */
58
+ export function createActivityRouter({ state } = {}) {
59
+ const router = Router();
60
+
61
+ // GET /activity — full log (newest first)
62
+ router.get('/activity', wrap(async (_req, res) => {
63
+ if (!state || typeof state.getOverview !== 'function') {
64
+ res.status(503).json({ error: 'unavailable', message: 'state not ready' });
65
+ return;
66
+ }
67
+ const overview = state.getOverview();
68
+ const items = Array.isArray(overview.recentActivity)
69
+ ? [...overview.recentActivity]
70
+ : [];
71
+ items.sort((a, b) => String(b.ts).localeCompare(String(a.ts)));
72
+ res.json({ items, total: items.length });
73
+ }));
74
+
75
+ // GET /activity/hidden — list of hidden event keys
76
+ router.get('/activity/hidden', wrap(async (_req, res) => {
77
+ res.json(readHidden());
78
+ }));
79
+
80
+ // POST /activity/hide — add keys to hidden list
81
+ router.post('/activity/hide', wrap(async (req, res) => {
82
+ const keys = Array.isArray(req.body?.keys) ? req.body.keys : [];
83
+ if (keys.length === 0) {
84
+ res.status(400).json({ error: 'bad_request', message: 'keys[] required' });
85
+ return;
86
+ }
87
+ const data = readHidden();
88
+ const set = new Set(data.hidden);
89
+ for (const k of keys) if (typeof k === 'string') set.add(k);
90
+ data.hidden = Array.from(set);
91
+ writeHidden(data);
92
+ res.json(data);
93
+ }));
94
+
95
+ // DELETE /activity/hide — clear all hidden
96
+ router.delete('/activity/hide', wrap(async (_req, res) => {
97
+ writeHidden({ hidden: [] });
98
+ res.json({ hidden: [] });
99
+ }));
100
+
101
+ // DELETE /activity/hide/:key — unhide one
102
+ router.delete('/activity/hide/:key', wrap(async (req, res) => {
103
+ const data = readHidden();
104
+ data.hidden = data.hidden.filter((k) => k !== req.params.key);
105
+ writeHidden(data);
106
+ res.json(data);
107
+ }));
108
+
109
+ return router;
110
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * src/server/routes/agents.mjs
3
+ *
4
+ * /api/agents — list
5
+ * /api/agents/stuck — agents exceeding the stuck threshold
6
+ * /api/agents/hierarchy — parent/child tree
7
+ * /api/agents/:name — single agent metadata
8
+ * /api/agents (POST) — create
9
+ * /api/agents/:name (PUT) — update
10
+ * /api/agents/:name/invoke (POST) — log an invocation intent
11
+ * /api/agents/:name/status (POST) — update status (idle|working|error|stuck)
12
+ * /api/agents/:name/heartbeat (POST) — bump last-heartbeat
13
+ * /api/agents/:name/restart (POST) — restart
14
+ * /api/agents/:name (DELETE) — remove
15
+ *
16
+ * Express-ordering notes:
17
+ * - /agents/stuck MUST come before /agents/:name (else "stuck"
18
+ * gets captured as a name).
19
+ * - /agents/hierarchy MUST come before /agents/:name (same reason).
20
+ */
21
+ import { Router } from 'express';
22
+ import { agentsStore, buildHierarchyTree } from '../agents-store.mjs';
23
+ import { wrap } from './_shared.mjs';
24
+
25
+ /**
26
+ * @param {object} deps
27
+ * @param {object} deps.state
28
+ * @param {Function} deps.broadcast
29
+ * @returns {import('express').Router}
30
+ */
31
+ export function createAgentsRouter({ state, broadcast }) {
32
+ const router = Router();
33
+
34
+ router.get('/agents', wrap(async (_req, res) => {
35
+ res.json({ agents: agentsStore.list() });
36
+ }));
37
+
38
+ // v3.1.0 — /api/agents/stuck must be defined BEFORE the /:name
39
+ // catch-all or Express will treat "stuck" as an agent name.
40
+ router.get('/agents/stuck', wrap(async (_req, res) => {
41
+ res.json({ stuck: agentsStore.stuck() });
42
+ }));
43
+
44
+ // v3.2.0 — Agent hierarchy. Must be defined BEFORE /agents/:name
45
+ // or Express will treat "hierarchy" as a name.
46
+ router.get('/agents/hierarchy', wrap(async (_req, res) => {
47
+ const agents = agentsStore.list();
48
+ const tree = buildHierarchyTree(agents);
49
+ res.json(tree);
50
+ }));
51
+
52
+ router.get('/agents/:name', wrap(async (req, res) => {
53
+ const agent = agentsStore.get(req.params.name);
54
+ if (!agent) {
55
+ res.status(404).json({ error: 'not_found' });
56
+ return;
57
+ }
58
+ res.json(agent);
59
+ }));
60
+
61
+ router.post('/agents', wrap(async (req, res) => {
62
+ const agent = agentsStore.create(req.body || {});
63
+ broadcast({ type: 'agents:change' });
64
+ res.status(201).json(agent);
65
+ }));
66
+
67
+ router.put('/agents/:name', wrap(async (req, res) => {
68
+ const agent = agentsStore.update(req.params.name, req.body || {});
69
+ broadcast({ type: 'agents:change' });
70
+ res.json(agent);
71
+ }));
72
+
73
+ router.post('/agents/:name/invoke', wrap(async (req, res) => {
74
+ const name = req.params.name;
75
+ const prompt = (req.body && req.body.prompt) || '';
76
+ if (!prompt.trim()) {
77
+ res.status(400).json({ error: 'bad_request', message: 'prompt is required' });
78
+ return;
79
+ }
80
+ state.appendActivity({ kind: 'agent.invoke', agent: name, prompt: String(prompt).slice(0, 500) });
81
+ res.status(202).json({ accepted: true, agent: name });
82
+ }));
83
+
84
+ // v3.1.0 — Agent status (idle / working / error / stuck). The opencode
85
+ // plugin pings this when it picks up or finishes a task; the dashboard
86
+ // also calls it on the lifecycle hooks below.
87
+ router.post('/agents/:name/status', wrap(async (req, res) => {
88
+ const name = req.params.name;
89
+ const { status, currentTaskId } = req.body || {};
90
+ const valid = ['idle', 'working', 'error', 'stuck'];
91
+ if (status && !valid.includes(status)) {
92
+ res.status(400).json({ error: 'bad_request', message: `invalid status (use: ${valid.join(', ')})` });
93
+ return;
94
+ }
95
+ const agent = agentsStore.updateStatus(name, status || 'idle', currentTaskId ?? null);
96
+ if (!agent) {
97
+ res.status(404).json({ error: 'not_found' });
98
+ return;
99
+ }
100
+ broadcast({ type: 'agent:status', agent });
101
+ res.json(agent);
102
+ }));
103
+
104
+ router.post('/agents/:name/heartbeat', wrap(async (req, res) => {
105
+ const agent = agentsStore.heartbeat(req.params.name);
106
+ if (!agent) {
107
+ res.status(404).json({ error: 'not_found' });
108
+ return;
109
+ }
110
+ res.json(agent);
111
+ }));
112
+
113
+ router.post('/agents/:name/restart', wrap(async (req, res) => {
114
+ const agent = agentsStore.restart(req.params.name);
115
+ if (!agent) {
116
+ res.status(404).json({ error: 'not_found' });
117
+ return;
118
+ }
119
+ broadcast({ type: 'agent:restarted', agent });
120
+ res.json(agent);
121
+ }));
122
+
123
+ router.delete('/agents/:name', wrap(async (req, res) => {
124
+ const ok = agentsStore.delete(req.params.name);
125
+ if (!ok) {
126
+ res.status(404).json({ error: 'not_found' });
127
+ return;
128
+ }
129
+ broadcast({ type: 'agents:change' });
130
+ res.status(204).end();
131
+ }));
132
+
133
+ return router;
134
+ }