@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,89 @@
1
+ /**
2
+ * src/server/routes/misc.mjs
3
+ *
4
+ * Routes that don't fit cleanly into a single domain file. Kept
5
+ * here so we don't inflate the api.mjs composer with one-off
6
+ * routers.
7
+ *
8
+ * /api/search — fuzzy search across projects/tasks/artifacts/agents
9
+ * /api/tailscale/status — tailscale state
10
+ * /api/tailscale/enable (POST) — enable tailscale serve
11
+ * /api/tailscale/disable (POST) — disable
12
+ * /api/updates/status — current version
13
+ * /api/updates/check — latest + hasUpdates flag
14
+ * /api/updates/apply (POST) — start update (broadcasts progress)
15
+ */
16
+ import { Router } from 'express';
17
+ import { searchStore } from '../search-store.mjs';
18
+ import { tailscaleStore } from '../tailscale-store.mjs';
19
+ import { updateStore } from '../update-store.mjs';
20
+ import { projectsStore } from '../projects-store.mjs';
21
+ import { wrap } from './_shared.mjs';
22
+
23
+ /**
24
+ * @param {object} deps
25
+ * @param {object} deps.state
26
+ * @param {Function} deps.broadcast
27
+ * @returns {import('express').Router}
28
+ */
29
+ export function createMiscRouter({ state, broadcast }) {
30
+ const router = Router();
31
+
32
+ // ── /api/search ────────────────────────────────────────────────────────
33
+ router.get('/search', wrap(async (req, res) => {
34
+ const q = (req.query.q || '').toString();
35
+ const scope = (req.query.scope || 'all').toString();
36
+ const active = projectsStore.active();
37
+ res.json(searchStore.search(q, { activeProjectId: active?.id, scope }));
38
+ }));
39
+
40
+ // ── /api/tailscale ─────────────────────────────────────────────────────
41
+ router.get('/tailscale/status', wrap(async (_req, res) => {
42
+ res.json(await tailscaleStore.status());
43
+ }));
44
+
45
+ router.post('/tailscale/enable', wrap(async (req, res) => {
46
+ res.json(await tailscaleStore.enable(req.body || {}));
47
+ }));
48
+
49
+ router.post('/tailscale/disable', wrap(async (_req, res) => {
50
+ res.json(await tailscaleStore.disable());
51
+ }));
52
+
53
+ // ── /api/updates (v3.3.3) ──────────────────────────────────────────
54
+ router.get('/updates/status', wrap(async (_req, res) => {
55
+ res.json({ current: updateStore.current() });
56
+ }));
57
+
58
+ router.get('/updates/check', wrap(async (_req, res) => {
59
+ try {
60
+ const current = updateStore.current();
61
+ const latest = await updateStore.latest();
62
+ res.json({
63
+ current,
64
+ latest,
65
+ hasUpdates: updateStore.hasUpdates(current, latest),
66
+ });
67
+ } catch (err) {
68
+ res.status(500).json({ error: 'check_failed', message: err.message });
69
+ }
70
+ }));
71
+
72
+ router.post('/updates/apply', async (req, res) => {
73
+ const requested = Array.isArray(req.body?.packages) ? req.body.packages : ['bizar', 'bizar-dash', 'bizar-plugin'];
74
+ const packages = requested.filter((pkg) => ['bizar', 'bizar-dash', 'bizar-plugin'].includes(String(pkg)));
75
+ if (packages.length === 0) {
76
+ res.status(400).json({ error: 'bad_request', message: 'packages must include at least one known package id' });
77
+ return;
78
+ }
79
+ // Start the update in the background — return immediately so the client
80
+ // can receive progress events over the WebSocket channel.
81
+ updateStore.applyWithProgress({
82
+ packages,
83
+ broadcast,
84
+ }).catch((err) => console.error('[updates] error:', err));
85
+ res.json({ started: true, packages });
86
+ });
87
+
88
+ return router;
89
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * src/server/routes/mods.mjs
3
+ *
4
+ * /api/mods — list
5
+ * /api/mods/registry — fetch available mods from public registry
6
+ * /api/mods/audit — tail the mod audit log
7
+ * /api/mods/:id — metadata
8
+ * /api/mods (POST) — install from path OR registry id
9
+ * /api/mods/:id (PUT) — enable/disable
10
+ * /api/mods/:id (DELETE) — uninstall
11
+ * /api/mods/:id/files — file tree
12
+ * /api/mods/:id/mod-file/* (GET) — read single file (named route to avoid :id/* clash)
13
+ * /api/mods/:id/mod-file/* (PUT) — write single file
14
+ * /api/mods/views — mod view registry
15
+ * /api/mods/:id/mod-web/* — serve files from mod's web/ dir
16
+ * /api/mods/:id/instructions — list installed instruction files (v3.20)
17
+ * /api/mods/:id/instructions/reinstall — force-reinstall instructions (v3.20)
18
+ * /api/mods/:id/upgrade — upgrade an installed mod from the registry (v3.20.5)
19
+ * /api/mods/:id/views/* — serve a mod-shipped React view (v3.20.5)
20
+ *
21
+ * Express-ordering note: /mods/views MUST come before /mods/:id/* —
22
+ * actually only the literal /mods (without :id) takes precedence
23
+ * over /mods/:id, but /mods/views is the same shape as /mods/:id so
24
+ * we declare it before /mods/:id to be safe.
25
+ */
26
+ import { Router } from 'express';
27
+ import { existsSync, readFileSync, statSync } from 'node:fs';
28
+ import { resolve, relative, join } from 'node:path';
29
+ import { homedir } from 'node:os';
30
+ import { modsLoader } from '../mods-loader.mjs';
31
+ import { wrap } from './_shared.mjs';
32
+
33
+ /**
34
+ * Read a file at an absolute path or under $HOME. Returns null if
35
+ * the file doesn't exist or can't be read. The path argument may
36
+ * use `~` as a home-relative shortcut (resolved against homedir()).
37
+ */
38
+ function tryRead(p) {
39
+ try {
40
+ const full = p.startsWith('~/') || p === '~' ? join(homedir(), p.slice(1)) : p;
41
+ if (!existsSync(full)) return null;
42
+ return readFileSync(full, 'utf8');
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @returns {import('express').Router}
50
+ */
51
+ export function createModsRouter() {
52
+ const router = Router();
53
+
54
+ router.get('/mods', wrap(async (_req, res) => {
55
+ res.json({ mods: modsLoader.list() });
56
+ }));
57
+
58
+ // /mods/registry MUST come before /mods/:id — otherwise Express would
59
+ // match "registry" as an :id value.
60
+ router.get('/mods/registry', wrap(async (_req, res) => {
61
+ const installed = new Set(modsLoader.list().map((m) => m.id));
62
+ try {
63
+ const registry = await modsLoader.fetchRegistry();
64
+ // Annotate each entry with installed flag and (if installed) the
65
+ // local version so the dashboard can show upgrade hints.
66
+ const mods = (registry.mods || []).map((m) => {
67
+ const local = installed.has(m.id) ? modsLoader.get(m.id) : null;
68
+ return {
69
+ ...m,
70
+ installed: local !== null,
71
+ installedVersion: local ? local.version : null,
72
+ upgradeAvailable:
73
+ local && local.version !== m.latest ? m.latest : null,
74
+ };
75
+ });
76
+ res.json({
77
+ registry: {
78
+ version: registry.version,
79
+ updatedAt: registry.updatedAt,
80
+ source: modsLoader.getRegistryUrl(),
81
+ },
82
+ mods,
83
+ });
84
+ } catch (err) {
85
+ res.status(502).json({
86
+ error: 'registry_unreachable',
87
+ message: err.message,
88
+ registryUrl: modsLoader.getRegistryUrl(),
89
+ });
90
+ }
91
+ }));
92
+
93
+ // /mods/audit — tail the mod audit log. Filters by mod id if provided.
94
+ router.get('/mods/audit', wrap(async (req, res) => {
95
+ const logPath = join(homedir(), '.cache', 'bizar', 'logs', 'mod-audit.log');
96
+ const modFilter = req.query?.mod;
97
+ const limit = Math.min(parseInt(req.query?.limit || '200', 10) || 200, 2000);
98
+ if (!existsSync(logPath)) {
99
+ res.json({ entries: [], logPath, modFilter });
100
+ return;
101
+ }
102
+ let text;
103
+ try {
104
+ text = readFileSync(logPath, 'utf8');
105
+ } catch (err) {
106
+ res.status(500).json({ error: 'read_failed', message: err.message });
107
+ return;
108
+ }
109
+ const lines = text.split(/\r?\n/).filter(Boolean).reverse();
110
+ const out = [];
111
+ for (const line of lines) {
112
+ if (out.length >= limit) break;
113
+ try {
114
+ const entry = JSON.parse(line);
115
+ if (modFilter && entry.mod !== modFilter) continue;
116
+ out.push(entry);
117
+ } catch {
118
+ /* skip malformed lines */
119
+ }
120
+ }
121
+ res.json({ entries: out.reverse(), logPath, modFilter });
122
+ }));
123
+
124
+ // /mods/views MUST come before /mods/:id — otherwise Express would
125
+ // match "views" as an :id value.
126
+ router.get('/mods/views', wrap(async (_req, res) => {
127
+ const views = modsLoader.listModViews();
128
+ res.json({ views });
129
+ }));
130
+
131
+ router.get('/mods/:id', wrap(async (req, res) => {
132
+ const mod = modsLoader.get(req.params.id);
133
+ if (!mod) {
134
+ res.status(404).json({ error: 'not_found' });
135
+ return;
136
+ }
137
+ res.json(mod);
138
+ }));
139
+
140
+ router.post('/mods', wrap(async (req, res) => {
141
+ // Two install paths:
142
+ // { path: "/local/path" } — install from a local directory
143
+ // { id: "registry-id" } — install from the public registry
144
+ if (req.body?.id) {
145
+ try {
146
+ const mod = await modsLoader.installFromRegistry(req.body.id);
147
+ res.status(201).json(mod);
148
+ } catch (err) {
149
+ res.status(500).json({ error: 'install_failed', message: err.message });
150
+ }
151
+ return;
152
+ }
153
+ const path = req.body?.path;
154
+ if (!path) {
155
+ res.status(400).json({
156
+ error: 'bad_request',
157
+ message: 'either "id" (registry install) or "path" (local install) is required',
158
+ });
159
+ return;
160
+ }
161
+ try {
162
+ const mod = modsLoader.installFromPath(path);
163
+ res.status(201).json(mod);
164
+ } catch (err) {
165
+ res.status(500).json({ error: 'install_failed', message: err.message });
166
+ }
167
+ }));
168
+
169
+ router.put('/mods/:id', wrap(async (req, res) => {
170
+ const mod = modsLoader.setEnabled(req.params.id, !!req.body?.enabled);
171
+ if (!mod) {
172
+ res.status(404).json({ error: 'not_found' });
173
+ return;
174
+ }
175
+ res.json(mod);
176
+ }));
177
+
178
+ router.delete('/mods/:id', wrap(async (req, res) => {
179
+ const ok = modsLoader.uninstall(req.params.id);
180
+ if (!ok) {
181
+ res.status(404).json({ error: 'not_found' });
182
+ return;
183
+ }
184
+ res.status(204).end();
185
+ }));
186
+
187
+ router.get('/mods/:id/files', wrap(async (req, res) => {
188
+ res.json({ files: modsLoader.listFiles(req.params.id) });
189
+ }));
190
+
191
+ // NOTE: we use /mods/:id/mod-file/* (named route) to avoid conflicting
192
+ // with mod route mounting at /api/mods/:id/*. The wildcard in /* was
193
+ // too greedy and captured paths like /mods/test-mod/hello.
194
+ router.get('/mods/:id/mod-file/*', wrap(async (req, res) => {
195
+ const rel = req.params[0] || '';
196
+ const content = modsLoader.readFile(req.params.id, rel);
197
+ if (content === null) {
198
+ res.status(404).json({ error: 'not_found' });
199
+ return;
200
+ }
201
+ res.type('text/plain').send(content);
202
+ }));
203
+
204
+ router.put('/mods/:id/mod-file/*', wrap(async (req, res) => {
205
+ const rel = req.params[0] || '';
206
+ const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body, null, 2);
207
+ modsLoader.writeFile(req.params.id, rel, body);
208
+ res.json({ ok: true });
209
+ }));
210
+
211
+ // ── v3.20 — Mod instructions (agents/commands/skills installed by this mod)
212
+ router.get('/mods/:id/instructions', wrap(async (req, res) => {
213
+ const mod = modsLoader.get(req.params.id);
214
+ if (!mod) {
215
+ res.status(404).json({ error: 'not_found' });
216
+ return;
217
+ }
218
+ const installed = modsLoader.listModInstructions(req.params.id);
219
+ // For each installed file, try to read its content so the UI can
220
+ // preview it without an extra round-trip.
221
+ const annotated = {
222
+ agents: installed.agents.map((f) => ({
223
+ filename: f,
224
+ path: join('agents', f),
225
+ fullPath: `~/.config/opencode/agents/${f}`,
226
+ content: tryRead(`~/.config/opencode/agents/${f}`),
227
+ })),
228
+ commands: installed.commands.map((f) => ({
229
+ filename: f,
230
+ path: join('commands', f),
231
+ fullPath: `~/.config/opencode/commands/${f}`,
232
+ content: tryRead(`~/.config/opencode/commands/${f}`),
233
+ })),
234
+ skills: installed.skills.map((d) => ({
235
+ name: d,
236
+ fullPath: `~/.opencode/skills/${d}`,
237
+ content: tryRead(`~/.opencode/skills/${d}/SKILL.md`),
238
+ })),
239
+ };
240
+ res.json({
241
+ modId: req.params.id,
242
+ modName: mod.name,
243
+ total: annotated.agents.length + annotated.commands.length + annotated.skills.length,
244
+ ...annotated,
245
+ });
246
+ }));
247
+
248
+ // v3.20 — Force-reinstall the mod's instructions into opencode config
249
+ // (useful after editing files inside the mod's agents/commands/skills).
250
+ router.post('/mods/:id/instructions/reinstall', wrap(async (req, res) => {
251
+ const counts = modsLoader.reinstallInstructions(req.params.id);
252
+ if (counts === null) {
253
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not installed` });
254
+ return;
255
+ }
256
+ res.json({ ok: true, counts });
257
+ }));
258
+
259
+ // v3.20.5 — Upgrade an installed mod from the registry. Body opts:
260
+ // { backup?: boolean, url?: string }
261
+ // backup: if true, copy the existing mod folder into
262
+ // ~/.config/bizar/mods/.backup/<id>-<timestamp>/ before replacing.
263
+ // url: optional override for the registry URL.
264
+ // Returns { from, to, backupPath?, mod } on success.
265
+ router.post('/mods/:id/upgrade', wrap(async (req, res) => {
266
+ try {
267
+ const result = await modsLoader.upgradeFromRegistry(req.params.id, {
268
+ backup: !!req.body?.backup,
269
+ url: typeof req.body?.url === 'string' ? req.body.url : undefined,
270
+ });
271
+ res.json({
272
+ ok: true,
273
+ modId: req.params.id,
274
+ from: result.from,
275
+ to: result.to,
276
+ backupPath: result.backupPath,
277
+ mod: result.mod,
278
+ });
279
+ } catch (err) {
280
+ const code = /not installed/i.test(err.message) ? 404 : 500;
281
+ res.status(code).json({ error: 'upgrade_failed', message: err.message });
282
+ }
283
+ }));
284
+
285
+ // ── /api/mods/:id/mod-web/* and /api/mods/:id/web/* ────────────────
286
+ // Serve files from each mod's web/ directory (for iframe embedding).
287
+ // Two aliases — `mod-web` (explicit, won't collide with mod routes) and
288
+ // bare `web` (intuitive, what users actually type). They serve identical
289
+ // content so the mod author can use either.
290
+ const webHandler = wrap(async (req, res) => {
291
+ const mod = modsLoader.get(req.params.id);
292
+ if (!mod || !mod.enabled) {
293
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
294
+ return;
295
+ }
296
+ const rel = req.params[0] || '';
297
+ const webRoot = resolve(mod.path, 'web');
298
+ const filePath = resolve(webRoot, rel);
299
+ const relPath = relative(webRoot, filePath);
300
+ if (relPath.startsWith('..') || relPath === '') {
301
+ res.status(403).json({ error: 'forbidden' });
302
+ return;
303
+ }
304
+ if (!existsSync(filePath)) {
305
+ res.status(404).json({ error: 'not_found', message: `file not found: ${rel}` });
306
+ return;
307
+ }
308
+ res.sendFile(filePath);
309
+ });
310
+ router.get('/mods/:id/mod-web/*', webHandler);
311
+ router.get('/mods/:id/web/*', webHandler);
312
+
313
+ // v3.20.5 — Serve a mod-shipped React view as an ES module. Mods
314
+ // declare view components in views/registry.json (e.g.
315
+ // { id: "graph", component: "GraphView.js" }
316
+ // ) and ship the built file at views/<component>. The dashboard
317
+ // dynamically imports it on demand and renders it inside ModView.
318
+ //
319
+ // The file is served with Content-Type: application/javascript so
320
+ // the browser treats it as a real ES module. Mods are expected to
321
+ // ship pre-built code (no JSX) and use React.createElement (or htm
322
+ // with React.createElement) for composition. React itself is loaded
323
+ // as part of the dashboard bundle — see /assets/*.js on first load.
324
+ const viewsHandler = wrap(async (req, res) => {
325
+ const mod = modsLoader.get(req.params.id);
326
+ if (!mod || !mod.enabled) {
327
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
328
+ return;
329
+ }
330
+ const rel = req.params[0] || '';
331
+ if (!rel || rel.endsWith('/')) {
332
+ res.status(400).json({ error: 'bad_request', message: 'file path required' });
333
+ return;
334
+ }
335
+ // Allow only .js / .mjs / .jsx / .tsx files — anything else is
336
+ // a misconfiguration or attack attempt.
337
+ if (!/\.(js|mjs|jsx|tsx)$/i.test(rel)) {
338
+ res.status(403).json({ error: 'forbidden', message: 'only JS-family files may be served as views' });
339
+ return;
340
+ }
341
+ const viewsRoot = resolve(mod.path, 'views');
342
+ const filePath = resolve(viewsRoot, rel);
343
+ const relPath = relative(viewsRoot, filePath);
344
+ if (relPath.startsWith('..') || relPath === '') {
345
+ res.status(403).json({ error: 'forbidden' });
346
+ return;
347
+ }
348
+ if (!existsSync(filePath)) {
349
+ res.status(404).json({ error: 'not_found', message: `view not found: ${rel}` });
350
+ return;
351
+ }
352
+ res.set('Content-Type', 'application/javascript; charset=utf-8');
353
+ res.set('Cache-Control', 'no-store');
354
+ res.sendFile(filePath);
355
+ });
356
+ router.get('/mods/:id/views/*', viewsHandler);
357
+
358
+ return router;
359
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * src/server/routes/notifications.mjs
3
+ *
4
+ * /api/notifications — list (?unread=true to filter)
5
+ * /api/notifications/:id/read (POST) — mark one as read
6
+ * /api/notifications/read-all (POST) — mark every as read
7
+ * /api/notifications/:id (DELETE) — dismiss
8
+ */
9
+ import { Router } from 'express';
10
+ import { notificationsStore } from '../notifications-store.mjs';
11
+ import { wrap } from './_shared.mjs';
12
+
13
+ /**
14
+ * @param {object} deps
15
+ * @param {Function} deps.broadcast
16
+ * @returns {import('express').Router}
17
+ */
18
+ export function createNotificationsRouter({ broadcast }) {
19
+ const router = Router();
20
+
21
+ // v3.3.0 — Per-user notification stream. Backed by an append-only
22
+ // JSONL log at ~/.config/bizar/notifications.jsonl. Read state is
23
+ // persisted separately at notifications.read.json.
24
+ router.get('/notifications', wrap(async (req, res) => {
25
+ const unread = req.query.unread === 'true' || req.query.unread === '1';
26
+ const limit = Math.min(500, Math.max(1, parseInt(req.query.limit || '200', 10) || 200));
27
+ const items = notificationsStore.list({ unread, limit });
28
+ res.json({ notifications: items, stats: notificationsStore.stats() });
29
+ }));
30
+
31
+ router.post('/notifications/:id/read', wrap(async (req, res) => {
32
+ const ok = notificationsStore.markRead(req.params.id);
33
+ if (ok) broadcast({ type: 'notifications:change' });
34
+ res.json({ ok });
35
+ }));
36
+
37
+ router.post('/notifications/read-all', wrap(async (_req, res) => {
38
+ const marked = notificationsStore.markAllRead();
39
+ broadcast({ type: 'notifications:change' });
40
+ res.json({ ok: true, marked });
41
+ }));
42
+
43
+ router.delete('/notifications/:id', wrap(async (req, res) => {
44
+ const ok = notificationsStore.remove(req.params.id);
45
+ if (!ok) {
46
+ res.status(404).json({ error: 'not_found' });
47
+ return;
48
+ }
49
+ broadcast({ type: 'notifications:change' });
50
+ res.status(204).end();
51
+ }));
52
+
53
+ return router;
54
+ }