@polderlabs/bizar 3.23.1 → 4.0.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 (346) hide show
  1. package/README.md +72 -2
  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 +249 -0
  60. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  61. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  62. package/bizar-dash/src/server/memory-store.mjs +424 -0
  63. package/bizar-dash/src/server/mod-security.mjs +398 -0
  64. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  65. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  66. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  67. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  68. package/bizar-dash/src/server/pair-store.mjs +138 -0
  69. package/bizar-dash/src/server/projects-store.mjs +364 -0
  70. package/bizar-dash/src/server/providers-store.mjs +789 -0
  71. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  72. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  73. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  74. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  75. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  76. package/bizar-dash/src/server/routes/background.mjs +165 -0
  77. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  78. package/bizar-dash/src/server/routes/config.mjs +129 -0
  79. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  80. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  81. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  82. package/bizar-dash/src/server/routes/history.mjs +78 -0
  83. package/bizar-dash/src/server/routes/memory.mjs +499 -0
  84. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  85. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  86. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  87. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  88. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  89. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  90. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  91. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  92. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  93. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  94. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  95. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  96. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  97. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  98. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  99. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  100. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  101. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  102. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  103. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  104. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  105. package/bizar-dash/src/server/search-store.mjs +220 -0
  106. package/bizar-dash/src/server/serve-info.mjs +684 -0
  107. package/bizar-dash/src/server/server.mjs +796 -0
  108. package/bizar-dash/src/server/settings-store.mjs +45 -0
  109. package/bizar-dash/src/server/skills-store.mjs +300 -0
  110. package/bizar-dash/src/server/state.mjs +437 -0
  111. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  112. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  113. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  114. package/bizar-dash/src/server/tui.mjs +923 -0
  115. package/bizar-dash/src/server/update-store.mjs +168 -0
  116. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  117. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  118. package/bizar-dash/src/server/watcher.mjs +81 -0
  119. package/bizar-dash/src/server/yaml.mjs +238 -0
  120. package/bizar-dash/src/web/App.tsx +718 -0
  121. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  122. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  123. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  124. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  125. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  126. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  127. package/bizar-dash/src/web/components/Button.tsx +56 -0
  128. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  129. package/bizar-dash/src/web/components/Card.tsx +40 -0
  130. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  131. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  132. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  133. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  134. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  135. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  136. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  137. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  138. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  139. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  140. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  141. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  142. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  143. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  144. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  145. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  146. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  147. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  148. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  149. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  150. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  151. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  152. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  153. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  154. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  155. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  156. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  157. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  158. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  159. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  160. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  161. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  162. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  163. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  164. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  165. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  166. package/bizar-dash/src/web/index.html +37 -0
  167. package/bizar-dash/src/web/lib/api.ts +193 -0
  168. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  169. package/bizar-dash/src/web/lib/types.ts +668 -0
  170. package/bizar-dash/src/web/lib/utils.ts +114 -0
  171. package/bizar-dash/src/web/lib/ws.ts +176 -0
  172. package/bizar-dash/src/web/main.tsx +34 -0
  173. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  174. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  175. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  176. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  177. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  178. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  179. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  180. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  181. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  183. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  184. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  185. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  186. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  188. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  189. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  190. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  194. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  195. package/bizar-dash/src/web/mobile.html +20 -0
  196. package/bizar-dash/src/web/mobile.tsx +13 -0
  197. package/bizar-dash/src/web/styles/chat.css +198 -0
  198. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  199. package/bizar-dash/src/web/styles/main.css +8855 -0
  200. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  201. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  202. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  203. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  204. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  205. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  206. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  207. package/bizar-dash/src/web/views/Config.tsx +1603 -0
  208. package/bizar-dash/src/web/views/History.tsx +335 -0
  209. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  210. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  211. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  212. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  213. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  214. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  215. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  216. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  217. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  218. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  219. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  220. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  221. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  222. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  223. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  224. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  225. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  226. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  227. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  228. package/bizar-dash/tests/memory-git.test.mjs +138 -0
  229. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  230. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  231. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  232. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  233. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  234. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  235. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  236. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  237. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  238. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  239. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  240. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  241. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  242. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  243. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  244. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  245. package/bizar-dash/tests/yaml.test.mjs +128 -0
  246. package/bizar-dash/tsconfig.json +23 -0
  247. package/bizar-dash/vite.config.ts +27 -0
  248. package/cli/atomic.mjs +73 -0
  249. package/cli/bin.mjs +43 -46
  250. package/cli/copy.mjs +3 -0
  251. package/cli/dev-link.mjs +17 -1
  252. package/cli/init.mjs +135 -1
  253. package/cli/install.mjs +10 -23
  254. package/cli/memory-constants.mjs +59 -0
  255. package/cli/memory.mjs +757 -0
  256. package/cli/service.mjs +5 -6
  257. package/config/skills/obsidian/SKILL.md +213 -65
  258. package/install.sh +48 -18
  259. package/package.json +49 -14
  260. package/packages/sdk/LICENSE +21 -0
  261. package/packages/sdk/README.md +80 -0
  262. package/packages/sdk/src/client.ts +183 -0
  263. package/packages/sdk/src/errors.ts +120 -0
  264. package/packages/sdk/src/events.ts +153 -0
  265. package/packages/sdk/src/index.ts +63 -0
  266. package/packages/sdk/src/types.ts +176 -0
  267. package/packages/sdk/src/version.ts +4 -0
  268. package/packages/sdk/tests/client.test.ts +217 -0
  269. package/packages/sdk/tests/errors.test.ts +108 -0
  270. package/packages/sdk/tests/events.test.ts +139 -0
  271. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  272. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  273. package/packages/sdk/tsconfig.json +25 -0
  274. package/packages/sdk/vitest.config.ts +9 -0
  275. package/plugins/bizar/LICENSE +21 -0
  276. package/plugins/bizar/README.md +448 -0
  277. package/plugins/bizar/dist/index.js +29901 -0
  278. package/plugins/bizar/index.ts +1484 -0
  279. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  280. package/plugins/bizar/src/background-state.ts +551 -0
  281. package/plugins/bizar/src/background.ts +1250 -0
  282. package/plugins/bizar/src/commands-impl.ts +464 -0
  283. package/plugins/bizar/src/commands.ts +1127 -0
  284. package/plugins/bizar/src/dashboard-client.ts +233 -0
  285. package/plugins/bizar/src/event-stream.ts +606 -0
  286. package/plugins/bizar/src/fingerprint.ts +120 -0
  287. package/plugins/bizar/src/handoff.ts +79 -0
  288. package/plugins/bizar/src/http-client.ts +467 -0
  289. package/plugins/bizar/src/key-rotation.ts +218 -0
  290. package/plugins/bizar/src/logger.ts +144 -0
  291. package/plugins/bizar/src/loop.ts +176 -0
  292. package/plugins/bizar/src/opencode-runner.ts +390 -0
  293. package/plugins/bizar/src/options.ts +421 -0
  294. package/plugins/bizar/src/plan-fs.ts +323 -0
  295. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  296. package/plugins/bizar/src/report.ts +178 -0
  297. package/plugins/bizar/src/research-prompt.ts +35 -0
  298. package/plugins/bizar/src/serve-info.ts +228 -0
  299. package/plugins/bizar/src/serve.ts +496 -0
  300. package/plugins/bizar/src/settings.ts +349 -0
  301. package/plugins/bizar/src/state.ts +298 -0
  302. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  303. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  304. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  305. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  306. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  307. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  308. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  309. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  310. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  311. package/plugins/bizar/tests/background-state.test.ts +277 -0
  312. package/plugins/bizar/tests/background.test.ts +402 -0
  313. package/plugins/bizar/tests/block.test.ts +195 -0
  314. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  315. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  316. package/plugins/bizar/tests/commands.test.ts +548 -0
  317. package/plugins/bizar/tests/config.test.ts +122 -0
  318. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  319. package/plugins/bizar/tests/dispose.test.ts +336 -0
  320. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  321. package/plugins/bizar/tests/event.test.ts +262 -0
  322. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  323. package/plugins/bizar/tests/http-client.test.ts +404 -0
  324. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  325. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  326. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  327. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  328. package/plugins/bizar/tests/loop.test.ts +397 -0
  329. package/plugins/bizar/tests/options.test.ts +276 -0
  330. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  331. package/plugins/bizar/tests/serve.test.ts +339 -0
  332. package/plugins/bizar/tests/settings.test.ts +351 -0
  333. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  334. package/plugins/bizar/tests/state.test.ts +276 -0
  335. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  336. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  337. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  338. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  339. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  340. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  341. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  342. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  343. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  344. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  345. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  346. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * src/server/routes/dialogs.mjs
3
+ *
4
+ * v3.9.0 — Dialog-request HTTP routes.
5
+ *
6
+ * GET /api/dialogs — list recent dialog history (debug)
7
+ * GET /api/dialogs/queue — list currently-queued (undelivered) dialogs
8
+ * POST /api/dialogs — enqueue a dialog (used by the slash-command plugin)
9
+ * DELETE /api/dialogs/:id — remove a dialog from the queue
10
+ *
11
+ * WIRING (REQUIRED — read carefully):
12
+ *
13
+ * This router is OWNED BY THE DIALOG SCOPE (Tyr). The route is
14
+ * MOUNTED BY THE SLASH-COMMAND SIBLING SCOPE (Thor). Thor owns
15
+ * `server.mjs`; I do NOT modify that file.
16
+ *
17
+ * When Thor lands, they need to add the following to `server.mjs`:
18
+ *
19
+ * import { createDialogsRouter } from './routes/dialogs.mjs';
20
+ * import { startDialogPoller } from './dialog-poller.mjs';
21
+ *
22
+ * // …inside createServer or whatever the bootstrap is…
23
+ * app.use(createDialogsRouter({ broadcast }));
24
+ * startDialogPoller();
25
+ *
26
+ * Until that's done, the router is wired but unreachable and the
27
+ * poller never starts. This is intentional — sibling separation
28
+ * prevents both of us from racing on the same file.
29
+ *
30
+ * Service-mode gap: `cli/service.mjs` also does NOT start the dialog
31
+ * poller. If you want slash-command dialogs to flow in a service-only
32
+ * deployment (no dashboard), add `startDialogPoller()` to
33
+ * `cli/service.mjs`. Out of scope for this PR.
34
+ */
35
+ import { Router } from 'express';
36
+ import { dialogStore } from '../dialog-store.mjs';
37
+ import { wrap } from './_shared.mjs';
38
+
39
+ /**
40
+ * @param {object} deps
41
+ * @param {Function} deps.broadcast
42
+ * @returns {import('express').Router}
43
+ */
44
+ export function createDialogsRouter({ broadcast }) {
45
+ const router = Router();
46
+
47
+ // Debug / introspection — what dialogs are currently queued?
48
+ router.get('/dialogs/queue', wrap(async (_req, res) => {
49
+ const queue = dialogStore.list();
50
+ res.json({ count: queue.length, queue });
51
+ }));
52
+
53
+ // Debug / history — what's been delivered recently? Since dialogs
54
+ // are one-shot and removed after broadcast, this endpoint returns
55
+ // the *currently queued* set as well; a true history view would
56
+ // need a separate append-only log (out of scope here).
57
+ router.get('/dialogs', wrap(async (_req, res) => {
58
+ const queue = dialogStore.list();
59
+ res.json({ dialogs: queue, dir: dialogStore.DIALOG_DIR });
60
+ }));
61
+
62
+ // Used by the slash-command plugin (Thor owns that, I just provide
63
+ // the route). The plugin POSTs the dialog descriptor and the route
64
+ // persists it; the poller picks it up on the next tick.
65
+ router.post('/dialogs', wrap(async (req, res) => {
66
+ const dialog = dialogStore.add(req.body || {});
67
+ // Best-effort immediate broadcast — the poller will re-broadcast on
68
+ // the next tick if this fails, but a successful immediate broadcast
69
+ // cuts latency to ~ms for already-connected dashboards.
70
+ try {
71
+ broadcast({ type: 'dialog:show', dialog });
72
+ } catch { /* best-effort */ }
73
+ res.status(201).json(dialog);
74
+ }));
75
+
76
+ router.delete('/dialogs/:id', wrap(async (req, res) => {
77
+ const ok = dialogStore.remove(req.params.id);
78
+ if (!ok) {
79
+ res.status(404).json({ error: 'not_found' });
80
+ return;
81
+ }
82
+ res.status(204).end();
83
+ }));
84
+
85
+ return router;
86
+ }
@@ -0,0 +1,429 @@
1
+ /**
2
+ * src/server/routes/fs.mjs
3
+ *
4
+ * GET /api/fs?path=<absolute> — list directory entries
5
+ * POST /api/fs/mkdir — create a new subdirectory
6
+ *
7
+ * Read-only filesystem browser for the Add Project dialog. The
8
+ * frontend passes an absolute path; the server resolves it against
9
+ * the allow-list (`os.homedir()` + the user's configured
10
+ * `dashboard.projectsDirectory` + each entry in
11
+ * `dashboard.allowedRoots`) and returns the immediate children.
12
+ *
13
+ * Security boundaries:
14
+ * - Only absolute, resolved paths under an allowed root are
15
+ * honoured. Anything else returns 403.
16
+ * - First-level dotdirs under home (`.ssh`, `.aws`, `.gnupg`,
17
+ * …) are silently skipped in listings and refused as roots.
18
+ * - The GET endpoint never writes to disk. The POST endpoint
19
+ * creates exactly one new directory; nothing else.
20
+ * - All filesystem errors map to structured JSON, never an
21
+ * unhandled exception.
22
+ * - GET responses are capped at `MAX_ENTRIES` so a directory
23
+ * with millions of children (a `node_modules` clone, a Go
24
+ * module cache) cannot OOM the browser.
25
+ */
26
+ import { Router } from 'express';
27
+ import { mkdir, readdir, stat } from 'node:fs/promises';
28
+ import { dirname, join } from 'node:path';
29
+ import { homedir } from 'node:os';
30
+ import { wrap, readSettings } from './_shared.mjs';
31
+ import {
32
+ resolveSafePath,
33
+ buildAllowedRootsFromSettings,
34
+ } from '../lib/path-safe.mjs';
35
+
36
+ /**
37
+ * v3.11.0 — Hard cap on the number of entries returned by GET
38
+ * /api/fs. Sliced (not paginated) to keep the response shape
39
+ * stable and the round-trip cheap; the client gets a `truncated`
40
+ * flag so it can show "showing 500 of N" feedback.
41
+ */
42
+ const MAX_ENTRIES = 500;
43
+
44
+ /**
45
+ * v3.11.0 — Hard cap on the new-directory name length. 255 chars
46
+ * matches the typical ext4/NTFS max-component limit; longer names
47
+ * are almost certainly a bug or an attack.
48
+ */
49
+ const MAX_NAME_LENGTH = 255;
50
+
51
+ /**
52
+ * Build the per-request allow-list. We always include `os.homedir()`,
53
+ * and additionally include every entry in `settings.dashboard.allowedRoots`
54
+ * plus `settings.dashboard.projectsDirectory` — but only after
55
+ * re-validating each one against home so a tampered settings file
56
+ * can't widen the boundary.
57
+ *
58
+ * @returns {string[]}
59
+ */
60
+ function buildAllowedRoots() {
61
+ const settings = readSettings();
62
+ return buildAllowedRootsFromSettings({
63
+ settings: settings.data,
64
+ home: homedir(),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Decide whether a given directory entry should be visible to the
70
+ * file browser. We hide dotfiles / dotdirs at every depth — keeps
71
+ * the browser focused on real user-visible projects and prevents
72
+ * accidental click-throughs into things like `node_modules/.cache`.
73
+ *
74
+ * (Dotdir *roots* under home are blocked at a different layer by
75
+ * resolveSafePath; this is the in-listing filter.)
76
+ *
77
+ * @param {string} name
78
+ * @returns {boolean}
79
+ */
80
+ function isHiddenEntry(name) {
81
+ return typeof name === 'string' && name.startsWith('.');
82
+ }
83
+
84
+ /**
85
+ * Sort entries: directories first (case-insensitive), then files
86
+ * (case-insensitive). Stable for ties.
87
+ *
88
+ * @param {{ isDir: boolean, name: string }[]} entries
89
+ */
90
+ function sortEntries(entries) {
91
+ return [...entries].sort((a, b) => {
92
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
93
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
94
+ });
95
+ }
96
+
97
+ /**
98
+ * v3.11.0 — Validate a new-directory name. Defense in depth: the
99
+ * frontend should validate too, but the server is the boundary.
100
+ *
101
+ * Returns `null` on success, or a human-readable error message on
102
+ * failure. Designed to surface as `400 { error: "bad_request",
103
+ * message }` to the client.
104
+ *
105
+ * Rules:
106
+ * - must be a non-empty string
107
+ * - length 1..255
108
+ * - must NOT be `.` or `..`
109
+ * - must NOT start with `-` (could be confused with a CLI flag)
110
+ * - must NOT contain `/`, `\`, NUL, `:`, `*`, `?`, `"`, `<`, `>`, `|`
111
+ *
112
+ * @param {unknown} name
113
+ * @returns {string | null}
114
+ */
115
+ function validateDirName(name) {
116
+ if (typeof name !== 'string' || name.length === 0) {
117
+ return 'name is required';
118
+ }
119
+ if (name.length > MAX_NAME_LENGTH) {
120
+ return `name is too long (max ${MAX_NAME_LENGTH} characters)`;
121
+ }
122
+ if (name === '.' || name === '..') {
123
+ return 'name may not be "." or ".."';
124
+ }
125
+ if (name.startsWith('-')) {
126
+ return 'name may not start with "-"';
127
+ }
128
+ // Denylist of characters that have a special meaning on the shell,
129
+ // Windows, or POSIX paths. Listed as discrete substrings (some are
130
+ // multi-character escapes) for an explicit denylist with clear
131
+ // error messages.
132
+ const denylist = ['/', '\\', '\0', ':', '*', '?', '"', '<', '>', '|'];
133
+ for (const c of denylist) {
134
+ if (name.includes(c)) {
135
+ return `name may not contain "${c}"`;
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * v3.11.0 — Build the absolute path of the would-be new directory,
143
+ * verifying it remains inside the allow-list. A symlink or
144
+ * pre-existing weirdness in the parent could in principle make
145
+ * `path.join` produce a target that escapes; the re-check is
146
+ * cheap and prevents the race.
147
+ *
148
+ * @param {string} safeParent
149
+ * @param {string} name
150
+ * @param {string[]} allowedRoots
151
+ * @returns {string | null}
152
+ */
153
+ function resolveTargetUnderParent(safeParent, name, allowedRoots) {
154
+ const target = join(safeParent, name);
155
+ return resolveSafePath(target, allowedRoots);
156
+ }
157
+
158
+ /**
159
+ * @param {object} deps
160
+ * @param {object} deps.state
161
+ * @returns {import('express').Router}
162
+ */
163
+ export function createFsRouter({ state }) {
164
+ const router = Router();
165
+
166
+ router.get('/fs', wrap(async (req, res) => {
167
+ const requested = typeof req.query.path === 'string' ? req.query.path : '';
168
+ const allowedRoots = buildAllowedRoots();
169
+ const safe = resolveSafePath(requested, allowedRoots);
170
+ if (!safe) {
171
+ res.status(403).json({
172
+ error: 'forbidden',
173
+ message: 'path is outside the allowed roots (home + dashboard.projectsDirectory + dashboard.allowedRoots)',
174
+ });
175
+ return;
176
+ }
177
+
178
+ let dirStat;
179
+ try {
180
+ dirStat = await stat(safe);
181
+ } catch (err) {
182
+ if (err && err.code === 'ENOENT') {
183
+ res.status(404).json({ error: 'not_found' });
184
+ return;
185
+ }
186
+ if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
187
+ res.status(403).json({ error: 'permission_denied' });
188
+ return;
189
+ }
190
+ throw err;
191
+ }
192
+
193
+ if (!dirStat.isDirectory()) {
194
+ res.status(400).json({ error: 'not_a_directory' });
195
+ return;
196
+ }
197
+
198
+ let dirents;
199
+ try {
200
+ dirents = await readdir(safe, { withFileTypes: true });
201
+ } catch (err) {
202
+ if (err && err.code === 'ENOENT') {
203
+ res.status(404).json({ error: 'not_found' });
204
+ return;
205
+ }
206
+ if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
207
+ res.status(403).json({ error: 'permission_denied' });
208
+ return;
209
+ }
210
+ throw err;
211
+ }
212
+
213
+ const entries = [];
214
+ for (const dirent of dirents) {
215
+ const name = dirent.name;
216
+ // Hide dotfile / dotdir entries at every depth.
217
+ if (isHiddenEntry(name)) continue;
218
+ const sep = safe.includes('\\') && !safe.includes('/') ? '\\' : '/';
219
+ const entryPath = safe.endsWith(sep) ? safe + name : safe + sep + name;
220
+ // Defaults — used when stat fails. dirent.isDirectory() is a
221
+ // cheap, reliable indicator that does not require an extra
222
+ // syscall, so prefer it for isDir.
223
+ let isDir = dirent.isDirectory();
224
+ let isSymlink = dirent.isSymbolicLink();
225
+ try {
226
+ // Follow the symlink (if any) so the user can see whether
227
+ // it resolves to a real directory.
228
+ const s = await stat(entryPath);
229
+ if (s.isDirectory()) isDir = true;
230
+ if (s.isSymbolicLink()) isSymlink = true;
231
+ } catch {
232
+ // Best-effort. Common cases:
233
+ // - EACCES on a sibling we can't stat (parent only readable
234
+ // to us, entry mode 000). Trust the dirent.
235
+ // - ENOENT means the entry disappeared between readdir and
236
+ // stat (rare race). Drop it.
237
+ if (dirent.isSymbolicLink() && !isDir) {
238
+ // Symlink whose target we can't read — show it as a
239
+ // symlink, not a directory, so the user knows to be
240
+ // careful before entering.
241
+ isDir = false;
242
+ }
243
+ }
244
+ entries.push({ name, path: entryPath, isDir, isSymlink });
245
+ }
246
+
247
+ const sorted = sortEntries(entries);
248
+ const totalEntries = sorted.length;
249
+ // v3.11.0 — Cap the response so a 50k-entry `node_modules`
250
+ // clone cannot OOM the browser. We always include the
251
+ // `truncated` flag (and `totalEntries`) for type stability —
252
+ // the client can rely on the shape even when no truncation
253
+ // happened.
254
+ const truncated = totalEntries > MAX_ENTRIES;
255
+ const capped = truncated ? sorted.slice(0, MAX_ENTRIES) : sorted;
256
+ const parent = dirname(safe);
257
+ // The "parent" we return should be null when we're already at
258
+ // the filesystem root — Express/Node will give us '/' there.
259
+ const isRoot = parent === safe;
260
+ res.json({
261
+ path: safe,
262
+ parent: isRoot ? null : parent,
263
+ entries: capped,
264
+ truncated,
265
+ totalEntries,
266
+ });
267
+ }));
268
+
269
+ /**
270
+ * v3.11.0 — Create a new directory. This is the FIRST write
271
+ * endpoint in the file-browser surface, so it is paranoid about
272
+ * race conditions: check-then-create (EEXIST → 409), and
273
+ * validate the result via `stat` after creation.
274
+ *
275
+ * Auth: mounted under `/api`, so `requireAuth` in server.mjs
276
+ * gates this automatically.
277
+ *
278
+ * Rate limiting: not yet implemented at the server side. The
279
+ * frontend should debounce the click. Follow-up: add a per-IP
280
+ * token bucket if abuse becomes a concern.
281
+ */
282
+ router.post('/fs/mkdir', wrap(async (req, res) => {
283
+ const parent = typeof req.body?.parent === 'string' ? req.body.parent : '';
284
+ const name = typeof req.body?.name === 'string' ? req.body.name : '';
285
+
286
+ if (!parent) {
287
+ res.status(400).json({
288
+ error: 'bad_request',
289
+ message: 'parent is required',
290
+ });
291
+ return;
292
+ }
293
+
294
+ const allowedRoots = buildAllowedRoots();
295
+
296
+ // 1. Parent must be inside the allow-list.
297
+ const safeParent = resolveSafePath(parent, allowedRoots);
298
+ if (!safeParent) {
299
+ res.status(403).json({
300
+ error: 'forbidden',
301
+ message: 'parent is outside the allowed roots',
302
+ });
303
+ return;
304
+ }
305
+
306
+ // 2. Name must pass the denylist validation.
307
+ const nameError = validateDirName(name);
308
+ if (nameError) {
309
+ res.status(400).json({ error: 'bad_request', message: nameError });
310
+ return;
311
+ }
312
+
313
+ // 3. Compute target and re-validate it is in the allow-list
314
+ // (defense against symlinks or pre-existing weirdness in
315
+ // the parent).
316
+ const safeTarget = resolveTargetUnderParent(safeParent, name, allowedRoots);
317
+ if (!safeTarget) {
318
+ res.status(403).json({
319
+ error: 'forbidden',
320
+ message: 'target is outside the allowed roots',
321
+ });
322
+ return;
323
+ }
324
+
325
+ // 4. Parent must exist and be a directory.
326
+ let parentStat;
327
+ try {
328
+ parentStat = await stat(safeParent);
329
+ } catch (err) {
330
+ if (err && err.code === 'ENOENT') {
331
+ res.status(404).json({ error: 'not_found', message: 'parent does not exist' });
332
+ return;
333
+ }
334
+ if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
335
+ res.status(403).json({ error: 'permission_denied' });
336
+ return;
337
+ }
338
+ throw err;
339
+ }
340
+ if (!parentStat.isDirectory()) {
341
+ res.status(400).json({ error: 'not_a_directory' });
342
+ return;
343
+ }
344
+
345
+ // 5. Pre-check existence (best-effort; EEXIST from mkdir is
346
+ // authoritative).
347
+ try {
348
+ await stat(safeTarget);
349
+ res.status(409).json({
350
+ error: 'already_exists',
351
+ message: `an entry named "${name}" already exists in this directory`,
352
+ });
353
+ return;
354
+ } catch (err) {
355
+ if (err && err.code !== 'ENOENT') throw err;
356
+ // ENOENT is the expected "good to go" case.
357
+ }
358
+
359
+ // 6. Create the directory. recursive:false — we want a flat
360
+ // one-level create, never a multi-level path.
361
+ try {
362
+ await mkdir(safeTarget, { recursive: false });
363
+ } catch (err) {
364
+ if (err && err.code === 'EEXIST') {
365
+ // Race: someone else created the dir between our stat
366
+ // and our mkdir. Surface as 409.
367
+ res.status(409).json({
368
+ error: 'already_exists',
369
+ message: `an entry named "${name}" already exists in this directory`,
370
+ });
371
+ return;
372
+ }
373
+ if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
374
+ res.status(403).json({
375
+ error: 'permission_denied',
376
+ message: err.message,
377
+ });
378
+ return;
379
+ }
380
+ if (err && err.code === 'ENOSPC') {
381
+ res.status(500).json({
382
+ error: 'mkdir_failed',
383
+ message: 'no space left on device',
384
+ });
385
+ return;
386
+ }
387
+ res.status(500).json({
388
+ error: 'mkdir_failed',
389
+ message: err?.message || String(err),
390
+ });
391
+ return;
392
+ }
393
+
394
+ // 7. Verify the result via stat. Catches a vanishingly rare
395
+ // race where something removed the dir between mkdir and
396
+ // stat, and any other weirdness (e.g. EPERM on a follow-up
397
+ // stat due to a mode change).
398
+ let createdStat;
399
+ try {
400
+ createdStat = await stat(safeTarget);
401
+ } catch (err) {
402
+ res.status(500).json({
403
+ error: 'mkdir_verify_failed',
404
+ message: err?.message || String(err),
405
+ });
406
+ return;
407
+ }
408
+ if (!createdStat.isDirectory()) {
409
+ res.status(409).json({
410
+ error: 'already_exists',
411
+ message: `an entry named "${name}" is no longer a directory`,
412
+ });
413
+ return;
414
+ }
415
+
416
+ res.status(201).json({
417
+ path: safeTarget,
418
+ parent: safeParent,
419
+ name,
420
+ });
421
+ }));
422
+
423
+ // Keep `state` referenced so this router follows the same factory
424
+ // signature as the others (settings/overview/etc.). Future fs
425
+ // operations may broadcast state changes.
426
+ void state;
427
+
428
+ return router;
429
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * src/server/routes/history.mjs
3
+ *
4
+ * /api/history — cross-project timeline
5
+ *
6
+ * Aggregates activity log events with per-project task / plan counts.
7
+ * Supports ?since=<iso> date filtering. Used by the History view.
8
+ */
9
+ import { Router } from 'express';
10
+ import { tasksStore } from '../tasks-store.mjs';
11
+ import { artifactsStore } from '../artifacts-store.mjs';
12
+ import { projectsStore } from '../projects-store.mjs';
13
+ import { wrap } from './_shared.mjs';
14
+
15
+ /**
16
+ * @param {object} deps
17
+ * @param {string} deps.projectRoot
18
+ * @returns {import('express').Router}
19
+ */
20
+ export function createHistoryRouter({ projectRoot }) {
21
+ const router = Router();
22
+
23
+ router.get('/history', wrap(async (req, res) => {
24
+ const { activityLog } = await import('../activity-log.mjs');
25
+ const limit = Math.min(2000, Math.max(1, parseInt(req.query.limit || '500', 10) || 500));
26
+ const sinceStr = req.query.since ? String(req.query.since) : null;
27
+ const since = sinceStr ? new Date(sinceStr).getTime() : null;
28
+ const events = activityLog.recent(limit)
29
+ .filter((e) => {
30
+ if (!since) return true;
31
+ const t = new Date(e.ts).getTime();
32
+ return Number.isFinite(t) && t >= since;
33
+ })
34
+ .reverse(); // oldest-first for timeline display
35
+
36
+ // Per-project rollups
37
+ const projectTasks = {};
38
+ const projectPlans = {};
39
+ // projectsStore.list() returns { projects, active }, not a bare
40
+ // array. Pull .projects off before iterating. (Bug carried over
41
+ // from the original api.mjs — fixed here while splitting.)
42
+ const projects = projectsStore.list().projects || [];
43
+ for (const p of projects) {
44
+ try {
45
+ const tasks = tasksStore.loadTasks(p.id);
46
+ projectTasks[p.id] = {
47
+ total: tasks.length,
48
+ done: tasks.filter((t) => t.status === 'done').length,
49
+ doing: tasks.filter((t) => t.status === 'doing').length,
50
+ blocked: tasks.filter((t) => t.status === 'blocked').length,
51
+ queued: tasks.filter((t) => t.status === 'queued').length,
52
+ };
53
+ } catch {
54
+ projectTasks[p.id] = { total: 0, done: 0, doing: 0, blocked: 0, queued: 0 };
55
+ }
56
+ }
57
+ try {
58
+ const plans = artifactsStore.list(projectRoot);
59
+ for (const plan of plans) {
60
+ const pid = plan.projectId || 'global';
61
+ projectPlans[pid] = (projectPlans[pid] || 0) + 1;
62
+ }
63
+ } catch { /* best-effort */ }
64
+
65
+ res.json({
66
+ events,
67
+ projects: projects.map((p) => ({
68
+ ...p,
69
+ tasks: projectTasks[p.id] || { total: 0, done: 0, doing: 0, blocked: 0, queued: 0 },
70
+ plans: projectPlans[p.id] || 0,
71
+ })),
72
+ stats: activityLog.stats(),
73
+ generatedAt: new Date().toISOString(),
74
+ });
75
+ }));
76
+
77
+ return router;
78
+ }