@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,468 @@
1
+ /**
2
+ * src/server/routes/tasks.mjs
3
+ *
4
+ * /api/tasks — list
5
+ * /api/tasks (POST) — create
6
+ * /api/tasks/submit (POST) — Odin-style multi-task dispatch
7
+ * /api/tasks/bulk (POST) — bulk action across ids
8
+ * /api/tasks/:id (PUT) — update
9
+ * /api/tasks/:id (DELETE) — delete
10
+ * /api/tasks/:id/start (POST) — trigger dispatch of a queued task
11
+ * /api/tasks/:id/status (PATCH) — move status (queued|doing|done|blocked|archived)
12
+ * /api/tasks/:id/comments (POST) — append comment
13
+ * /api/tasks/:id/chat (GET) — opencode messages for the task's bg instance
14
+ * /api/tasks/:id/artifacts (GET) — list artifacts for this task
15
+ * /api/tasks/:id/artifacts (POST) — attach artifact + link to task
16
+ * /api/tasks/:id/timer (POST) — toggle timer
17
+ * /api/tasks/:id/timer/start (POST) — start timer
18
+ * /api/tasks/:id/timer/stop (POST) — stop timer
19
+ * /api/tasks/:id/archive (POST) — archive
20
+ * /api/tasks/:id/unarchive (POST) — unarchive
21
+ * /api/tasks/:id/work (POST) — agent claims progress
22
+ * /api/tasks/:id/progress (POST) — progress + current step update
23
+ *
24
+ * IMPORTANT — ordering matters for Express. /tasks/bulk and
25
+ * /tasks/submit MUST be declared before /tasks/:id so that the
26
+ * literal "bulk" / "submit" segments don't get captured by the
27
+ * :id parameter. Same for /tasks/:id/start vs /tasks/:id (less of a
28
+ * conflict since :id is followed by something either way).
29
+ */
30
+ import { Router } from 'express';
31
+ import { tasksStore } from '../tasks-store.mjs';
32
+ import { agentsStore } from '../agents-store.mjs';
33
+ import { notificationsStore } from '../notifications-store.mjs';
34
+ import { projectsStore } from '../projects-store.mjs';
35
+ import { artifactsStore } from '../artifacts-store.mjs';
36
+ import {
37
+ readServeInfo,
38
+ listOpencodeMessages,
39
+ normalizeOpencodeMessage,
40
+ } from '../serve-info.mjs';
41
+ import { readActiveProjectId, wrap } from './_shared.mjs';
42
+
43
+ /**
44
+ * @param {object} deps
45
+ * @param {object} deps.state
46
+ * @param {Function} deps.broadcast
47
+ * @param {string} deps.projectRoot
48
+ * @returns {import('express').Router}
49
+ */
50
+ export function createTasksRouter({ state, broadcast, projectRoot }) {
51
+ const router = Router();
52
+
53
+ // /tasks/submit must come BEFORE /tasks/:id so the literal segment
54
+ // isn't captured by the :id param. The Odin task delegator splits
55
+ // the input into subtasks and dispatches them to bg infrastructure.
56
+ router.post('/tasks/submit', wrap(async (req, res) => {
57
+ const projectId = req.body?.projectId || readActiveProjectId();
58
+ const { taskDelegator } = await import('../task-delegator.mjs');
59
+ try {
60
+ const result = await taskDelegator.submit(req.body || {}, {
61
+ projectRoot,
62
+ projectId,
63
+ state,
64
+ broadcast,
65
+ });
66
+ // 207 Multi-Status when some dispatches failed — caller can decide
67
+ // whether to retry. Pure success keeps 201.
68
+ const partial = (result?.dispatch?.errors || []).length > 0;
69
+ res.status(partial ? 207 : 201).json(result);
70
+ } catch (err) {
71
+ const status = err?.message?.includes('required') ? 400 : 500;
72
+ res.status(status).json({
73
+ error: status === 400 ? 'bad_request' : 'submission_failed',
74
+ message: err?.message || String(err),
75
+ });
76
+ }
77
+ }));
78
+
79
+ router.get('/tasks', wrap(async (req, res) => {
80
+ const projectId = req.query.projectId || readActiveProjectId();
81
+ const includeArchived = req.query.archived === 'true' || req.query.archived === '1';
82
+ const onlyArchived = req.query.archived === 'only' || req.query.archived === 'archived';
83
+ res.json(tasksStore.loadTasks(projectId, { includeArchived, onlyArchived }));
84
+ }));
85
+
86
+ router.post('/tasks', wrap(async (req, res) => {
87
+ const projectId = req.body?.projectId || readActiveProjectId();
88
+ const task = await tasksStore.create(projectId, req.body || {});
89
+ broadcast({ type: 'tasks:change', task });
90
+ res.status(201).json(task);
91
+ }));
92
+
93
+ router.put('/tasks/:id', wrap(async (req, res) => {
94
+ const projectId = req.body?.projectId || readActiveProjectId();
95
+ const task = await tasksStore.update(projectId, req.params.id, req.body || {});
96
+ if (!task) {
97
+ res.status(404).json({ error: 'not_found' });
98
+ return;
99
+ }
100
+ broadcast({ type: 'tasks:change', task });
101
+ res.json(task);
102
+ }));
103
+
104
+ // v3.5.4 — Trigger execution of a queued task. Used by external
105
+ // operators, tests, and the UI's "Retry dispatch" button. Reuses the
106
+ // same dispatch logic as `/tasks/submit` so dispatch errors surface
107
+ // in the same shape. Returns 400 when the task is not in `queued`
108
+ // status, 404 when missing.
109
+ router.post('/tasks/:id/start', wrap(async (req, res) => {
110
+ const projectId = req.body?.projectId || req.query?.projectId || readActiveProjectId();
111
+ const taskId = req.params.id;
112
+ const task = await tasksStore.getById(projectId, taskId);
113
+ if (!task) {
114
+ res.status(404).json({ error: 'not_found', message: `task ${taskId} not found` });
115
+ return;
116
+ }
117
+ if (task.status !== 'queued') {
118
+ res.status(400).json({
119
+ error: 'invalid_status',
120
+ message: `task is '${task.status}', must be 'queued' to start`,
121
+ task,
122
+ });
123
+ return;
124
+ }
125
+ const { taskDelegator } = await import('../task-delegator.mjs');
126
+ const result = await taskDelegator.dispatchSingleTask(taskId, {
127
+ projectRoot,
128
+ projectId,
129
+ state,
130
+ broadcast,
131
+ });
132
+ if (!result.ok) {
133
+ // Surface dispatch errors with 502 so callers know the task did
134
+ // not actually start. `result.task` carries the refreshed state.
135
+ const code = (result.errors || []).some((e) => e.kind === 'not_found') ? 404 : 502;
136
+ res.status(code).json({
137
+ error: 'dispatch_failed',
138
+ message: (result.errors || []).map((e) => e.message).join('; ') || 'dispatch failed',
139
+ task: result.task,
140
+ errors: result.errors || [],
141
+ warnings: result.warnings || [],
142
+ });
143
+ return;
144
+ }
145
+ res.status(202).json({
146
+ ok: true,
147
+ task: result.task,
148
+ dispatched: result.dispatched,
149
+ warnings: result.warnings || [],
150
+ });
151
+ }));
152
+
153
+ router.patch('/tasks/:id/status', wrap(async (req, res) => {
154
+ const projectId = req.body?.projectId || readActiveProjectId();
155
+ const { status } = req.body || {};
156
+ if (!['queued', 'doing', 'done', 'blocked', 'archived'].includes(status)) {
157
+ res.status(400).json({ error: 'bad_request', message: 'invalid status' });
158
+ return;
159
+ }
160
+ const task = await tasksStore.move(projectId, req.params.id, status);
161
+ if (!task) {
162
+ res.status(404).json({ error: 'not_found' });
163
+ return;
164
+ }
165
+ broadcast({ type: 'tasks:change', task });
166
+ if (status === 'done' && task.recurring) {
167
+ const next = await tasksStore.spawnNextRecurrence(projectId, task);
168
+ if (next) broadcast({ type: 'tasks:change', task: next });
169
+ }
170
+ // v3.3.0 — Fire a notification on completion. The Tasks view
171
+ // already paints the badge in real time via tasks:change; the
172
+ // notification is the cross-tab history bit.
173
+ if (status === 'done') {
174
+ try {
175
+ notificationsStore.add({
176
+ severity: 'success',
177
+ source: 'tasks',
178
+ title: 'Task completed',
179
+ message: task.title || task.id,
180
+ meta: { taskId: task.id },
181
+ }, { broadcast });
182
+ } catch { /* best-effort */ }
183
+ } else if (status === 'blocked') {
184
+ try {
185
+ notificationsStore.add({
186
+ severity: 'warning',
187
+ source: 'tasks',
188
+ title: 'Task blocked',
189
+ message: task.title || task.id,
190
+ meta: { taskId: task.id },
191
+ }, { broadcast });
192
+ } catch { /* best-effort */ }
193
+ }
194
+ res.json(task);
195
+ }));
196
+
197
+ router.post('/tasks/:id/comments', wrap(async (req, res) => {
198
+ const projectId = req.body?.projectId || readActiveProjectId();
199
+ const task = await tasksStore.addComment(projectId, req.params.id, req.body?.text || '');
200
+ if (!task) {
201
+ res.status(404).json({ error: 'not_found' });
202
+ return;
203
+ }
204
+ broadcast({ type: 'tasks:change', task });
205
+ res.json(task);
206
+ }));
207
+
208
+ // v3.5.5 — Chat-task linkage. Given a task id, return the
209
+ // opencode session's messages so the chat UI can open a thread
210
+ // for an in-progress or completed task. The task must have a
211
+ // `bgInstanceId` (or `sessionId`) in its metadata — the delegator
212
+ // writes that when the opencode session is created.
213
+ //
214
+ // Response shape:
215
+ // { taskId, bgInstanceId, sessionId, agent, messages: [{id,role,content,ts}, ...] }
216
+ //
217
+ // Status codes:
218
+ // 200 — messages returned (may be empty)
219
+ // 404 — task not found, or no bg instance / session id
220
+ // 503 — plugin offline
221
+ // 502 — opencode listMessages call failed
222
+ router.get('/tasks/:id/chat', wrap(async (req, res) => {
223
+ const projectId = req.query.projectId || readActiveProjectId();
224
+ const task = await tasksStore.getById(projectId, req.params.id);
225
+ if (!task) {
226
+ res.status(404).json({ error: 'not_found', message: `task ${req.params.id} not found` });
227
+ return;
228
+ }
229
+ const meta = task.metadata || {};
230
+ let bgInstanceId = meta.bgInstanceId || meta.instanceId || null;
231
+ let sessionId = meta.sessionId || null;
232
+ let agent = meta.agent || task.assignee || null;
233
+
234
+ // If we have a bgInstanceId but not a sessionId, look it up.
235
+ if (bgInstanceId && !sessionId) {
236
+ try {
237
+ const { backgroundStore } = await import('../background-store.mjs');
238
+ const bg = backgroundStore.get(bgInstanceId);
239
+ if (bg) {
240
+ sessionId = bg.sessionId || null;
241
+ agent = agent || bg.agent || null;
242
+ }
243
+ } catch { /* best effort */ }
244
+ }
245
+ if (!bgInstanceId && !sessionId) {
246
+ return res.status(404).json({
247
+ error: 'no_bg_instance',
248
+ message: 'task has no bg instance or opencode session id',
249
+ taskId: task.id,
250
+ });
251
+ }
252
+
253
+ const serveInfo = readServeInfo();
254
+ if (!serveInfo) {
255
+ return res.status(503).json({
256
+ error: 'plugin_offline',
257
+ message: 'opencode plugin is not running',
258
+ taskId: task.id,
259
+ });
260
+ }
261
+ if (!sessionId) {
262
+ return res.status(404).json({
263
+ error: 'no_session',
264
+ message: 'bg instance has no opencode session id',
265
+ taskId: task.id,
266
+ bgInstanceId,
267
+ });
268
+ }
269
+
270
+ const active = projectsStore.active();
271
+ const directory = (active && active.path) || serveInfo.worktree || '';
272
+ const result = await listOpencodeMessages(serveInfo, sessionId, directory);
273
+ if (!result.ok) {
274
+ return res.status(502).json({
275
+ error: 'opencode_error',
276
+ message: result.error || 'listOpencodeMessages failed',
277
+ taskId: task.id,
278
+ sessionId,
279
+ });
280
+ }
281
+ const messages = Array.isArray(result.messages)
282
+ ? result.messages.map(normalizeOpencodeMessage)
283
+ : [];
284
+ res.json({
285
+ taskId: task.id,
286
+ bgInstanceId,
287
+ sessionId,
288
+ agent,
289
+ messages,
290
+ });
291
+ }));
292
+
293
+ // v3.5.5 — Per-task artifact endpoints. These live in tasks.mjs
294
+ // (not artifacts.mjs) because the path parameter is the task id.
295
+ // The standalone /api/artifacts/:id routes are in artifacts.mjs.
296
+ router.get('/tasks/:id/artifacts', wrap(async (req, res) => {
297
+ const items = artifactsStore.list({ taskId: req.params.id });
298
+ res.json({ artifacts: items, taskId: req.params.id });
299
+ }));
300
+
301
+ router.post('/tasks/:id/artifacts', wrap(async (req, res) => {
302
+ const projectId = req.body?.projectId || readActiveProjectId();
303
+ const active = projectsStore.active();
304
+ const { name, contentType, content } = req.body || {};
305
+ if (typeof content !== 'string' || !content.length) {
306
+ res.status(400).json({ error: 'content_required', message: 'content (string) is required' });
307
+ return;
308
+ }
309
+ const meta = artifactsStore.save({
310
+ taskId: req.params.id,
311
+ projectId: projectId || (active ? active.id : null),
312
+ name: name || `Artifact for ${req.params.id}`,
313
+ contentType: contentType || 'text/html',
314
+ content,
315
+ });
316
+ // Link the artifact back to the task. We do NOT overwrite
317
+ // existing artifactId/artifactIds metadata; the new id is
318
+ // appended to the array and the legacy `artifactId` field is
319
+ // bumped to the latest.
320
+ let updated = null;
321
+ if (projectId) {
322
+ const task = await tasksStore.getById(projectId, req.params.id);
323
+ const prevMeta = (task && task.metadata) || {};
324
+ const prevIds = Array.isArray(prevMeta.artifactIds) ? prevMeta.artifactIds : [];
325
+ const newIds = prevIds.includes(meta.id) ? prevIds : [...prevIds, meta.id];
326
+ updated = await tasksStore.update(projectId, req.params.id, {
327
+ metadata: {
328
+ artifactId: meta.id,
329
+ artifactIds: newIds,
330
+ artifactName: meta.name,
331
+ },
332
+ });
333
+ if (updated) {
334
+ broadcast({ type: 'tasks:change', task: updated });
335
+ }
336
+ }
337
+ broadcast({ type: 'artifact:new', artifact: meta });
338
+ res.status(201).json(meta);
339
+ }));
340
+
341
+ router.post('/tasks/:id/timer', wrap(async (req, res) => {
342
+ const projectId = req.body?.projectId || readActiveProjectId();
343
+ const task = await tasksStore.toggleTimer(projectId, req.params.id);
344
+ if (!task) {
345
+ res.status(404).json({ error: 'not_found' });
346
+ return;
347
+ }
348
+ broadcast({ type: 'tasks:change', task });
349
+ res.json(task);
350
+ }));
351
+
352
+ router.post('/tasks/:id/timer/start', wrap(async (req, res) => {
353
+ const projectId = req.body?.projectId || readActiveProjectId();
354
+ const task = await tasksStore.startTimer(projectId, req.params.id);
355
+ if (!task) { res.status(404).json({ error: 'not_found' }); return; }
356
+ broadcast({ type: 'tasks:change', task });
357
+ res.json(task);
358
+ }));
359
+
360
+ router.post('/tasks/:id/timer/stop', wrap(async (req, res) => {
361
+ const projectId = req.body?.projectId || readActiveProjectId();
362
+ const task = await tasksStore.stopTimer(projectId, req.params.id);
363
+ if (!task) { res.status(404).json({ error: 'not_found' }); return; }
364
+ broadcast({ type: 'tasks:change', task });
365
+ res.json(task);
366
+ }));
367
+
368
+ // v3.1.0 — Archive / unarchive / bulk.
369
+ router.post('/tasks/:id/archive', wrap(async (req, res) => {
370
+ const projectId = req.body?.projectId || readActiveProjectId();
371
+ const task = await tasksStore.archive(projectId, req.params.id);
372
+ if (!task) { res.status(404).json({ error: 'not_found' }); return; }
373
+ broadcast({ type: 'tasks:change', task });
374
+ res.json(task);
375
+ }));
376
+
377
+ router.post('/tasks/:id/unarchive', wrap(async (req, res) => {
378
+ const projectId = req.body?.projectId || readActiveProjectId();
379
+ const task = await tasksStore.unarchive(projectId, req.params.id);
380
+ if (!task) { res.status(404).json({ error: 'not_found' }); return; }
381
+ broadcast({ type: 'tasks:change', task });
382
+ res.json(task);
383
+ }));
384
+
385
+ // /tasks/bulk MUST come before /tasks/:id so the literal segment
386
+ // doesn't get captured as an id.
387
+ router.post('/tasks/bulk', wrap(async (req, res) => {
388
+ const projectId = req.body?.projectId || readActiveProjectId();
389
+ const { ids, action, params } = req.body || {};
390
+ if (!Array.isArray(ids)) {
391
+ res.status(400).json({ error: 'bad_request', message: 'ids[] required' });
392
+ return;
393
+ }
394
+ const out = await tasksStore.bulk(projectId, ids, action, params || {});
395
+ for (const r of out.affected) {
396
+ if (!r.ok) continue;
397
+ if (action === 'delete') {
398
+ broadcast({ type: 'tasks:delete', id: r.id });
399
+ } else {
400
+ const all = await tasksStore.loadTasks(projectId, { includeArchived: true });
401
+ const t = all.find((x) => x.id === r.id);
402
+ if (t) broadcast({ type: 'tasks:change', task: t });
403
+ }
404
+ }
405
+ res.json(out);
406
+ }));
407
+
408
+ // v3.1.0 — Mark a task as worked-on by an agent. Bumps both sides.
409
+ router.post('/tasks/:id/work', wrap(async (req, res) => {
410
+ const projectId = req.body?.projectId || readActiveProjectId();
411
+ const { agent, status, complete } = req.body || {};
412
+ if (!agent) {
413
+ res.status(400).json({ error: 'bad_request', message: 'agent is required' });
414
+ return;
415
+ }
416
+ const task = await tasksStore.setWorkedBy(projectId, req.params.id, agent, { status, complete: !!complete });
417
+ if (!task) { res.status(404).json({ error: 'not_found' }); return; }
418
+ let agentSnapshot = null;
419
+ if (status === 'doing') {
420
+ agentSnapshot = agentsStore.updateStatus(agent, 'working', task.id);
421
+ } else if (status === 'done' || complete) {
422
+ agentSnapshot = agentsStore.recordTaskResult(agent, task.id, complete !== false);
423
+ } else {
424
+ agentSnapshot = agentsStore.updateStatus(agent, 'idle', null);
425
+ }
426
+ if (agentSnapshot) broadcast({ type: 'agent:status', agent: agentSnapshot });
427
+ broadcast({ type: 'tasks:change', task });
428
+ if (task.recurring && status === 'done') {
429
+ const next = await tasksStore.spawnNextRecurrence(projectId, task);
430
+ if (next) broadcast({ type: 'tasks:change', task: next });
431
+ }
432
+ res.json(task);
433
+ }));
434
+
435
+ router.delete('/tasks/:id', wrap(async (req, res) => {
436
+ const projectId = req.query.projectId || readActiveProjectId();
437
+ const ok = await tasksStore.delete(projectId, req.params.id);
438
+ if (!ok) {
439
+ res.status(404).json({ error: 'not_found' });
440
+ return;
441
+ }
442
+ broadcast({ type: 'tasks:delete', id: req.params.id });
443
+ res.status(204).end();
444
+ }));
445
+
446
+ // v3.3.0 — Task progress updates from agents. Updates the in-store
447
+ // metadata.progress + metadata.currentStep and broadcasts a WS
448
+ // event so the Tasks view can paint the progress bar in real
449
+ // time.
450
+ router.post('/tasks/:id/progress', wrap(async (req, res) => {
451
+ const projectId = req.body?.projectId || readActiveProjectId();
452
+ const { progress, step, agent } = req.body || {};
453
+ const task = await tasksStore.updateProgress(projectId, req.params.id, {
454
+ progress: Number.isFinite(progress) ? Math.max(0, Math.min(100, progress)) : 0,
455
+ step: typeof step === 'string' ? step.slice(0, 200) : null,
456
+ agent: typeof agent === 'string' ? agent.slice(0, 60) : null,
457
+ });
458
+ if (!task) {
459
+ res.status(404).json({ error: 'not_found' });
460
+ return;
461
+ }
462
+ broadcast({ type: 'task:progress', taskId: task.id, progress: task.metadata?.progress, step: task.metadata?.currentStep, agent: task.metadata?.progressAgent });
463
+ broadcast({ type: 'tasks:change', task });
464
+ res.json(task);
465
+ }));
466
+
467
+ return router;
468
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * src/server/routes/themes.mjs
3
+ *
4
+ * /api/themes — list saved themes
5
+ * /api/themes (POST) — add a named theme
6
+ * /api/themes/:name (DELETE) — remove
7
+ *
8
+ * Themes live on the state object (not disk) — the registry is
9
+ * in-memory. The active theme is on settings.json; the named
10
+ * themes here are the user's saved palette presets.
11
+ */
12
+ import { Router } from 'express';
13
+ import { wrap } from './_shared.mjs';
14
+
15
+ /**
16
+ * @param {object} deps
17
+ * @param {object} deps.state
18
+ * @returns {import('express').Router}
19
+ */
20
+ export function createThemesRouter({ state }) {
21
+ const router = Router();
22
+
23
+ router.get('/themes', wrap(async (_req, res) => {
24
+ res.json(state.getThemes());
25
+ }));
26
+
27
+ router.post('/themes', wrap(async (req, res) => {
28
+ const { name, colors } = req.body || {};
29
+ if (typeof name !== 'string' || !name.trim()) {
30
+ res.status(400).json({ error: 'bad_request', message: 'name is required' });
31
+ return;
32
+ }
33
+ if (!colors || typeof colors !== 'object') {
34
+ res.status(400).json({ error: 'bad_request', message: 'colors object is required' });
35
+ return;
36
+ }
37
+ const out = state.addTheme(name.trim().slice(0, 60), colors);
38
+ res.status(201).json(out);
39
+ }));
40
+
41
+ router.delete('/themes/:name', wrap(async (req, res) => {
42
+ const out = state.removeTheme(decodeURIComponent(req.params.name));
43
+ if (!out.removed) {
44
+ res.status(404).json({ error: 'not_found' });
45
+ return;
46
+ }
47
+ res.status(204).end();
48
+ }));
49
+
50
+ return router;
51
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * src/server/routes-v2/auth.mjs
3
+ *
4
+ * v0.7.0-alpha.1 — HTTP Basic auth middleware for /api/v2/* routes.
5
+ *
6
+ * Username: `opencode` (per opencode convention used elsewhere in Bizar).
7
+ * Password: the dashboard-generated secret in ~/.cache/bizarharness/dash-auth.json.
8
+ */
9
+
10
+ import { Buffer } from 'node:buffer';
11
+
12
+ export const V2_AUTH_REALM = 'Bizar Dashboard v2';
13
+
14
+ export function v2BasicAuth(getPassword) {
15
+ return function (req, res, next) {
16
+ const header = req.headers?.authorization ?? req.headers?.Authorization;
17
+ if (!header || typeof header !== 'string' || !header.startsWith('Basic ')) {
18
+ res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
19
+ return res.status(401).json({
20
+ name: 'DashboardError',
21
+ data: { statusCode: 401, message: 'Missing Authorization header' },
22
+ });
23
+ }
24
+
25
+ const expected = getPassword();
26
+ if (typeof expected !== 'string' || expected.length === 0) {
27
+ return res.status(500).json({
28
+ name: 'DashboardError',
29
+ data: { statusCode: 500, message: 'Dashboard auth not initialized' },
30
+ });
31
+ }
32
+
33
+ let decoded;
34
+ try {
35
+ const b64 = header.slice('Basic '.length);
36
+ decoded = Buffer.from(b64, 'base64').toString('utf-8');
37
+ } catch {
38
+ res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
39
+ return res.status(401).json({
40
+ name: 'DashboardError',
41
+ data: { statusCode: 401, message: 'Malformed Authorization header' },
42
+ });
43
+ }
44
+
45
+ const colonIdx = decoded.indexOf(':');
46
+ const user = colonIdx === -1 ? decoded : decoded.slice(0, colonIdx);
47
+ const pass = colonIdx === -1 ? '' : decoded.slice(colonIdx + 1);
48
+
49
+ if (user !== 'opencode' || pass !== expected) {
50
+ res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
51
+ return res.status(401).json({
52
+ name: 'DashboardError',
53
+ data: { statusCode: 401, message: 'Invalid credentials' },
54
+ });
55
+ }
56
+
57
+ return next();
58
+ };
59
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * src/server/routes-v2/events.mjs
3
+ *
4
+ * v0.7.0-alpha.1 — SSE event stream + publish endpoint.
5
+ *
6
+ * GET /api/v2/event — subscribe (text/event-stream, sends dashboard.connected first)
7
+ * POST /api/v2/event — publish (returns 204)
8
+ *
9
+ * Matches the opencode SSE wire format:
10
+ * event: <type>
11
+ * data: {"type": "<type>", "properties": {...}}
12
+ *
13
+ * Replay buffer: last 100 events, accessible via `?since=<seq>`.
14
+ */
15
+
16
+ import express from 'express';
17
+
18
+ export function createV2EventsRouter({ eventBus }) {
19
+ const router = express.Router();
20
+
21
+ router.get('/event', (req, res) => {
22
+ res.status(200);
23
+ res.setHeader('Content-Type', 'text/event-stream');
24
+ res.setHeader('Cache-Control', 'no-cache');
25
+ res.setHeader('Connection', 'keep-alive');
26
+ res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering
27
+ res.flushHeaders?.();
28
+
29
+ // First event: dashboard.connected.
30
+ writeSseEvent(res, { type: 'dashboard.connected', properties: {} });
31
+
32
+ const sinceRaw = req.query?.since;
33
+ const since = typeof sinceRaw === 'string' ? Number.parseInt(sinceRaw, 10) : undefined;
34
+ const validSince = Number.isFinite(since) ? since : undefined;
35
+
36
+ const sub = eventBus.subscribe({ since: validSince });
37
+
38
+ // Async pump: pull events from sub.events and write them to res.
39
+ const pump = (async () => {
40
+ try {
41
+ for await (const record of sub.events) {
42
+ if (res.writableEnded || res.destroyed) break;
43
+ writeSseEvent(res, record);
44
+ }
45
+ } catch (err) {
46
+ if (!res.writableEnded) {
47
+ try {
48
+ res.end();
49
+ } catch {
50
+ /* ignore */
51
+ }
52
+ }
53
+ }
54
+ })();
55
+
56
+ // Heartbeat every 25s to keep the connection alive through proxies.
57
+ const heartbeat = setInterval(() => {
58
+ if (res.writableEnded || res.destroyed) {
59
+ clearInterval(heartbeat);
60
+ return;
61
+ }
62
+ try {
63
+ res.write(': keepalive\n\n');
64
+ } catch {
65
+ clearInterval(heartbeat);
66
+ }
67
+ }, 25_000);
68
+
69
+ req.on('close', () => {
70
+ clearInterval(heartbeat);
71
+ sub.close();
72
+ });
73
+
74
+ // Don't await pump — it's a long-lived stream.
75
+ void pump;
76
+ });
77
+
78
+ router.post('/event', (req, res) => {
79
+ const body = req.body;
80
+ if (!body || typeof body !== 'object' || typeof body.type !== 'string') {
81
+ return res.status(400).json({
82
+ name: 'DashboardError',
83
+ data: { statusCode: 400, message: 'Event must have a string `type` field' },
84
+ });
85
+ }
86
+ try {
87
+ eventBus.publish({ type: body.type, properties: body.properties ?? {} });
88
+ } catch (err) {
89
+ return res.status(400).json({
90
+ name: 'DashboardError',
91
+ data: { statusCode: 400, message: err instanceof Error ? err.message : String(err) },
92
+ });
93
+ }
94
+ return res.status(204).end();
95
+ });
96
+
97
+ return router;
98
+ }
99
+
100
+ function writeSseEvent(res, record) {
101
+ const payload = JSON.stringify({
102
+ type: record.type,
103
+ properties: record.properties ?? {},
104
+ });
105
+ res.write(`event: ${record.type}\n`);
106
+ res.write(`data: ${payload}\n\n`);
107
+ }