@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,522 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * src/cli.mjs
4
+ *
5
+ * v3.0.0 — `bizar-dash` command.
6
+ *
7
+ * v3.20.7 — Added `cleanup` subcommand and duplicate-dashboard detection.
8
+ * Running `bizar dash start` while another dashboard (on any port in
9
+ * the default range) is already serving will refuse and point at
10
+ * `cleanup` unless `--force` is given. Solves the "dashboard on
11
+ * port 4321 won't go away" zombie problem.
12
+ *
13
+ * Subcommands:
14
+ * start — launch dashboard (default)
15
+ * stop — kill the running dashboard
16
+ * status — print port + URL of any running dashboard
17
+ * tui — run the TUI dashboard
18
+ * cleanup — find and kill zombie / orphan dashboards (keeps canonical)
19
+ * --no-web — TUI only (no browser)
20
+ * --web-only — web only
21
+ * --bg, --detach — start in background and return
22
+ */
23
+ import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import { homedir } from 'node:os';
26
+ import { spawn } from 'node:child_process';
27
+ import { fileURLToPath, pathToFileURL } from 'node:url';
28
+ import { dirname } from 'node:path';
29
+ import {
30
+ findAllDashboards,
31
+ killDashboard,
32
+ clearCanonicalFiles,
33
+ waitForExit,
34
+ summarize,
35
+ } from './cli/dashboard-ports.mjs';
36
+
37
+ const __filename = fileURLToPath(import.meta.url);
38
+ const __dirname = dirname(__filename);
39
+ // Use %APPDATA%\bizar on Windows, ~/.config/bizar on Unix.
40
+ const BIZAR_HOME = process.platform === 'win32'
41
+ ? join(process.env.APPDATA || homedir(), 'bizar')
42
+ : join(homedir(), '.config', 'bizar');
43
+ const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
44
+ const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
45
+ const DEFAULT_PORT = 4321;
46
+
47
+ function readVersion() {
48
+ try {
49
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
50
+ return pkg?.version || '0.0.0';
51
+ } catch {
52
+ return '0.0.0';
53
+ }
54
+ }
55
+
56
+ function showHelp() {
57
+ console.log(`
58
+ bizar — Web + TUI dashboard for the Bizar agent platform
59
+
60
+ Usage:
61
+ bizar Launch the dashboard (default = web)
62
+ bizar start Start the dashboard in this process
63
+ bizar stop Kill the running dashboard
64
+ bizar status Show port + URL of any running dashboard
65
+ bizar cleanup Find and kill zombie / orphan dashboards
66
+ bizar tui [--no-web] Run the TUI dashboard
67
+ bizar --bg, --detach Start in background, return to shell
68
+ bizar --web-only Web dashboard only (no TUI)
69
+ bizar --no-web TUI only (no browser)
70
+ bizar --help Show this help
71
+
72
+ Notes:
73
+ The dashboard reads your opencode config at
74
+ ~/.config/opencode/. The per-project state lives in
75
+ ~/.config/opencode/projects/<id>/. Mods are installed to
76
+ ~/.config/bizar/mods/.
77
+
78
+ Install:
79
+ npm install -g @polderlabs/bizar-dash
80
+ npm install -g @polderlabs/bizar # required peer
81
+ `);
82
+ }
83
+
84
+ async function findFreePort(preferred) {
85
+ const net = await import('node:net');
86
+ for (let p = preferred; p < preferred + 100; p++) {
87
+ if (await isPortFree(net, p)) return p;
88
+ }
89
+ throw new Error(`no free port in range ${preferred}..${preferred + 99}`);
90
+ }
91
+
92
+ function isPortFree(net, port) {
93
+ return new Promise((resolve) => {
94
+ const server = net.createServer();
95
+ let settled = false;
96
+ const finish = (ok) => {
97
+ if (settled) return;
98
+ settled = true;
99
+ resolve(ok);
100
+ };
101
+ server.once('error', () => finish(false));
102
+ server.once('listening', () => server.close(() => finish(true)));
103
+ const t = setTimeout(() => finish(false), 1000);
104
+ server.listen(port, '127.0.0.1', () => clearTimeout(t));
105
+ });
106
+ }
107
+
108
+ async function startDashboard({ port, projectRoot, opencodeConfigDir, bizarRoot, bg = false } = {}) {
109
+ const { createServer } = await import('./server/server.mjs');
110
+ const { launchBrowser } = await import('./server/browser.mjs');
111
+
112
+ // v3.20.7 — Detect duplicate dashboards BEFORE binding the port.
113
+ // If another dashboard is already serving (on any port in the
114
+ // default range), refuse to start unless --force. This prevents
115
+ // the "two dashboards, one is a zombie" deadlock.
116
+ const force = process.argv.includes('--force') || process.argv.includes('-f');
117
+ if (!force) {
118
+ const existing = await findAllDashboards();
119
+ const others = existing.filter((i) => i.state === 'healthy' && i.pid !== process.pid);
120
+ if (others.length > 0) {
121
+ console.error(' ✗ Another Bizar dashboard is already running.');
122
+ console.error('');
123
+ for (const inst of others) {
124
+ const port = inst.port ? `:${inst.port}` : '';
125
+ console.error(` pid ${inst.pid}${port} [${inst.state}] ${inst.cmdline.slice(0, 80)}`);
126
+ }
127
+ console.error('');
128
+ console.error(' To replace it, run `bizar dash cleanup` first, or pass');
129
+ console.error(' --force to skip this check.');
130
+ console.error(' To stop the canonical one, use `bizar dash stop`.');
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ const usePort = port || (await findFreePort(DEFAULT_PORT));
136
+ const { server, close } = await createServer({
137
+ port: usePort,
138
+ projectRoot: projectRoot || process.cwd(),
139
+ opencodeConfigDir: opencodeConfigDir || (
140
+ process.platform === 'win32'
141
+ ? join(process.env.APPDATA || homedir(), 'opencode')
142
+ : join(homedir(), '.config', 'opencode')
143
+ ),
144
+ bizarRoot: bizarRoot || join(__dirname, '..', '..'),
145
+ });
146
+
147
+ // v3.6.0 — Default bind is localhost (more secure). Operators
148
+ // exposing the dashboard over Tailscale or LAN can override with
149
+ // BIZAR_DASHBOARD_BIND=0.0.0.0 (the auth.mjs token will then gate
150
+ // the surface).
151
+ const bindHost = process.env.BIZAR_DASHBOARD_BIND || '127.0.0.1';
152
+ await new Promise((resolve, reject) => {
153
+ server.once('error', reject);
154
+ server.listen(usePort, bindHost, () => {
155
+ server.off('error', reject);
156
+ resolve();
157
+ });
158
+ });
159
+
160
+ mkdirSync(BIZAR_HOME, { recursive: true });
161
+ writeFileSync(PORT_FILE, String(usePort), 'utf8');
162
+ writeFileSync(PID_FILE, String(process.pid), 'utf8');
163
+
164
+ // v3.6.0 — Surface the bind in the URL hint so the operator can see
165
+ // whether they're bound to localhost only or to all interfaces.
166
+ const url = bindHost === '127.0.0.1' || bindHost === 'localhost'
167
+ ? `http://localhost:${usePort}/`
168
+ : `http://${bindHost}:${usePort}/`;
169
+ console.log(`Bizar dashboard: ${url} (bind: ${bindHost})`);
170
+ if (!bg) {
171
+ // Foreground mode: try to launch the browser; then block on a
172
+ // signal so the process keeps running until the operator hits
173
+ // Ctrl-C (or `bizar stop`).
174
+ launchBrowser(url).catch(() => {});
175
+ console.log('Press Ctrl-C to stop the dashboard.');
176
+ let shuttingDown = false;
177
+ const cleanup = () => {
178
+ if (shuttingDown) return;
179
+ shuttingDown = true;
180
+ try { close(); } catch { /* ignore */ }
181
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
182
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
183
+ };
184
+ const handleSignal = () => {
185
+ cleanup();
186
+ process.exit(0);
187
+ };
188
+ process.once('SIGINT', handleSignal);
189
+ process.once('SIGTERM', handleSignal);
190
+ await new Promise(() => {});
191
+ return { url, port: usePort, close };
192
+ }
193
+ // Background mode: do NOT launch a browser (the caller already detached),
194
+ // do NOT block on a signal. The caller (detachAfterBoot) decides how to
195
+ // keep this process alive (or not) once we return.
196
+ return { url, port: usePort, close };
197
+ }
198
+
199
+ /**
200
+ * Called after startDashboard returns in background mode. The server is
201
+ * listening and the PID/PORT files are written. We intentionally keep
202
+ * the process alive (a Node process with nothing keeping it busy exits
203
+ * once the event loop is empty), but detach stdio and ignore signals so
204
+ * the launching shell can exit cleanly while the dashboard keeps
205
+ * serving.
206
+ */
207
+ function detachAfterBoot() {
208
+ console.log(`Detached. Use 'bizar dash stop' to terminate, 'bizar dash status' to check.`);
209
+ // Don't exit — the server keeps the event loop busy. But stop
210
+ // responding to stdin so the parent shell can close.
211
+ if (process.stdin && process.stdin.pause) process.stdin.pause();
212
+ }
213
+
214
+ async function startInBackground(args) {
215
+ const binPath = join(__dirname, 'cli.mjs');
216
+ const child = spawn(process.execPath, [binPath, ...args], {
217
+ detached: true,
218
+ stdio: 'ignore',
219
+ cwd: process.cwd(),
220
+ env: process.env,
221
+ });
222
+ child.on('error', (err) => {
223
+ console.error(`Failed to start background dashboard: ${err.message}`);
224
+ });
225
+ child.unref();
226
+ await new Promise((resolve) => setTimeout(resolve, 1500));
227
+ if (existsSync(PORT_FILE)) {
228
+ const port = readFileSync(PORT_FILE, 'utf8').trim();
229
+ console.log(`Bizar dashboard started in background on http://localhost:${port}/`);
230
+ console.log(`Use 'bizar status' to check, 'bizar stop' to stop.`);
231
+ } else {
232
+ console.log('Bizar dashboard starting in background (port file not yet written)...');
233
+ }
234
+ }
235
+
236
+ /**
237
+ * v3.20.7 — Find and kill every non-canonical dashboard. Keeps the
238
+ * canonical one (PID file) intact. Useful when a previous start left
239
+ * a zombie behind, or when an old dashboard on a different port is
240
+ * still serving.
241
+ */
242
+ async function cleanupDashboards({ force = false, signal = 'SIGTERM' } = {}) {
243
+ const instances = await findAllDashboards();
244
+ if (instances.length === 0) {
245
+ console.log('No dashboards detected.');
246
+ return 0;
247
+ }
248
+ console.log(summarize(instances));
249
+ console.log('');
250
+
251
+ // Strategy (v3.20.14):
252
+ // - Always kill zombies (PID alive + cmdline looks like dashboard,
253
+ // but port doesn't respond to /api/health).
254
+ // - Always kill orphans (port responds but no PID claimed it).
255
+ // - Kill healthy non-canonical dashboards UNLESS the canonical PID
256
+ // is alive and matches the canonical PID file. In that case the
257
+ // user wants the canonical one running and the others are duplicates.
258
+ // - Never kill the canonical PID.
259
+ //
260
+ // Previous bug: cleanup refused to kill `healthy` non-canonical
261
+ // dashboards even when the canonical PID file was dead or recycled.
262
+ // This left orphaned dashboards running that `bizar dash stop`
263
+ // couldn't reach (it only knows the canonical PID file).
264
+ const canonical = instances.find((i) => i.isCanonical && i.pid);
265
+ const canonicalIsAlive = canonical && existsSync(`/proc/${canonical.pid}`);
266
+ const targets = instances.filter((i) => {
267
+ if (i.pid && i.pid === canonical?.pid) return false; // never kill canonical
268
+ if (i.state === 'zombie' || i.state === 'orphan') return true;
269
+ if (i.state === 'dead') return true; // dead PIDs just need their canonical files cleared
270
+ if (i.state === 'healthy' && !i.isCanonical) {
271
+ // If the canonical PID is alive, this is a duplicate — only kill
272
+ // with --force (user confirmed they want to replace it).
273
+ // If the canonical PID is dead/recycled, the PID file is stale;
274
+ // there's no real "canonical" dashboard to preserve, so kill
275
+ // this one to unblock the user.
276
+ return force || !canonicalIsAlive;
277
+ }
278
+ return false;
279
+ });
280
+
281
+ if (targets.length === 0) {
282
+ if (canonical && !canonicalIsAlive) {
283
+ // The canonical PID is gone — clear the stale files even though
284
+ // there's nothing else to clean up.
285
+ console.log(`Canonical PID ${canonical.pid} is dead or recycled — clearing stale PID/port files.`);
286
+ clearCanonicalFiles();
287
+ } else {
288
+ console.log('Nothing to clean up. Use --force to also kill healthy non-canonical duplicates.');
289
+ }
290
+ return 0;
291
+ }
292
+
293
+ let killed = 0;
294
+ for (const inst of targets) {
295
+ if (inst.pid) {
296
+ console.log(` Killing pid ${inst.pid} (${inst.state}, port ${inst.port || '?'}) — ${signal}`);
297
+ const ok = killDashboard(inst.pid, signal);
298
+ if (ok) killed += 1;
299
+ } else if (inst.state === 'orphan') {
300
+ // No PID — port is held by a process we don't know. Tell the user.
301
+ console.log(` Orphan on port ${inst.port}: no PID file claims it. Use 'lsof -i :${inst.port}' or 'fuser ${inst.port}/tcp' to find the owner.`);
302
+ } else if (inst.state === 'dead') {
303
+ console.log(` Dead PID ${inst.pid} — clearing stale port/pid files.`);
304
+ clearCanonicalFiles(inst.pid);
305
+ }
306
+ }
307
+
308
+ // If we sent SIGTERM, wait briefly for graceful exit then SIGKILL stragglers.
309
+ if (signal === 'SIGTERM' && killed > 0) {
310
+ const pids = targets.filter((t) => t.pid).map((t) => t.pid);
311
+ await new Promise((resolve) => setTimeout(resolve, 1500));
312
+ for (const pid of pids) {
313
+ try {
314
+ process.kill(pid, 0); // still alive?
315
+ // Yes — escalate to SIGKILL
316
+ console.log(` Escalating to SIGKILL for pid ${pid}`);
317
+ killDashboard(pid, 'SIGKILL');
318
+ } catch {
319
+ // already exited — good
320
+ }
321
+ }
322
+ }
323
+
324
+ // Clear canonical files if the canonical PID is dead.
325
+ if (canonical && !existsSync(`/proc/${canonical.pid}`)) {
326
+ console.log(`Canonical PID ${canonical.pid} is dead — clearing stale port file.`);
327
+ clearCanonicalFiles();
328
+ }
329
+
330
+ console.log('');
331
+ console.log(`Cleaned up ${killed} dashboard${killed === 1 ? '' : 's'}.`);
332
+ return killed;
333
+ }
334
+
335
+ async function stopDashboard() {
336
+ // v3.20.14: scan for any running dashboard, not just the one in
337
+ // the PID file. If the PID file points to a dead/recycled PID but
338
+ // ps-scan finds a real dashboard (started by a different user or
339
+ // after the PID file was deleted), kill it. Otherwise the user is
340
+ // stuck — `start` refuses because "another dashboard is running"
341
+ // but `stop` reports "no dashboard is running" because it only
342
+ // knows about the PID file.
343
+ const instances = await findAllDashboards();
344
+ const alive = instances.filter((i) => (i.state === 'healthy' || i.state === 'zombie') && i.pid);
345
+ if (alive.length === 0) {
346
+ // No live dashboard. Clear stale PID/port files if they exist.
347
+ if (existsSync(PID_FILE)) {
348
+ console.log(`Stale PID file at ${PID_FILE} — clearing.`);
349
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
350
+ }
351
+ if (existsSync(PORT_FILE)) {
352
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
353
+ }
354
+ console.log('No Bizar dashboard is running.');
355
+ return;
356
+ }
357
+ for (const inst of alive) {
358
+ try {
359
+ process.kill(inst.pid, 'SIGTERM');
360
+ console.log(`Stopped Bizar dashboard (pid ${inst.pid}${inst.port ? `, port ${inst.port}` : ''}, ${inst.state}).`);
361
+ } catch (err) {
362
+ console.log(`Could not stop dashboard (pid ${inst.pid}): ${err.message}`);
363
+ }
364
+ }
365
+ // Wait for graceful exit; escalate to SIGKILL for any stragglers.
366
+ for (const inst of alive) {
367
+ const exited = await waitForExit(inst.pid, 3000);
368
+ if (!exited) {
369
+ try {
370
+ process.kill(inst.pid, 'SIGKILL');
371
+ console.log(` pid ${inst.pid} did not exit gracefully; sent SIGKILL.`);
372
+ } catch { /* ignore */ }
373
+ }
374
+ }
375
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
376
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
377
+ }
378
+
379
+ async function showStatus() {
380
+ // v3.20.14: scan ps + canonical PID file rather than only trusting
381
+ // the PID file. A dashboard started by another user/process without
382
+ // writing to our PID file would have been invisible here.
383
+ const instances = await findAllDashboards();
384
+ const alive = instances.filter((i) => i.state === 'healthy' && i.pid);
385
+ if (alive.length === 0) {
386
+ console.log('No Bizar dashboard is running. Use: bizar dash start');
387
+ return;
388
+ }
389
+ for (const inst of alive) {
390
+ const port = inst.port ? `:${inst.port}` : '';
391
+ const canonical = inst.isCanonical ? ' (canonical)' : '';
392
+ console.log(`Bizar dashboard is running at http://localhost:${inst.port}/ — pid ${inst.pid}${port}${canonical}`);
393
+ }
394
+ }
395
+
396
+ async function runTui({ launchWeb } = {}) {
397
+ const { createServer } = await import('./server/server.mjs');
398
+ const { launchBrowser } = await import('./server/browser.mjs');
399
+ const { launchTui } = await import('./server/tui.mjs');
400
+
401
+ const port = await findFreePort(DEFAULT_PORT);
402
+ const { server, close: closeServer } = await createServer({
403
+ port,
404
+ projectRoot: process.cwd(),
405
+ opencodeConfigDir: (
406
+ process.platform === 'win32'
407
+ ? join(process.env.APPDATA || homedir(), 'opencode')
408
+ : join(homedir(), '.config', 'opencode')
409
+ ),
410
+ bizarRoot: join(__dirname, '..', '..'),
411
+ });
412
+ // v3.6.0 — Honor BIZAR_DASHBOARD_BIND same as startDashboard.
413
+ const bindHost = process.env.BIZAR_DASHBOARD_BIND || '127.0.0.1';
414
+ await new Promise((resolve, reject) => {
415
+ server.once('error', reject);
416
+ server.listen(port, bindHost, () => {
417
+ server.off('error', reject);
418
+ resolve();
419
+ });
420
+ });
421
+ mkdirSync(BIZAR_HOME, { recursive: true });
422
+ writeFileSync(PORT_FILE, String(port), 'utf8');
423
+ writeFileSync(PID_FILE, String(process.pid), 'utf8');
424
+
425
+ if (launchWeb) {
426
+ const url = `http://localhost:${port}/`;
427
+ launchBrowser(url).catch(() => {});
428
+ console.log(`Web dashboard: ${url}`);
429
+ }
430
+
431
+ try {
432
+ await launchTui({ port });
433
+ } finally {
434
+ try { closeServer(); } catch { /* ignore */ }
435
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
436
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
437
+ }
438
+ }
439
+
440
+ // ── Exports (in-process use by `bizar` via @polderlabs/bizar-dash/dash-cli) ──
441
+ export { startDashboard as start, stopDashboard as stop, showStatus as status, runTui as tui, cleanupDashboards as cleanup };
442
+ export { startDashboard, stopDashboard, showStatus, runTui, startInBackground, cleanupDashboards };
443
+
444
+ function isMainEntry() {
445
+ try {
446
+ const entry = process.argv[1];
447
+ if (!entry) return false;
448
+ return import.meta.url === pathToFileURL(entry).href;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+
454
+ // ── CLI dispatch (only when this file is the entry point) ────────────────────
455
+ // When `bizar` imports this file in-process, import.meta.url does not match
456
+ // process.argv[1], so none of the code below runs.
457
+ async function main() {
458
+ const args = process.argv.slice(2);
459
+
460
+ // Install CLI-level signal handlers ONLY when running as a CLI entry point.
461
+ // When imported in-process, the parent process owns its signals.
462
+ process.on('SIGINT', () => {
463
+ stopDashboard().finally(() => process.exit(0));
464
+ });
465
+ process.on('SIGTERM', () => {
466
+ stopDashboard().finally(() => process.exit(0));
467
+ });
468
+
469
+ // Parse --port and --bind from CLI args (v3.11.1 — was missing)
470
+ const portArgIdx = args.indexOf('--port');
471
+ const portArg = portArgIdx >= 0 ? parseInt(args[portArgIdx + 1], 10) : undefined;
472
+ const bindArgIdx = args.indexOf('--bind');
473
+ const bindArg = bindArgIdx >= 0 ? args[bindArgIdx + 1] : undefined;
474
+ const portOpts = {};
475
+ if (Number.isFinite(portArg)) portOpts.port = portArg;
476
+ if (bindArg) process.env.BIZAR_DASHBOARD_BIND = bindArg;
477
+
478
+ if (args.includes('--help') || args.includes('-h')) {
479
+ showHelp();
480
+ } else if (args.includes('--version') || args.includes('-v')) {
481
+ console.log(readVersion());
482
+ } else if (args[0] === 'stop') {
483
+ await stopDashboard();
484
+ } else if (args[0] === 'status') {
485
+ showStatus();
486
+ } else if (args[0] === 'cleanup') {
487
+ // v3.20.7 — `bizar dash cleanup` finds and kills zombie/orphan
488
+ // dashboards. --force also kills healthy non-canonical duplicates.
489
+ // --kill escalates to SIGKILL.
490
+ const force = args.includes('--force') || args.includes('-f');
491
+ const kill = args.includes('--kill');
492
+ await cleanupDashboards({ force, signal: kill ? 'SIGKILL' : 'SIGTERM' });
493
+ } else if (args[0] === 'tui') {
494
+ const rest = args.slice(1);
495
+ const skipWeb = rest.includes('--no-web');
496
+ await runTui({ launchWeb: !skipWeb });
497
+ } else if (args[0] === 'start' && (args.includes('--bg') || args.includes('--detach'))) {
498
+ // `bizar start --bg` (or --detach) — start in background and return.
499
+ // MUST be matched before the plain `start` branch or the --bg flag
500
+ // is ignored and we run the dashboard in the foreground.
501
+ const portIdx = args.indexOf('--port');
502
+ const bgPort = portIdx >= 0 ? parseInt(args[portIdx + 1], 10) : undefined;
503
+ const bgOpts = { bg: true, ...(Number.isFinite(bgPort) ? { port: bgPort } : {}) };
504
+ await startDashboard(bgOpts).then(() => detachAfterBoot());
505
+ } else if (args.includes('--bg') || args.includes('--detach')) {
506
+ // Bare `bizar --bg` / `bizar --detach` — same intent.
507
+ await startDashboard({ bg: true }).then(() => detachAfterBoot());
508
+ } else if (args.includes('--web-only')) {
509
+ await startDashboard(portOpts);
510
+ } else if (args[0] === 'start' || args.length === 0) {
511
+ await startDashboard(portOpts);
512
+ } else {
513
+ showHelp();
514
+ }
515
+ }
516
+
517
+ if (isMainEntry()) {
518
+ main().catch((err) => {
519
+ console.error(err && err.message ? err.message : err);
520
+ process.exit(1);
521
+ });
522
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * src/server/activity-log.mjs
3
+ *
4
+ * v3.2.0 — Append-only activity log for the dashboard's Activity tab
5
+ * and canvas-node detail panels.
6
+ *
7
+ * Storage:
8
+ * ~/.config/opencode/activity.jsonl (one JSON object per line)
9
+ *
10
+ * Why JSONL:
11
+ * - Cheap append (write one line, no full-file rewrite).
12
+ * - Easy to tail / filter / reverse-scan without parsing the whole
13
+ * file.
14
+ * - Survives partial writes — corrupt trailing lines are skipped on
15
+ * read.
16
+ *
17
+ * Use cases:
18
+ * - The Activity tab reads the latest N entries for the global
19
+ * stream and filters by `nodeId` for per-node drilldowns.
20
+ * - Comment/task injection on canvas nodes writes here with
21
+ * `kind: 'node.comment'` and a `nodeId` so the detail panel can
22
+ * show a thread.
23
+ * - Task delegation broadcasts append with `kind: 'task.delegated'`
24
+ * and a `nodeId` of `task:<id>`.
25
+ */
26
+
27
+ import {
28
+ existsSync,
29
+ readFileSync,
30
+ writeFileSync,
31
+ renameSync,
32
+ appendFileSync,
33
+ mkdirSync,
34
+ statSync,
35
+ } from 'node:fs';
36
+ import { join, dirname } from 'node:path';
37
+ import { homedir } from 'node:os';
38
+
39
+ const HOME = homedir();
40
+ const LOG_FILE = join(HOME, '.config', 'opencode', 'activity.jsonl');
41
+
42
+ // Hard cap on retained entries (the file is rotated/truncated by the
43
+ // caller; we just stop reading past this many in `recent()`).
44
+ const MAX_RETAINED = 5000;
45
+ const MAX_LINES_PER_READ = 5000;
46
+
47
+ // Atomic write: write to a sibling temp file, then rename into place.
48
+ // `rename` is atomic on POSIX (same filesystem), so a crash between
49
+ // read and write never leaves a partial / empty log file.
50
+ function atomicWriteText(filePath, text) {
51
+ const tmp = `${filePath}.tmp.${process.pid}`;
52
+ writeFileSync(tmp, text, 'utf8');
53
+ renameSync(tmp, filePath);
54
+ }
55
+
56
+ function ensureFile() {
57
+ mkdirSync(dirname(LOG_FILE), { recursive: true });
58
+ if (!existsSync(LOG_FILE)) {
59
+ writeFileSync(LOG_FILE, '', 'utf8');
60
+ }
61
+ }
62
+
63
+ // Rotate the log when it exceeds the size cap. Atomic: write the
64
+ // truncated contents to a temp file, then rename into place so a
65
+ // crash between read and write can't lose the whole log.
66
+ const ROTATE_MAX_BYTES = 5 * 1024 * 1024;
67
+ function rotateIfNeeded(logFile) {
68
+ if (!existsSync(logFile)) return;
69
+ const st = statSync(logFile);
70
+ if (st.size < ROTATE_MAX_BYTES) return;
71
+ const lines = readFileSync(logFile, 'utf8').split(/\r?\n/).filter(Boolean);
72
+ const kept = lines.slice(-MAX_RETAINED);
73
+ atomicWriteText(logFile, kept.join('\n') + '\n');
74
+ }
75
+
76
+ function safeParse(line) {
77
+ try {
78
+ return JSON.parse(line);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ export const activityLog = {
85
+ LOG_FILE,
86
+
87
+ /**
88
+ * Append an event. `event` is a plain object; we add `ts` if absent.
89
+ */
90
+ append(event) {
91
+ try {
92
+ ensureFile();
93
+ const record = { ...(event || {}), ts: new Date().toISOString() };
94
+ appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
95
+ // Best-effort rotate when the file gets large.
96
+ try {
97
+ rotateIfNeeded(LOG_FILE);
98
+ } catch (err) {
99
+ console.error('[activity-log] rotation failed:', err.message);
100
+ }
101
+ return record;
102
+ } catch (err) {
103
+ console.error('[activity-log] append failed:', err.message);
104
+ return null;
105
+ }
106
+ },
107
+
108
+ /**
109
+ * Read up to `limit` most-recent entries, newest-first.
110
+ */
111
+ recent(limit = 100) {
112
+ try {
113
+ ensureFile();
114
+ const text = readFileSync(LOG_FILE, 'utf8');
115
+ if (!text.trim()) return [];
116
+ const lines = text.split(/\r?\n/).filter(Boolean);
117
+ const slice = lines.slice(-Math.min(MAX_LINES_PER_READ, Math.max(1, limit * 4)));
118
+ const out = [];
119
+ for (let i = slice.length - 1; i >= 0 && out.length < limit; i--) {
120
+ const parsed = safeParse(slice[i]);
121
+ if (parsed) out.push(parsed);
122
+ }
123
+ return out;
124
+ } catch {
125
+ return [];
126
+ }
127
+ },
128
+
129
+ /**
130
+ * Filter the recent log by nodeId. Newest-first.
131
+ */
132
+ forNode(nodeId, limit = 50) {
133
+ if (!nodeId) return [];
134
+ const all = this.recent(limit * 4);
135
+ return all.filter((e) => e.nodeId === nodeId).slice(0, limit);
136
+ },
137
+
138
+ /**
139
+ * Filter by kind. Newest-first.
140
+ */
141
+ byKind(kind, limit = 100) {
142
+ const all = this.recent(limit * 4);
143
+ return all.filter((e) => e.kind === kind).slice(0, limit);
144
+ },
145
+
146
+ /**
147
+ * Stats: counts by kind, last ts, file size.
148
+ */
149
+ stats() {
150
+ try {
151
+ ensureFile();
152
+ const st = statSync(LOG_FILE);
153
+ const text = readFileSync(LOG_FILE, 'utf8');
154
+ const lines = text.split(/\r?\n/).filter(Boolean);
155
+ const counts = {};
156
+ let lastTs = null;
157
+ for (const line of lines) {
158
+ const p = safeParse(line);
159
+ if (!p) continue;
160
+ counts[p.kind || 'unknown'] = (counts[p.kind || 'unknown'] || 0) + 1;
161
+ if (!lastTs || p.ts > lastTs) lastTs = p.ts;
162
+ }
163
+ return {
164
+ file: LOG_FILE,
165
+ size: st.size,
166
+ lines: lines.length,
167
+ lastTs,
168
+ counts,
169
+ };
170
+ } catch {
171
+ return { file: LOG_FILE, size: 0, lines: 0, lastTs: null, counts: {} };
172
+ }
173
+ },
174
+ };