@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,488 @@
1
+ /**
2
+ * src/cli/dashboard-ports.mjs
3
+ *
4
+ * v3.20.7 — Discover every running Bizar dashboard on this machine,
5
+ * classify it as healthy / zombie / orphan / dead, and provide cleanup
6
+ * helpers. Solves the long-standing "why is there a dashboard on
7
+ * port 4321 that I can't reach but won't go away?" problem.
8
+ *
9
+ * We can't trust the port file (it tracks only the most-recent start,
10
+ * written to the last successful boot). Use a combination of:
11
+ * 1. Read the canonical PID from ~/.config/bizar/dashboard.pid
12
+ * 2. Scan all node processes via `ps` for `bizar dash start` /
13
+ * `@polderlabs/bizar-dash/src/cli.mjs start`
14
+ * 3. For each candidate PID, walk /proc/<pid>/fd → socket inodes →
15
+ * /proc/net/tcp to find its listening port (Linux only)
16
+ * 4. TCP-probe each candidate port (1.5s timeout, transport-only)
17
+ * 5. HTTP-probe each reachable port's /api/health endpoint
18
+ *
19
+ * Result classification:
20
+ * - 'healthy' PID alive + /api/health returns ok:true
21
+ * - 'zombie' PID alive + port accepts TCP but health probe fails
22
+ * (request handler deadlock, event loop starved, etc.)
23
+ * - 'dead' PID does not exist (orphaned port file)
24
+ * - 'orphan' Port responds to health but no PID file knows about it
25
+ * (started by a different process, port file deleted)
26
+ *
27
+ * Exports:
28
+ * - findAllDashboards() → DashboardInstance[]
29
+ * - killDashboard(pid, signal?) → boolean
30
+ * - summarize(instances) → friendly multi-line report
31
+ *
32
+ * Cross-platform: the /proc-fd walk is Linux-only. On macOS/Windows
33
+ * we fall back to canonical-PID + port range scan.
34
+ */
35
+ import { execSync, spawn } from 'node:child_process';
36
+ import {
37
+ existsSync,
38
+ readFileSync,
39
+ readlinkSync,
40
+ readdirSync,
41
+ writeFileSync,
42
+ unlinkSync,
43
+ } from 'node:fs';
44
+ import { join } from 'node:path';
45
+ import { homedir } from 'node:os';
46
+ import net from 'node:net';
47
+
48
+ export const DEFAULT_PORT_START = 4321;
49
+ export const DEFAULT_PORT_END = 4530;
50
+ export const HEALTH_PROBE_TIMEOUT_MS = 1500;
51
+ export const TCP_PROBE_TIMEOUT_MS = 800;
52
+
53
+ const HOME = homedir();
54
+ const BIZAR_HOME = process.platform === 'win32'
55
+ ? join(process.env.APPDATA || HOME, 'bizar')
56
+ : join(HOME, '.config', 'bizar');
57
+ export const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
58
+ export const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
59
+
60
+ /**
61
+ * @typedef {Object} DashboardInstance
62
+ * @property {number|null} pid PID of the node process (null if unknown)
63
+ * @property {number|null} port TCP port the dashboard listens on
64
+ * @property {'healthy'|'zombie'|'dead'|'orphan'|'unknown'} state
65
+ * @property {boolean} portReachable TCP probe succeeded
66
+ * @property {boolean} healthOk /api/health returned ok:true
67
+ * @property {string} cmdline Full process command line (or note)
68
+ * @property {'pid-file'|'ps-scan'|'port-scan'} source How we found it
69
+ * @property {boolean} isCanonical Matches the PID file
70
+ */
71
+
72
+ export function readCanonical() {
73
+ let pid = null;
74
+ let port = null;
75
+ try {
76
+ const v = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
77
+ if (Number.isFinite(v) && v > 0) pid = v;
78
+ } catch { /* ignore */ }
79
+ try {
80
+ const v = parseInt(readFileSync(PORT_FILE, 'utf8').trim(), 10);
81
+ if (Number.isFinite(v) && v > 0 && v <= 65535) port = v;
82
+ } catch { /* ignore */ }
83
+ return { pid, port };
84
+ }
85
+
86
+ /** True if a PID is alive (signal 0 trick). */
87
+ export function pidAlive(pid) {
88
+ if (!pid || !Number.isFinite(pid)) return false;
89
+ try {
90
+ process.kill(pid, 0);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /** TCP connect probe — auth-free, transport-only. */
98
+ export function tcpProbe(port, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
99
+ return new Promise((resolve) => {
100
+ const sock = net.createConnection({ port, host: '127.0.0.1' });
101
+ let settled = false;
102
+ const finish = (ok) => {
103
+ if (settled) return;
104
+ settled = true;
105
+ try { sock.destroy(); } catch { /* ignore */ }
106
+ resolve(ok);
107
+ };
108
+ sock.setTimeout(timeoutMs);
109
+ sock.once('connect', () => finish(true));
110
+ sock.once('error', () => finish(false));
111
+ sock.once('timeout', () => finish(false));
112
+ });
113
+ }
114
+
115
+ /** HTTP probe — returns true only if /api/health returns ok:true. */
116
+ export async function httpHealth(port, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
117
+ try {
118
+ const ctrl = new AbortController();
119
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
120
+ const res = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: ctrl.signal });
121
+ clearTimeout(timer);
122
+ if (!res.ok) return false;
123
+ let json = null;
124
+ try { json = await res.json(); } catch { return false; }
125
+ return json && json.ok === true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Find the listening TCP port for a PID by walking /proc/<pid>/fd and
133
+ * matching socket inodes against /proc/net/tcp. Linux-only.
134
+ * Returns null on other platforms or if the PID has no listening socket.
135
+ */
136
+ export function pidListeningPort(pid) {
137
+ if (process.platform !== 'linux') return null;
138
+ try {
139
+ const fdDir = `/proc/${pid}/fd`;
140
+ if (!existsSync(fdDir)) return null;
141
+ const inodes = new Set();
142
+ for (const fd of readdirSync(fdDir)) {
143
+ try {
144
+ const target = readlinkSync(join(fdDir, fd));
145
+ const m = target.match(/socket:\[(\d+)\]/);
146
+ if (m) inodes.add(m[1]);
147
+ } catch { /* stale fd — ignore */ }
148
+ }
149
+ if (inodes.size === 0) return null;
150
+ const tcp = readFileSync('/proc/net/tcp', 'utf8');
151
+ // 127.0.0.1 in little-endian hex = 0100007F
152
+ // 0.0.0.0 in little-endian hex = 00000000
153
+ const LOCAL_IPS = new Set(['0100007F', '00000000']);
154
+ for (const line of tcp.split('\n').slice(1)) {
155
+ const cols = line.trim().split(/\s+/);
156
+ if (cols.length < 10) continue;
157
+ const local = cols[1];
158
+ if (!local.includes(':')) continue;
159
+ const [ipHex, portHex] = local.split(':');
160
+ if (!LOCAL_IPS.has(ipHex)) continue;
161
+ const port = parseInt(portHex, 16);
162
+ const inode = cols[9];
163
+ if (inodes.has(inode)) return port;
164
+ }
165
+ } catch { /* ignore */ }
166
+ return null;
167
+ }
168
+
169
+ /**
170
+ * `ps`-based fallback to find every node process whose argv mentions
171
+ * the Bizar dashboard CLI. Returns a list of { pid, cmdline }.
172
+ *
173
+ * v3.20.14 — tightened the pattern + added an explicit cmdline
174
+ * re-verification step that the caller MUST do before trusting the
175
+ * match. The previous version matched anything containing the substring
176
+ * "bizar dash" or "@polderlabs/bizar-dash/src/cli.mjs" — which is fine
177
+ * for finding real dashboards but breaks when:
178
+ * - The PID is recycled to a different process between ps-scan and
179
+ * `pidAlive(pid)` (Linux recycles PIDs in milliseconds).
180
+ * - A previous run wrote a stale `dashboard.pid` file referencing a
181
+ * PID that's now owned by an unrelated process (chrome-headless-shell
182
+ * renderer, ssh session, etc.).
183
+ * - The cmdline contains a path that no longer exists on disk
184
+ * (e.g. /home/drb0rk/.local/npm/bin/bizar after a clean npm install).
185
+ *
186
+ * The fix: only record a ps-scan match if the cmdline actually looks
187
+ * like a node-launched Bizar dashboard CLI. The `cmdlineLooksLikeDashboard`
188
+ * helper does the structural check (node binary + cli.mjs OR a known
189
+ * `bizar` bin path + `dash start`). Then the caller's `pidAlive`
190
+ * step re-reads /proc/<pid>/cmdline and calls the same helper to
191
+ * confirm the PID hasn't been recycled.
192
+ *
193
+ * Skips our own process (the runner) so the CLI doesn't see itself.
194
+ */
195
+ export function psScanDashboards() {
196
+ const out = new Map(); // pid → cmdline
197
+ if (process.platform === 'win32') {
198
+ // tasklist | findstr — too platform-specific for now; users on
199
+ // Windows who hit this can restart their machine. The canonical
200
+ // PID file path still works on Windows.
201
+ return out;
202
+ }
203
+ try {
204
+ const result = execSync(
205
+ // Match `bizar dash` or `@polderlabs/bizar-dash/src/cli.mjs`. The
206
+ // pipe through grep -v grep is still needed to suppress grep's
207
+ // own argv from matching the regex.
208
+ `ps -eo pid=,args= 2>/dev/null | grep -E '(bizar[ ]+dash|@polderlabs/bizar-dash/src/cli\\.mjs)' | grep -v grep || true`,
209
+ { encoding: 'utf8', timeout: 5000 },
210
+ );
211
+ for (const line of result.split('\n')) {
212
+ const trimmed = line.trim();
213
+ if (!trimmed) continue;
214
+ // Format: "<pid> <args...>" — pid is the first whitespace-delimited token.
215
+ const m = trimmed.match(/^(\d+)\s+(.+)$/);
216
+ if (!m) continue;
217
+ const pid = parseInt(m[1], 10);
218
+ if (!Number.isFinite(pid)) continue;
219
+ if (pid === process.pid) continue; // skip our own process
220
+ const cmdline = m[2].trim();
221
+ // Structural check: only keep matches that look like a real
222
+ // dashboard launch. Anything else is a false positive (PID
223
+ // recycled, or a process whose argv happens to contain the
224
+ // words "bizar dash" — e.g. a stale node process or a shell
225
+ // command that includes the words in a comment).
226
+ if (!cmdlineLooksLikeDashboard(cmdline)) continue;
227
+ out.set(pid, cmdline);
228
+ }
229
+ } catch {
230
+ // ps not available — fall back to canonical only
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Structural check: does this cmdline look like a Bizar dashboard
237
+ * launch? We accept:
238
+ * - `node <anything>/@polderlabs/bizar-dash/src/cli.mjs [start|tui|...]`
239
+ * - `<path>/bizar dash <subcommand>` where `<path>` is a real-looking
240
+ * filesystem path (contains `/` or starts with `.`).
241
+ *
242
+ * Reject anything that doesn't match those patterns (e.g. a `bash -c`
243
+ * shell command whose body mentions `bizar dash` in a comment).
244
+ *
245
+ * Implementation note: we anchor the `bizar` bin to a path-like prefix
246
+ * by requiring it to be preceded by either the start of the cmdline,
247
+ * a `/`, or a `\`. This rejects strings like
248
+ * `bash -c "echo I want to run bizar dash start"` where `bizar dash
249
+ * start` is mid-string inside a quoted argument — the `bizar` is
250
+ * preceded by a space, not by a path separator.
251
+ */
252
+ export function cmdlineLooksLikeDashboard(cmdline) {
253
+ if (!cmdline || typeof cmdline !== 'string') return false;
254
+ // Direct CLI invocation: @polderlabs/bizar-dash/src/cli.mjs <verb>
255
+ if (/\/@polderlabs\/bizar-dash\/src\/cli\.mjs\b/.test(cmdline)) return true;
256
+ // Wrapped via the `bizar` bin: <path>/bizar dash <subcommand>.
257
+ // - The `bizar` token must be preceded by a path separator (so it's
258
+ // a binary path, not an arbitrary word in a comment).
259
+ // - The `dash` and subcommand must be whitespace-delimited argv.
260
+ if (/(?:^|[\/\\])\S*bizar\s+dash\s+(?:start|tui|stop|status|cleanup)\b/.test(cmdline)) return true;
261
+ return false;
262
+ }
263
+
264
+ /**
265
+ * Re-read /proc/<pid>/cmdline and verify the cmdline still looks like
266
+ * a dashboard launch. Linux recycles PIDs aggressively — between
267
+ * `psScanDashboards` returning a PID and `pidAlive(pid)` succeeding,
268
+ * the PID may have been reassigned to an unrelated process. We catch
269
+ * that by reading /proc/<pid>/cmdline fresh and re-applying the
270
+ * structural check. Returns the cmdline string on success, null on
271
+ * failure (PID dead, /proc unreadable, or cmdline no longer matches).
272
+ */
273
+ export function verifyCmdlineAtProbeTime(pid) {
274
+ if (!pid) return null;
275
+ try {
276
+ const cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim();
277
+ if (cmdlineLooksLikeDashboard(cmdline)) return cmdline;
278
+ return null;
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Discover every Bizar dashboard process on this machine.
286
+ *
287
+ * @param {Object} opts
288
+ * @param {number} [opts.portStart=4321] Start of port scan range
289
+ * @param {number} [opts.portEnd=4530] End of port scan range
290
+ * @param {boolean} [opts.scanPorts=true] If true, probe ports in [start, end] for orphans
291
+ * @returns {Promise<DashboardInstance[]>}
292
+ */
293
+ export async function findAllDashboards({
294
+ portStart = DEFAULT_PORT_START,
295
+ portEnd = DEFAULT_PORT_END,
296
+ scanPorts = true,
297
+ } = {}) {
298
+ const canonical = readCanonical();
299
+ /** @type {Map<number, DashboardInstance>} */
300
+ const byPid = new Map();
301
+
302
+ // 1. Canonical PID — even if dead, record it so the user can clean up.
303
+ if (canonical.pid) {
304
+ const alive = pidAlive(canonical.pid);
305
+ let cmdline = '';
306
+ try { cmdline = readFileSync(`/proc/${canonical.pid}/cmdline`, 'utf8').replace(/\0/g, ' ').trim(); } catch {}
307
+ if (!cmdline) cmdline = '(no /proc entry — process likely exited)';
308
+ // v3.20.14 — only treat the canonical PID as canonical if the
309
+ // current cmdline still looks like a dashboard launch. A recycled
310
+ // PID (now owned by chrome, ssh, etc.) should NOT count as the
311
+ // canonical dashboard — the PID file is stale.
312
+ const cmdlineMatches = cmdlineLooksLikeDashboard(cmdline);
313
+ byPid.set(canonical.pid, {
314
+ pid: canonical.pid,
315
+ port: canonical.port,
316
+ // If PID is alive but cmdline is no longer a dashboard, the PID
317
+ // has been recycled. Mark as 'dead' so cleanup clears the
318
+ // stale PID file. Same if PID is genuinely dead.
319
+ state: alive && cmdlineMatches ? 'unknown' : 'dead',
320
+ portReachable: false,
321
+ healthOk: false,
322
+ cmdline,
323
+ // Only canonical if PID file path AND cmdline both look real.
324
+ // This prevents the "Found 1 dashboard: [healthy] pid 938619
325
+ // from ps-scan" trap where the PID file's PID has been recycled
326
+ // to an unrelated process.
327
+ isCanonical: cmdlineMatches,
328
+ source: 'pid-file',
329
+ });
330
+ }
331
+
332
+ // 2. ps scan — find every node process running the dashboard CLI.
333
+ const psResults = psScanDashboards();
334
+ for (const [pid, cmdline] of psResults) {
335
+ if (byPid.has(pid)) {
336
+ // Already known — backfill cmdline if it was empty.
337
+ const existing = byPid.get(pid);
338
+ if (!existing.cmdline || existing.cmdline.startsWith('(')) {
339
+ existing.cmdline = cmdline;
340
+ }
341
+ continue;
342
+ }
343
+ byPid.set(pid, {
344
+ pid,
345
+ port: null,
346
+ state: 'unknown',
347
+ portReachable: false,
348
+ healthOk: false,
349
+ cmdline,
350
+ source: 'ps-scan',
351
+ isCanonical: false,
352
+ });
353
+ }
354
+
355
+ // 3. Probe each PID's listening port + health.
356
+ for (const inst of byPid.values()) {
357
+ if (!pidAlive(inst.pid)) {
358
+ inst.state = 'dead';
359
+ continue;
360
+ }
361
+ // v3.20.14 — re-read /proc/<pid>/cmdline fresh and re-verify it
362
+ // still looks like a dashboard launch. Linux recycles PIDs
363
+ // aggressively; a PID that matched ps-scan a moment ago may
364
+ // now belong to an unrelated process (chrome-headless-shell
365
+ // renderer, ssh session, etc.). If the cmdline no longer matches
366
+ // the dashboard pattern, the PID is recycled — mark it dead.
367
+ if (inst.source === 'ps-scan') {
368
+ const fresh = verifyCmdlineAtProbeTime(inst.pid);
369
+ if (fresh === null) {
370
+ inst.state = 'dead';
371
+ continue;
372
+ }
373
+ // Update cmdline in case it changed (the argv we saw at scan time
374
+ // might have been a stale /proc snapshot — extremely rare but
375
+ // possible during a process exec).
376
+ if (fresh !== inst.cmdline) inst.cmdline = fresh;
377
+ }
378
+ // Resolve port: prefer /proc fd walk; fall back to canonical port for PID file match.
379
+ if (!inst.port) {
380
+ const discovered = pidListeningPort(inst.pid);
381
+ if (discovered) inst.port = discovered;
382
+ }
383
+ if (!inst.port) continue; // can't probe without a port
384
+ inst.portReachable = await tcpProbe(inst.port);
385
+ if (!inst.portReachable) continue;
386
+ inst.healthOk = await httpHealth(inst.port);
387
+ inst.state = inst.healthOk ? 'healthy' : 'zombie';
388
+ }
389
+
390
+ // 4. Port-range scan for orphans — ports that respond to health but
391
+ // no PID claimed them.
392
+ if (scanPorts) {
393
+ const claimed = new Set(
394
+ Array.from(byPid.values()).filter(i => i.port).map(i => i.port),
395
+ );
396
+ for (let p = portStart; p <= portEnd; p++) {
397
+ if (claimed.has(p)) continue;
398
+ const reachable = await tcpProbe(p);
399
+ if (!reachable) continue;
400
+ const healthOk = await httpHealth(p);
401
+ if (!healthOk) continue;
402
+ // We found a port responding to /api/health that no PID claimed.
403
+ // This is an orphan — typically started by a different user or
404
+ // after the port file was deleted.
405
+ byPid.set(-p, {
406
+ pid: null,
407
+ port: p,
408
+ state: 'orphan',
409
+ portReachable: true,
410
+ healthOk: true,
411
+ cmdline: '(unknown — no PID claimed this port)',
412
+ source: 'port-scan',
413
+ isCanonical: false,
414
+ });
415
+ }
416
+ }
417
+
418
+ return Array.from(byPid.values()).sort((a, b) => {
419
+ // Canonical first, then by port, then by pid
420
+ if (a.isCanonical !== b.isCanonical) return a.isCanonical ? -1 : 1;
421
+ if (a.port !== b.port) return (a.port || 0) - (b.port || 0);
422
+ return (a.pid || 0) - (b.pid || 0);
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Send a signal to a PID. Returns true if the signal was delivered.
428
+ * `signal` defaults to 'SIGTERM' (graceful). Use 'SIGKILL' for stuck procs.
429
+ */
430
+ export function killDashboard(pid, signal = 'SIGTERM') {
431
+ if (!pid) return false;
432
+ try {
433
+ process.kill(pid, signal);
434
+ return true;
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Clear the canonical PID/port files (only if the PID is dead or matches).
442
+ */
443
+ export function clearCanonicalFiles(onlyIfPid = null) {
444
+ if (onlyIfPid !== null) {
445
+ const canonical = readCanonical();
446
+ if (canonical.pid !== onlyIfPid) return false;
447
+ }
448
+ try { unlinkSync(PID_FILE); } catch { /* ignore */ }
449
+ try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
450
+ return true;
451
+ }
452
+
453
+ /**
454
+ * Wait for a PID to exit (poll). Returns true if it exited within
455
+ * `timeoutMs`, false otherwise.
456
+ */
457
+ export async function waitForExit(pid, timeoutMs = 5000, intervalMs = 200) {
458
+ const start = Date.now();
459
+ while (Date.now() - start < timeoutMs) {
460
+ if (!pidAlive(pid)) return true;
461
+ await new Promise((r) => setTimeout(r, intervalMs));
462
+ }
463
+ return !pidAlive(pid);
464
+ }
465
+
466
+ /**
467
+ * Render a friendly multi-line summary of dashboard instances for the CLI.
468
+ */
469
+ export function summarize(instances) {
470
+ if (!instances.length) {
471
+ return 'No dashboards detected.';
472
+ }
473
+ const lines = [];
474
+ lines.push(`Found ${instances.length} dashboard${instances.length === 1 ? '' : 's'}:`);
475
+ lines.push('');
476
+ for (const inst of instances) {
477
+ const tag = inst.isCanonical ? '★ canonical' : ' ';
478
+ const stateTag = `[${inst.state.padEnd(8)}]`;
479
+ const portStr = inst.port ? `:${inst.port}` : '(no port)';
480
+ const pidStr = inst.pid ? `pid ${inst.pid}` : 'pid ?';
481
+ lines.push(` ${tag} ${stateTag} ${pidStr.padEnd(12)} ${portStr.padEnd(10)} ${inst.source}`);
482
+ if (inst.cmdline) {
483
+ const cl = inst.cmdline.length > 100 ? inst.cmdline.slice(0, 97) + '...' : inst.cmdline;
484
+ lines.push(` ${cl}`);
485
+ }
486
+ }
487
+ return lines.join('\n');
488
+ }