@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,796 @@
1
+ /**
2
+ * src/server/server.mjs
3
+ *
4
+ * v3.0.0 — Express + WebSocket server for the Bizar dashboard.
5
+ *
6
+ * Wires the v3 API router, the file watcher, the WebSocket layer, and
7
+ * the static frontend (Vite-built React SPA from `dist/`) into a single
8
+ * HTTP + WS pair.
9
+ */
10
+ import express from 'express';
11
+ import cors from 'cors';
12
+ import { WebSocketServer } from 'ws';
13
+ import { createServer as createHttpServer } from 'node:http';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, join } from 'node:path';
16
+ import { existsSync, readFileSync, statSync, openSync, readSync, closeSync, readdirSync } from 'node:fs';
17
+ import { dirname as pathDirname } from 'node:path';
18
+ import { createApiRouter } from './api.mjs';
19
+ import { createState } from './state.mjs';
20
+ import { createWatcher } from './watcher.mjs';
21
+ import { modsLoader } from './mods-loader.mjs';
22
+ import { projectsStore } from './projects-store.mjs';
23
+ import { agentsStore } from './agents-store.mjs';
24
+ import { tasksStore } from './tasks-store.mjs';
25
+ import { schedulesStore } from './schedules-store.mjs';
26
+ import { providersStore, mcpsStore } from './providers-store.mjs';
27
+ import { homedir } from 'node:os';
28
+ import { startBgPoller, stopBgPoller } from './bg-poller.mjs';
29
+ import { startBgRetryLoop, stopBgRetryLoop } from './bg-retry.mjs';
30
+ import { startDialogPoller } from './dialog-poller.mjs';
31
+ import {
32
+ checkWebSocketAuth,
33
+ isAllowedDashboardOrigin,
34
+ isAllowedDashboardOriginForRequest,
35
+ } from './auth.mjs';
36
+ import { readSettings } from './routes/_shared.mjs';
37
+ import { buildAllowedRootsFromSettings, resolveSafePath } from './lib/path-safe.mjs';
38
+ import { V2EventBus } from './v2-event-bus.mjs';
39
+ import { loadOrCreateAuth, V2_DEFAULT_PORT } from './v2-auth-file.mjs';
40
+ import { createV2Router } from './routes-v2/index.mjs';
41
+
42
+ let processHandlersInstalled = false;
43
+ let v2Bus = null;
44
+ let v2Auth = null;
45
+
46
+ function installProcessHandlers() {
47
+ if (processHandlersInstalled) return;
48
+ processHandlersInstalled = true;
49
+ // Catch-all to prevent server crash on unhandled rejections
50
+ process.on('unhandledRejection', (err) => {
51
+ console.error('[unhandledRejection]', err);
52
+ });
53
+ process.on('uncaughtException', (err) => {
54
+ console.error('[uncaughtException]', err);
55
+ // For uncaughtException, the process state is uncertain. Log and continue
56
+ // unless it's a fatal error. Don't exit.
57
+ });
58
+ }
59
+
60
+ const __dirname = dirname(fileURLToPath(import.meta.url));
61
+ // server.mjs lives at src/server/ — dist/ is at the package root
62
+ const DIST_DIR = join(__dirname, '..', '..', 'dist');
63
+ const WS_BACKPRESSURE_LIMIT_BYTES = 1024 * 1024;
64
+ const MOBILE_UA_RE = /Android.+Mobile|iPhone|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|IEMobile/i;
65
+
66
+ let currentBroadcast = () => {};
67
+
68
+ export function broadcast(msg) {
69
+ return currentBroadcast(msg);
70
+ }
71
+
72
+ /**
73
+ * Publish an event on the v2 event bus (if initialized). Exposed for
74
+ * other parts of the dashboard server (e.g. bg-poller, dialog-poller)
75
+ * to push events into the v2 SSE stream without taking a direct
76
+ * dependency on the bus module.
77
+ */
78
+ export function publishV2Event(event) {
79
+ if (!v2Bus) return -1;
80
+ return v2Bus.publish(event);
81
+ }
82
+
83
+ /**
84
+ * Return v2 auth info for the plugin to consume (e.g. via a startup
85
+ * handshake or env var). Password is the same one persisted to the
86
+ * auth file.
87
+ */
88
+ export function getV2Auth() {
89
+ if (!v2Auth) return null;
90
+ return {
91
+ baseUrl: v2Auth.baseUrl,
92
+ port: v2Auth.port,
93
+ password: v2Auth.password,
94
+ file: v2Auth.file,
95
+ };
96
+ }
97
+
98
+ function shouldRedirectToMobile(req) {
99
+ if (req.method !== 'GET') return false;
100
+ if (req.path !== '/') return false;
101
+ if (req.query?.desktop === '1') return false;
102
+ const accept = req.headers.accept || '';
103
+ if (typeof accept === 'string' && !accept.includes('text/html')) return false;
104
+ const ua = req.headers['user-agent'] || '';
105
+ return typeof ua === 'string' && MOBILE_UA_RE.test(ua);
106
+ }
107
+
108
+ function mobileRedirectTarget(req) {
109
+ const params = new URLSearchParams();
110
+ for (const [key, value] of Object.entries(req.query || {})) {
111
+ if (key === 'desktop') continue;
112
+ if (Array.isArray(value)) {
113
+ for (const item of value) params.append(key, String(item));
114
+ } else if (value != null) {
115
+ params.set(key, String(value));
116
+ }
117
+ }
118
+ const qs = params.toString();
119
+ return qs ? `/m?${qs}` : '/m';
120
+ }
121
+
122
+ /**
123
+ * @param {object} opts
124
+ * @param {number} opts.port
125
+ * @param {string} opts.projectRoot
126
+ * @param {string} opts.opencodeConfigDir
127
+ * @param {string} opts.bizarRoot
128
+ */
129
+ export async function createServer({
130
+ port,
131
+ projectRoot,
132
+ opencodeConfigDir,
133
+ bizarRoot,
134
+ }) {
135
+ installProcessHandlers();
136
+ const app = express();
137
+ app.disable('x-powered-by');
138
+ // v3.5.4 (CORS) — Reflect the request Origin back as
139
+ // Access-Control-Allow-Origin so the Vite dev server (5174), a tunneled
140
+ // remote, or a localhost:4321 same-origin tab all work. We also allow
141
+ // credentials so the dashboard can keep using cookie-style pair tokens.
142
+ // This is a local tool — same-origin is the norm; reflection is the
143
+ // simplest correct policy.
144
+ app.use(cors({
145
+ origin(origin, callback) {
146
+ if (!origin) {
147
+ callback(null, true);
148
+ return;
149
+ }
150
+ const allowed = isAllowedDashboardOrigin(origin);
151
+ callback(allowed ? null : new Error('origin_not_allowed'), allowed);
152
+ },
153
+ credentials: true,
154
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
155
+ allowedHeaders: ['Content-Type', 'Authorization'],
156
+ }));
157
+ app.use(express.json({ limit: '2mb' }));
158
+ app.use((_req, res, next) => {
159
+ res.setHeader('X-Content-Type-Options', 'nosniff');
160
+ next();
161
+ });
162
+
163
+ app.use(
164
+ (
165
+ err,
166
+ _req,
167
+ res,
168
+ _next, // eslint-disable-line no-unused-vars
169
+ ) => {
170
+ const status = err?.status || err?.statusCode || 400;
171
+ res.status(status).json({
172
+ error: err?.type || 'bad_request',
173
+ message: err?.message || String(err),
174
+ });
175
+ },
176
+ );
177
+
178
+ const state = createState({ projectRoot, opencodeConfigDir, bizarRoot });
179
+
180
+ // v3.0.4 — Auto-detect the user's cwd as a project on startup. This is
181
+ // idempotent and safe to call on every boot. The first time a user runs
182
+ // the dashboard, BizarHarness/ (or whatever their cwd is) shows up as
183
+ // a project without any manual setup.
184
+ try {
185
+ const detected = projectsStore.autoDetect({ cwd: projectRoot });
186
+ if (detected) {
187
+ // eslint-disable-next-line no-console
188
+ console.log(`[bizar-dash] auto-detected project: ${detected.id} (${detected.path})`);
189
+ }
190
+ } catch (err) {
191
+ // eslint-disable-next-line no-console
192
+ console.error('[bizar-dash] autoDetect failed:', err.message);
193
+ }
194
+
195
+ // v3.6.0 — If the operator has configured a `dashboard.projectsDirectory`,
196
+ // also scan it for project roots on startup. Fire-and-forget: a slow
197
+ // scan or a missing directory must not block the server from booting.
198
+ //
199
+ // v3.11.0 — Rebuild the allow-list from settings (home +
200
+ // projectsDirectory + dashboard.allowedRoots), log it once for
201
+ // debuggability, and re-validate the configured `projectsDirectory`
202
+ // against the allow-list before scanning. A tampered settings file
203
+ // cannot widen the boundary.
204
+ try {
205
+ const settings = readSettings();
206
+ const allowedRoots = buildAllowedRootsFromSettings({
207
+ settings: settings.data,
208
+ home: homedir(),
209
+ });
210
+ // eslint-disable-next-line no-console
211
+ console.log(
212
+ `[bizar-dash] projects-directory scan: rebuilt allow-list (${allowedRoots.length} root(s))`,
213
+ );
214
+ const configured = settings.data?.dashboard?.projectsDirectory;
215
+ if (typeof configured === 'string' && configured.trim()) {
216
+ const safeRoot = resolveSafePath(configured, allowedRoots);
217
+ if (!safeRoot) {
218
+ // eslint-disable-next-line no-console
219
+ console.warn(
220
+ `[bizar-dash] projects-directory scan: skipping — ` +
221
+ `"${configured}" is outside the allow-list`,
222
+ );
223
+ } else {
224
+ projectsStore.scanDirectory(safeRoot).then(
225
+ (result) => {
226
+ if (result.error) {
227
+ // eslint-disable-next-line no-console
228
+ console.warn(`[bizar-dash] projects-directory scan: ${result.error}`);
229
+ } else if (result.added && result.added.length > 0) {
230
+ // eslint-disable-next-line no-console
231
+ console.log(
232
+ `[bizar-dash] projects-directory scan: added ${result.added.length} ` +
233
+ `project(s) (skipped ${result.skipped}, scanned ${result.scanned})`,
234
+ );
235
+ }
236
+ },
237
+ (err) => {
238
+ // eslint-disable-next-line no-console
239
+ console.warn(`[bizar-dash] projects-directory scan failed: ${err?.message || err}`);
240
+ },
241
+ );
242
+ }
243
+ }
244
+ } catch (err) {
245
+ // eslint-disable-next-line no-console
246
+ console.warn('[bizar-dash] startup scan setup failed:', err?.message || err);
247
+ }
248
+
249
+ const watchPaths = [
250
+ state.paths.opencodeJson,
251
+ state.paths.agentsDir,
252
+ state.paths.commandsDir,
253
+ state.paths.bizarDir,
254
+ state.paths.plansDir,
255
+ state.paths.globalPlansDir,
256
+ join(opencodeConfigDir, 'projects.json'),
257
+ ].filter((p) => existsSafe(p));
258
+
259
+ const watcher = createWatcher({
260
+ paths: watchPaths,
261
+ onChange: (event, p) => {
262
+ wss.clients.forEach((client) => {
263
+ safeSend(client, JSON.stringify({ type: 'change', event, path: p, ts: Date.now() }));
264
+ });
265
+ },
266
+ });
267
+
268
+ const server = createHttpServer(app);
269
+ server.requestTimeout = 30_000;
270
+ server.headersTimeout = 35_000;
271
+ server.keepAliveTimeout = 5_000;
272
+ app.set('port', port);
273
+ // v3.6.0 — Bind the WS server's noServer mode so we can authenticate
274
+ // the upgrade ourselves instead of letting ws accept any TCP
275
+ // connection to /ws. The actual upgrade handling happens further
276
+ // down via server.on('upgrade', ...).
277
+ const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
278
+
279
+ function safeSend(client, payload) {
280
+ if (client.readyState !== 1) return false;
281
+ if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES) {
282
+ try {
283
+ client.terminate();
284
+ } catch {
285
+ /* ignore */
286
+ }
287
+ return false;
288
+ }
289
+ try {
290
+ client.send(payload);
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ function localBroadcast(msg) {
298
+ const payload = JSON.stringify(msg);
299
+ wss.clients.forEach((client) => {
300
+ safeSend(client, payload);
301
+ });
302
+ }
303
+ currentBroadcast = localBroadcast;
304
+
305
+ const apiRouter = await createApiRouter({
306
+ state,
307
+ watcher,
308
+ projectRoot,
309
+ opencodeConfigDir,
310
+ bizarRoot,
311
+ broadcast: localBroadcast,
312
+ });
313
+
314
+ // All /api/* routes go through apiRouter (after mod routes are checked).
315
+ // IMPORTANT: mount v2 router FIRST so `/api/v2/*` matches before
316
+ // apiRouter's internal 404 catch-all (api.mjs line ~109) can swallow it.
317
+ // v2 plugin protocol
318
+ // ──────────────────────────────────────────────────────────────────
319
+ // HTTP+SSE bridge sourced from .bizar/research/OPENAPI_SPEC.yaml.
320
+ // Mounted at /api/v2/* to avoid colliding with existing /api/* routes.
321
+ // Auth: HTTP Basic (see v2-auth-file.mjs). Password persisted at
322
+ // ~/.cache/bizarharness/dash-auth.json (mode 0600).
323
+ try {
324
+ v2Auth = loadOrCreateAuth({ port: V2_DEFAULT_PORT });
325
+ v2Bus = new V2EventBus({ logger: console });
326
+ // eslint-disable-next-line no-console
327
+ console.log(
328
+ `[v2] auth file: ${v2Auth.file} (port ${v2Auth.port}, password len ${v2Auth.password.length})`,
329
+ );
330
+ app.use(
331
+ '/api/v2',
332
+ createV2Router({
333
+ eventBus: v2Bus,
334
+ getPassword: () => v2Auth.password,
335
+ version: '0.7.0-alpha.1',
336
+ startedAt: Date.now(),
337
+ }),
338
+ );
339
+ } catch (err) {
340
+ // eslint-disable-next-line no-console
341
+ console.warn('[v2] failed to initialize:', err?.message || err);
342
+ }
343
+
344
+ // v3.6.2 — Auth now wraps mod routes too. Previously mods mounted at
345
+ // `/api/mods/<id>` BEFORE `requireAuth`, which meant every mod route
346
+ // silently bypassed bearer-token checks.
347
+ const { requireAuth } = await import('./auth.mjs');
348
+ app.use('/api', requireAuth({ skipPaths: ['/auth/status', '/pair/verify', '/v2/health', '/v2/doc'] }));
349
+
350
+ // ── Mod route mounting ──────────────────────────────────────────────
351
+ {
352
+ const modCtx = { broadcast: localBroadcast, state, projectRoot, opencodeConfigDir };
353
+ const modRouters = await modsLoader.loadModRouters(modCtx);
354
+ for (const { id, router: modRouter, mountPath } of modRouters) {
355
+ app.use(mountPath, modRouter);
356
+ // eslint-disable-next-line no-console
357
+ console.log(`[mod] mounted ${id} routes at ${mountPath}`);
358
+ }
359
+ }
360
+
361
+ // All /api/* routes go through apiRouter (after mod routes are checked)
362
+ app.use('/api', apiRouter);
363
+
364
+ // v3.6.0 — Authenticate WebSocket upgrades. The wss is in
365
+ // `noServer: true` mode above, so this handler is the gate. We
366
+ // reject with 401 if the token is missing/invalid; otherwise we
367
+ // hand off to ws.handleUpgrade so the existing wss.on('connection')
368
+ // listener below receives the new socket.
369
+ server.on('upgrade', (req, socket, head) => {
370
+ const url = req.url || '';
371
+ if (!url.startsWith('/ws')) {
372
+ socket.destroy();
373
+ return;
374
+ }
375
+ if (!isAllowedDashboardOriginForRequest(req)) {
376
+ try {
377
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
378
+ socket.destroy();
379
+ } catch {
380
+ /* ignore */
381
+ }
382
+ return;
383
+ }
384
+ if (!checkWebSocketAuth(req)) {
385
+ try {
386
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
387
+ socket.destroy();
388
+ } catch {
389
+ /* ignore */
390
+ }
391
+ return;
392
+ }
393
+ wss.handleUpgrade(req, socket, head, (ws) => {
394
+ wss.emit('connection', ws, req);
395
+ });
396
+ });
397
+
398
+ const heartbeatInterval = setInterval(() => {
399
+ wss.clients.forEach((client) => {
400
+ if (client.isAlive === false) {
401
+ try {
402
+ client.terminate();
403
+ } catch {
404
+ /* ignore */
405
+ }
406
+ return;
407
+ }
408
+ client.isAlive = false;
409
+ try {
410
+ client.ping();
411
+ } catch {
412
+ try {
413
+ client.terminate();
414
+ } catch {
415
+ /* ignore */
416
+ }
417
+ }
418
+ });
419
+ }, 30_000);
420
+ if (typeof heartbeatInterval.unref === 'function') heartbeatInterval.unref();
421
+
422
+ // ── Static frontend (React SPA in dist/) ─────────────────────────
423
+ const distBuilt =
424
+ existsSync(DIST_DIR) && existsSync(join(DIST_DIR, 'index.html'));
425
+
426
+ if (distBuilt) {
427
+ const assetsDir = join(DIST_DIR, 'assets');
428
+ if (existsSync(assetsDir)) {
429
+ app.use(
430
+ '/assets',
431
+ express.static(assetsDir, { maxAge: '1y', immutable: true, index: false }),
432
+ );
433
+ // v3.23.0 — Serve the mobile CSS at a stable /mobile.css URL so the
434
+ // desktop index.html can link to it without needing to know the hash.
435
+ // MobileApp renders the mobile shell; without its CSS the user sees
436
+ // unformatted HTML. The actual file is named mobile-<hash>.css; we
437
+ // resolve it at startup so the URL is stable.
438
+ try {
439
+ const mobileCss = readdirSync(assetsDir).find((f) => /^mobile-.*\.css$/.test(f));
440
+ if (mobileCss) {
441
+ app.get('/mobile.css', (_req, res) => {
442
+ res.setHeader('Cache-Control', 'no-cache');
443
+ res.sendFile(join(assetsDir, mobileCss));
444
+ });
445
+ }
446
+ } catch { /* ignore — mobile.css won't be served, fine */ }
447
+ app.get('/', (req, res, next) => {
448
+ if (!shouldRedirectToMobile(req)) {
449
+ next();
450
+ return;
451
+ }
452
+ res.redirect(302, mobileRedirectTarget(req));
453
+ });
454
+ // v3.5.0 — Mobile dashboard at /m
455
+ app.get('/m', (_req, res) => {
456
+ res.sendFile(join(DIST_DIR, 'mobile.html'));
457
+ });
458
+ // SPA fallback for /m/* — but ONLY for HTML navigation requests
459
+ // The negative lookahead (?!assets/) excludes asset paths so they fall
460
+ // through to the static /assets/* handler above
461
+ app.get(/^\/m\/(?!assets\/)/, (_req, res) => {
462
+ res.sendFile(join(DIST_DIR, 'mobile.html'));
463
+ });
464
+ app.use(
465
+ express.static(DIST_DIR, {
466
+ extensions: ['html'],
467
+ setHeaders: (res, filePath) => {
468
+ if (filePath.endsWith('index.html') || filePath.endsWith('mobile.html')) {
469
+ res.setHeader('Cache-Control', 'no-cache');
470
+ }
471
+ },
472
+ }),
473
+ );
474
+ app.get('*', (req, res, next) => {
475
+ if (req.path.startsWith('/api') || req.path === '/ws') return next();
476
+ res.sendFile(join(DIST_DIR, 'index.html'));
477
+ });
478
+ } else {
479
+ const distIndex = join(DIST_DIR, 'index.html');
480
+ if (!existsSync(distIndex)) {
481
+ // eslint-disable-next-line no-console
482
+ console.error(`[bizar-dash] dist/index.html not found at ${DIST_DIR}`);
483
+ // eslint-disable-next-line no-console
484
+ console.error(`[bizar-dash] The published package should include a prebuilt dist/.`);
485
+ // eslint-disable-next-line no-console
486
+ console.error(`[bizar-dash] Try: npm install -g @polderlabs/bizar-dash --force`);
487
+ }
488
+ app.get('/', (_req, res) => res.status(503).type('html').send(renderNotBuiltPage()));
489
+ app.get('*', (_req, res) => {
490
+ if (_req.path.startsWith('/api') || _req.path === '/ws') {
491
+ res.status(404).json({ error: 'not_found', message: `no route for ${_req.method} ${_req.originalUrl}` });
492
+ return;
493
+ }
494
+ res.status(503).type('html').send(renderNotBuiltPage());
495
+ });
496
+ }
497
+
498
+ wss.on('connection', (ws, req) => {
499
+ ws.isAlive = true;
500
+ ws.on('pong', () => {
501
+ ws.isAlive = true;
502
+ });
503
+ const path = req.url
504
+ ? (() => {
505
+ try {
506
+ return new URL(req.url, 'http://localhost').pathname;
507
+ } catch {
508
+ return req.url || '';
509
+ }
510
+ })()
511
+ : '';
512
+
513
+ // /ws/logs — stream log file changes
514
+ if (path === '/ws/logs') {
515
+ const HOME = homedir();
516
+ const serviceLog = join(HOME, '.config', 'bizar', 'service.log');
517
+ const dashboardLog = join(HOME, '.config', 'bizar', 'dashboard.log');
518
+ const logFile = existsSync(serviceLog) ? serviceLog : existsSync(dashboardLog) ? dashboardLog : null;
519
+
520
+ let fileSize = logFile && existsSync(logFile) ? statSync(logFile).size : 0;
521
+
522
+ // Send initial tail
523
+ if (logFile) {
524
+ try {
525
+ const text = readFileSync(logFile, 'utf8');
526
+ const lines = text.split(/\r?\n/).filter(Boolean).slice(-100);
527
+ safeSend(ws, JSON.stringify({ type: 'log init', lines, file: logFile }));
528
+ } catch {
529
+ safeSend(ws, JSON.stringify({ type: 'log init', lines: [], file: logFile }));
530
+ }
531
+ } else {
532
+ safeSend(ws, JSON.stringify({ type: 'log init', lines: [], file: null }));
533
+ }
534
+
535
+ let destroying = false;
536
+ function sendLogChunk() {
537
+ if (destroying || ws.readyState !== 1) return;
538
+ try {
539
+ const f = logFile;
540
+ if (!f || !existsSync(f)) return;
541
+ const newSize = statSync(f).size;
542
+ // Reset if the file was rotated/truncated (size shrank).
543
+ if (newSize < fileSize) {
544
+ fileSize = 0;
545
+ }
546
+ if (newSize > fileSize) {
547
+ // Read only the new bytes
548
+ const fd = openSync(f, 'r');
549
+ const buf = Buffer.alloc(newSize - fileSize);
550
+ readSync(fd, buf, 0, buf.length, fileSize);
551
+ closeSync(fd);
552
+ const newText = buf.toString('utf8');
553
+ const newLines = newText.split(/\r?\n/).filter(Boolean);
554
+ for (const line of newLines) {
555
+ safeSend(ws, JSON.stringify({ type: 'log line', line, ts: Date.now() }));
556
+ }
557
+ fileSize = newSize;
558
+ }
559
+ } catch {
560
+ /* ignore */
561
+ }
562
+ }
563
+
564
+ const interval = setInterval(sendLogChunk, 1000);
565
+
566
+ ws.on('message', (raw) => {
567
+ try {
568
+ const msg = JSON.parse(raw.toString());
569
+ if (msg?.type === 'ping') {
570
+ safeSend(ws, JSON.stringify({ type: 'pong', ts: Date.now() }));
571
+ }
572
+ } catch {
573
+ /* ignore */
574
+ }
575
+ });
576
+
577
+ ws.on('close', () => {
578
+ destroying = true;
579
+ clearInterval(interval);
580
+ });
581
+
582
+ ws.on('error', () => {
583
+ destroying = true;
584
+ clearInterval(interval);
585
+ });
586
+ return;
587
+ }
588
+
589
+ // Default /ws — snapshot + ping/pong
590
+ try {
591
+ safeSend(
592
+ ws,
593
+ JSON.stringify({
594
+ type: 'snapshot',
595
+ ts: Date.now(),
596
+ data: buildSnapshotSafe(state, opencodeConfigDir),
597
+ }),
598
+ );
599
+ } catch {
600
+ /* ignore */
601
+ }
602
+
603
+ ws.on('message', (raw) => {
604
+ let msg;
605
+ try {
606
+ msg = JSON.parse(raw.toString());
607
+ } catch {
608
+ return;
609
+ }
610
+ if (msg?.type === 'ping') {
611
+ try {
612
+ safeSend(ws, JSON.stringify({ type: 'pong', ts: Date.now() }));
613
+ } catch {
614
+ /* ignore */
615
+ }
616
+ }
617
+ });
618
+ });
619
+
620
+ watcher.start();
621
+
622
+ // v3.5.5 — Bridge bg state file changes into task status updates +
623
+ // artifact auto-detection. Started after the watcher so the WS
624
+ // broadcast channel is fully wired by the time the first tick
625
+ // runs. Idempotent — calling startBgPoller twice is a no-op.
626
+ try {
627
+ startBgPoller();
628
+ } catch (err) {
629
+ // eslint-disable-next-line no-console
630
+ console.error('[bizar-dash] failed to start bg-poller:', err.message);
631
+ }
632
+
633
+ // v3.11.0 — Periodic recovery for bg instances stuck in
634
+ // `dispatchPending: true` with `toolCallCount === 0`. Before this
635
+ // existed, the only unstick path was the user manually hitting
636
+ // `POST /api/tasks/:id/start`. The retry loop walks every bg
637
+ // state file every 30s, attempts to re-dispatch via the opencode
638
+ // serve child, and caps each instance at MAX_DISPATCH_RETRIES
639
+ // (10) before marking it `failed`.
640
+ try {
641
+ startBgRetryLoop();
642
+ } catch (err) {
643
+ // eslint-disable-next-line no-console
644
+ console.error('[bizar-dash] failed to start bg-retry loop:', err.message);
645
+ }
646
+
647
+ // v0.5.1 — Poll for dialog descriptors written by the plugin and
648
+ // broadcast them to connected dashboard clients via WS.
649
+ // Idempotent — calling startDialogPoller twice is a no-op.
650
+ try {
651
+ startDialogPoller();
652
+ } catch (err) {
653
+ // eslint-disable-next-line no-console
654
+ console.error('[bizar-dash] failed to start dialog-poller:', err.message);
655
+ }
656
+
657
+ function close() {
658
+ try {
659
+ stopBgPoller();
660
+ } catch {
661
+ /* ignore */
662
+ }
663
+ try {
664
+ stopBgRetryLoop();
665
+ } catch {
666
+ /* ignore */
667
+ }
668
+ try {
669
+ watcher.stop();
670
+ } catch {
671
+ /* ignore */
672
+ }
673
+ try {
674
+ wss.clients.forEach((c) => c.terminate());
675
+ wss.close();
676
+ } catch {
677
+ /* ignore */
678
+ }
679
+ try {
680
+ clearInterval(heartbeatInterval);
681
+ } catch {
682
+ /* ignore */
683
+ }
684
+ try {
685
+ server.close();
686
+ } catch {
687
+ /* ignore */
688
+ }
689
+ currentBroadcast = () => {};
690
+ }
691
+
692
+ return { app, server, wss, state, watcher, port, close };
693
+ }
694
+
695
+ function buildSnapshotSafe(state, opencodeConfigDir) {
696
+ try {
697
+ return buildSnapshot(state, opencodeConfigDir);
698
+ } catch (err) {
699
+ return { error: 'snapshot_failed', message: err.message };
700
+ }
701
+ }
702
+
703
+ function buildSnapshot(state, opencodeConfigDir) {
704
+ const cfgFile = join(opencodeConfigDir, 'opencode.json');
705
+ let cfg = null;
706
+ if (existsSync(cfgFile)) {
707
+ try {
708
+ cfg = JSON.parse(readFileSync(cfgFile, 'utf8'));
709
+ } catch {
710
+ cfg = null;
711
+ }
712
+ }
713
+ const activeProject = projectsStore.active();
714
+ return {
715
+ overview: state.getOverview(),
716
+ agents: agentsStore.list(),
717
+ artifacts: state.getArtifacts(),
718
+ projects: projectsStore.list().projects,
719
+ activeProject,
720
+ config: {
721
+ path: cfgFile,
722
+ data: cfg,
723
+ raw: cfg ? JSON.stringify(cfg, null, 2) : '',
724
+ exists: existsSync(cfgFile),
725
+ },
726
+ settings: readSettings(),
727
+ tasks: activeProject ? tasksStore.loadTasks(activeProject.id) : [],
728
+ mods: modsLoader.list(),
729
+ schedules: activeProject ? schedulesStore.list(activeProject.id) : [],
730
+ providers: providersStore.list(),
731
+ mcps: mcpsStore.list(),
732
+ };
733
+ }
734
+
735
+ function existsSafe(p) {
736
+ try {
737
+ return existsSync(p) || existsSync(dirnameSafe(p));
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+
743
+ function dirnameSafe(p) {
744
+ const idx = p.lastIndexOf('/');
745
+ return idx === -1 ? '.' : p.slice(0, idx);
746
+ }
747
+
748
+ function renderNotBuiltPage() {
749
+ return `<!DOCTYPE html>
750
+ <html lang="en">
751
+ <head>
752
+ <meta charset="UTF-8" />
753
+ <title>Dashboard assets not found</title>
754
+ <style>
755
+ :root { color-scheme: dark; }
756
+ body {
757
+ margin: 0; min-height: 100vh; display: flex; align-items: center;
758
+ justify-content: center; background: #0b0e14; color: #c9d1d9;
759
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
760
+ padding: 24px;
761
+ }
762
+ .card {
763
+ max-width: 560px; background: #12161f; border: 1px solid #f87171;
764
+ border-radius: 12px; padding: 32px;
765
+ }
766
+ h1 { margin: 0 0 12px; color: #f87171; font-size: 22px; }
767
+ p { margin: 0 0 12px; line-height: 1.6; }
768
+ code {
769
+ font-family: 'JetBrains Mono', monospace; background: #1a1f2b;
770
+ border: 1px solid #232a39; padding: 2px 6px; border-radius: 4px;
771
+ font-size: 13px;
772
+ }
773
+ pre {
774
+ background: #1a1f2b; border: 1px solid #232a39; padding: 12px 16px;
775
+ border-radius: 8px; font-family: 'JetBrains Mono', monospace;
776
+ font-size: 13px; overflow-x: auto;
777
+ }
778
+ </style>
779
+ </head>
780
+ <body>
781
+ <div class="card">
782
+ <h1>Dashboard assets not found</h1>
783
+ <p>The <code>@polderlabs/bizar-dash</code> package ships prebuilt assets
784
+ in <code>dist/</code>. If you are seeing this, your install is broken.</p>
785
+ <p>Try:</p>
786
+ <pre><code>npm install -g @polderlabs/bizar-dash --force</code></pre>
787
+ <p>If the problem persists, file an issue at<br/>
788
+ <code>github.com/DrB0rk/BizarHarness</code></p>
789
+ <p>The REST API and WebSocket are still live at <code>/api/*</code>
790
+ and <code>/ws</code>.</p>
791
+ </div>
792
+ </body>
793
+ </html>`;
794
+ }
795
+
796
+ export { DIST_DIR, renderNotBuiltPage };