@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,1168 @@
1
+ /**
2
+ * commands.ts
3
+ *
4
+ * v0.5.0 — Pure slash-command parser.
5
+ *
6
+ * The plugin's `chat.message` hook calls `parseSlashCommand(text, ctx)`
7
+ * on every user message. If the message starts with `/`, this function
8
+ * classifies it as a known command or returns `null` (so the message
9
+ * passes through to the LLM normally).
10
+ *
11
+ * Design constraints:
12
+ * - Pure function. No I/O, no environment reads, no Date.now() calls
13
+ * inside the parser itself.
14
+ * - The function is the **only** place slash commands are parsed.
15
+ * The hook gathers context (current settings, available plan slugs)
16
+ * and feeds it to the parser, then dispatches the returned
17
+ * `sideEffect` via `executeSideEffect()` in commands-impl.ts.
18
+ * - Returns `null` for non-slash messages so the LLM still sees them.
19
+ *
20
+ * Output shape (per slash command):
21
+ * `{ handled: true, response, settingsPatch?, sideEffect? }`
22
+ *
23
+ * - `response`: text surfaced to the user / LLM.
24
+ * - `settingsPatch`: optional partial `PlanSettings` to apply.
25
+ * - `sideEffect`: optional descriptor for the hook to execute. The
26
+ * `tool_invocation` kind routes through `executeToolInvocation`,
27
+ * which builds a synthetic `ToolContext` and invokes the
28
+ * registered tool directly. The other kinds (`create_plan`,
29
+ * `list_plans`, `open_plan_url`) are handled by
30
+ * `executeSideEffect` in commands-impl.ts.
31
+ *
32
+ * The parser is unit-tested directly (tests/commands.test.ts).
33
+ * Side-effect execution is tested in tests/commands-impl.test.ts.
34
+ */
35
+
36
+ import type { DefaultTemplate, PlanSettings } from "./settings.js";
37
+ import { KNOWN_TEMPLATES } from "./settings.js";
38
+
39
+ // --- Public types --------------------------------------------------------
40
+
41
+ /**
42
+ * What the parser asks the chat.message hook to do on the host side.
43
+ *
44
+ * v0.5.0 — extended with the `tool_invocation` kind (per C3). Subcommand
45
+ * parsers translate the user's typed subcommand into a `tool_invocation`
46
+ * side-effect that calls a real `bizar_*` tool with a synthetic
47
+ * `ToolContext`. The chat hook does not need to know about the tool
48
+ * layer; it just hands the descriptor to `executeToolInvocation`.
49
+ */
50
+ export type SideEffect =
51
+ | {
52
+ kind: "create_plan";
53
+ slug: string;
54
+ template: DefaultTemplate | null;
55
+ }
56
+ | {
57
+ kind: "open_plan_url";
58
+ slug: string;
59
+ }
60
+ | {
61
+ kind: "list_plans";
62
+ }
63
+ | {
64
+ kind: "launch_dashboard";
65
+ defaultPort: number;
66
+ }
67
+ | {
68
+ kind: "tool_invocation";
69
+ toolName: string;
70
+ args: unknown;
71
+ };
72
+
73
+ /** Dialog component types that can be rendered in the dashboard. */
74
+ export type DialogComponent =
75
+ | "visual-plan"
76
+ | "plan-create"
77
+ | "plan-list"
78
+ | "help"
79
+ | "audit"
80
+ | "generic";
81
+
82
+ export interface DialogDescriptor {
83
+ id: string;
84
+ title: string;
85
+ command: string;
86
+ component: DialogComponent;
87
+ data?: Record<string, unknown>;
88
+ }
89
+
90
+ function generateId(): string {
91
+ return `dlg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
92
+ }
93
+
94
+ export interface SlashCommandResult {
95
+ handled: true;
96
+ /** Text shown to the user / LLM. */
97
+ response: string;
98
+ /** Optional settings mutation. */
99
+ settingsPatch?: Partial<PlanSettings>;
100
+ /** Optional side-effect to perform (file I/O lives in the hook, not here). */
101
+ sideEffect?: SideEffect;
102
+ /** Optional dialog to open in the dashboard instead of showing text. */
103
+ dialog?: DialogDescriptor;
104
+ }
105
+
106
+ /**
107
+ * Context passed to the parser. The parser does NOT call `getCurrentSettings`
108
+ * itself — instead the caller injects the live values. This keeps the parser
109
+ * pure and lets tests stub the context.
110
+ */
111
+ export interface ParseContext {
112
+ /** Current plan settings (already loaded by the caller). */
113
+ currentSettings: PlanSettings;
114
+ /** Slugs of plans that exist in the worktree's plans/ directory. */
115
+ availablePlanSlugs?: readonly string[];
116
+ /**
117
+ * The default port for `/plan open <slug>` URLs. The viewer runs in-process
118
+ * on 127.0.0.1:4321-4330. v0.5.0 defers actually starting the server; the
119
+ * URL is informational only.
120
+ */
121
+ defaultPort?: number;
122
+ }
123
+
124
+ // --- Internals -----------------------------------------------------------
125
+
126
+ /** Same slug rule used by `cli/plan.mjs`. */
127
+ const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;
128
+
129
+ function isValidSlug(s: string): boolean {
130
+ return SLUG_REGEX.test(s);
131
+ }
132
+
133
+ function isKnownTemplate(t: string): t is DefaultTemplate {
134
+ return (KNOWN_TEMPLATES as readonly string[]).includes(t);
135
+ }
136
+
137
+ function titleCase(slug: string): string {
138
+ return slug
139
+ .split(/[-_]/)
140
+ .map((w) => (w.length === 0 ? w : w[0]!.toUpperCase() + w.slice(1)))
141
+ .join(" ");
142
+ }
143
+
144
+ /** Valid status values for `/plan status`. Mirrors plan-action.ts. */
145
+ const PLAN_STATUSES = [
146
+ "draft",
147
+ "approved",
148
+ "rejected",
149
+ "in-progress",
150
+ "done",
151
+ ] as const;
152
+ type PlanStatus = (typeof PLAN_STATUSES)[number];
153
+
154
+ function isPlanStatus(s: string): s is PlanStatus {
155
+ return (PLAN_STATUSES as readonly string[]).includes(s);
156
+ }
157
+
158
+ // --- Tokenization --------------------------------------------------------
159
+
160
+ /**
161
+ * Tokenize a subcommand argument string into argv-like tokens, with
162
+ * double-quoted strings preserved as a single token. This is just
163
+ * enough for our flag parser; it does not support escape sequences
164
+ * beyond `\"`.
165
+ *
166
+ * Examples:
167
+ * `add foo --title "Hello world" --type task` →
168
+ * ["add", "foo", "--title", "Hello world", "--type", "task"]
169
+ * `comment foo "Make this bigger"` →
170
+ * ["comment", "foo", "Make this bigger"]
171
+ */
172
+ function tokenize(input: string): string[] {
173
+ const tokens: string[] = [];
174
+ let cur = "";
175
+ let inQuotes = false;
176
+ for (let i = 0; i < input.length; i++) {
177
+ const ch = input[i]!;
178
+ if (inQuotes) {
179
+ if (ch === "\\" && i + 1 < input.length && input[i + 1] === '"') {
180
+ cur += '"';
181
+ i++;
182
+ } else if (ch === '"') {
183
+ inQuotes = false;
184
+ } else {
185
+ cur += ch;
186
+ }
187
+ continue;
188
+ }
189
+ if (ch === '"') {
190
+ inQuotes = true;
191
+ continue;
192
+ }
193
+ if (ch === " " || ch === "\t") {
194
+ if (cur !== "") {
195
+ tokens.push(cur);
196
+ cur = "";
197
+ }
198
+ continue;
199
+ }
200
+ cur += ch;
201
+ }
202
+ if (cur !== "") tokens.push(cur);
203
+ return tokens;
204
+ }
205
+
206
+ interface ParsedFlags {
207
+ positional: string[];
208
+ flags: Record<string, string>;
209
+ }
210
+
211
+ /**
212
+ * Parse a flag list of the form `--key value` (or `--key=value`).
213
+ * Positional arguments (no leading `--`) come back in order. Flags
214
+ * without a value (e.g. `--verbose`) are stored as `""`.
215
+ */
216
+ function parseFlags(tokens: string[]): ParsedFlags {
217
+ const positional: string[] = [];
218
+ const flags: Record<string, string> = {};
219
+ for (let i = 0; i < tokens.length; i++) {
220
+ const t = tokens[i]!;
221
+ if (t.startsWith("--")) {
222
+ const eq = t.indexOf("=");
223
+ if (eq !== -1) {
224
+ flags[t.slice(2, eq)] = t.slice(eq + 1);
225
+ continue;
226
+ }
227
+ const key = t.slice(2);
228
+ const next = tokens[i + 1];
229
+ if (next !== undefined && !next.startsWith("--")) {
230
+ flags[key] = next;
231
+ i++;
232
+ } else {
233
+ flags[key] = "";
234
+ }
235
+ } else {
236
+ positional.push(t);
237
+ }
238
+ }
239
+ return { positional, flags };
240
+ }
241
+
242
+ // --- The main parser -----------------------------------------------------
243
+
244
+ /**
245
+ * Parse a user message and decide whether it's a slash command.
246
+ *
247
+ * Returns:
248
+ * - `null` — not a slash command; pass through.
249
+ * - `SlashCommandResult` — slash command was recognized.
250
+ *
251
+ * Recognized commands:
252
+ * - `/visual-plan on|off|status` — settings patch only
253
+ * - `/plan new <slug> [template]` — create_plan
254
+ * - `/plan list` — list_plans
255
+ * - `/plan open <slug>` — open_plan_url
256
+ * - `/plan get <slug>` — tool_invocation: bizar_plan_action (get_canvas)
257
+ * - `/plan add <slug> [--title … --type … --x N --y N …]`
258
+ * — tool_invocation: bizar_plan_action (add_element)
259
+ * - `/plan update <slug> <id> [--x N --y N --title T --content C]`
260
+ * — tool_invocation: bizar_plan_action (update_element)
261
+ * - `/plan delete <slug> <id>` — tool_invocation: bizar_plan_action (delete_element)
262
+ * - `/plan comment <slug> [id] "text"`
263
+ * — tool_invocation: bizar_plan_action (add_comment)
264
+ * - `/plan status <slug> <status>` — tool_invocation: bizar_plan_action (set_status)
265
+ * - `/plan comments <slug> [id]` — tool_invocation: bizar_get_plan_comments
266
+ * - `/plan wait <slug> [--timeout N]`
267
+ * — deferred (returns response; no tool call)
268
+ * - `/kb` — open the Bizar Memory vault in Obsidian
269
+ * - `/help` / `/commands` — help text
270
+ */
271
+ export function parseSlashCommand(
272
+ text: string,
273
+ ctx: ParseContext,
274
+ ): SlashCommandResult | null {
275
+ if (typeof text !== "string") return null;
276
+ const trimmed = text.trim();
277
+ if (trimmed === "") return null;
278
+ if (!trimmed.startsWith("/")) return null;
279
+
280
+ // Just a bare slash — not a command
281
+ if (trimmed === "/") return null;
282
+
283
+ // Match `/<command>` optionally followed by whitespace and args
284
+ const match = /^\/([a-zA-Z][\w-]*)(?:\s+(.*))?$/.exec(trimmed);
285
+ if (match === null) return null;
286
+
287
+ const command = match[1]!.toLowerCase();
288
+ const rest = (match[2] ?? "").trim();
289
+
290
+ switch (command) {
291
+ case "visual-plan":
292
+ return handleVisualPlan(rest, ctx);
293
+ case "plan":
294
+ return handlePlan(rest, ctx);
295
+ case "bizar":
296
+ return handleBizar(rest, ctx);
297
+ case "kb":
298
+ return handleKb(rest, ctx);
299
+ case "help":
300
+ case "commands":
301
+ return helpResult();
302
+ default:
303
+ return {
304
+ handled: true,
305
+ response: `Unknown command: /${command}. Try /help for a list of available commands.`,
306
+ };
307
+ }
308
+ }
309
+
310
+ // --- Command handlers ----------------------------------------------------
311
+
312
+ function handleVisualPlan(arg: string, ctx: ParseContext): SlashCommandResult {
313
+ const lc = arg.toLowerCase();
314
+ const currentEnabled = ctx.currentSettings.visualPlanEnabled;
315
+
316
+ if (lc === "") {
317
+ // No argument — return current state as a dialog
318
+ return {
319
+ handled: true,
320
+ response: `Visual plan mode is currently ${currentEnabled ? "on" : "off"}.`,
321
+ dialog: {
322
+ id: generateId(),
323
+ title: "Visual Plan",
324
+ command: "/visual-plan",
325
+ component: "visual-plan",
326
+ data: {
327
+ enabled: currentEnabled,
328
+ previousEnabled: currentEnabled,
329
+ defaultTemplate: ctx.currentSettings.defaultTemplate,
330
+ lastUsedSlug: ctx.currentSettings.lastUsedSlug ?? null,
331
+ mode: "status",
332
+ },
333
+ },
334
+ };
335
+ }
336
+
337
+ if (lc === "on" || lc === "true" || lc === "1" || lc === "enable") {
338
+ return {
339
+ handled: true,
340
+ response: `Visual plan mode is now on.`,
341
+ settingsPatch: { visualPlanEnabled: true },
342
+ dialog: {
343
+ id: generateId(),
344
+ title: "Visual Plan",
345
+ command: "/visual-plan on",
346
+ component: "visual-plan",
347
+ data: {
348
+ enabled: true,
349
+ previousEnabled: currentEnabled,
350
+ mode: "toggle",
351
+ },
352
+ },
353
+ };
354
+ }
355
+
356
+ if (lc === "off" || lc === "false" || lc === "0" || lc === "disable") {
357
+ return {
358
+ handled: true,
359
+ response: `Visual plan mode is now off.`,
360
+ settingsPatch: { visualPlanEnabled: false },
361
+ dialog: {
362
+ id: generateId(),
363
+ title: "Visual Plan",
364
+ command: "/visual-plan off",
365
+ component: "visual-plan",
366
+ data: {
367
+ enabled: false,
368
+ previousEnabled: currentEnabled,
369
+ mode: "toggle",
370
+ },
371
+ },
372
+ };
373
+ }
374
+
375
+ if (lc === "status" || lc === "state" || lc === "?") {
376
+ return {
377
+ handled: true,
378
+ response: `Visual plan mode is currently ${currentEnabled ? "on" : "off"}.`,
379
+ dialog: {
380
+ id: generateId(),
381
+ title: "Visual Plan",
382
+ command: "/visual-plan status",
383
+ component: "visual-plan",
384
+ data: {
385
+ enabled: currentEnabled,
386
+ previousEnabled: currentEnabled,
387
+ mode: "status",
388
+ },
389
+ },
390
+ };
391
+ }
392
+
393
+ return {
394
+ handled: true,
395
+ response: `Unknown argument to /visual-plan: "${arg}". Use "on" or "off".`,
396
+ };
397
+ }
398
+
399
+ function handlePlan(arg: string, ctx: ParseContext): SlashCommandResult {
400
+ if (arg === "") {
401
+ return helpPlan();
402
+ }
403
+
404
+ // Split subcommand from its args
405
+ const tokens = tokenize(arg);
406
+ const sub = (tokens[0] ?? "").toLowerCase();
407
+ const subTokens = tokens.slice(1);
408
+
409
+ switch (sub) {
410
+ case "new":
411
+ return handlePlanNew(subTokens, ctx);
412
+ case "list":
413
+ case "ls":
414
+ return handlePlanList(ctx);
415
+ case "open":
416
+ return handlePlanOpen(subTokens, ctx);
417
+ case "get":
418
+ return handlePlanGet(subTokens);
419
+ case "add":
420
+ return handlePlanAdd(subTokens);
421
+ case "update":
422
+ return handlePlanUpdate(subTokens);
423
+ case "delete":
424
+ case "del":
425
+ case "rm":
426
+ return handlePlanDelete(subTokens);
427
+ case "comment":
428
+ case "comments":
429
+ // `/plan comment` is the add-comment verb; `/plan comments` is the
430
+ // list-comments verb. Disambiguate by looking at the first token.
431
+ if (sub === "comments") return handlePlanComments(subTokens);
432
+ return handlePlanComment(subTokens);
433
+ case "status":
434
+ return handlePlanStatus(subTokens);
435
+ case "comments-list":
436
+ return handlePlanComments(subTokens);
437
+ case "wait":
438
+ return handlePlanWait(subTokens);
439
+ default:
440
+ return {
441
+ handled: true,
442
+ response: `Unknown /plan subcommand: "${sub}". Try /plan for usage.`,
443
+ };
444
+ }
445
+ }
446
+
447
+ function helpPlan(): SlashCommandResult {
448
+ return {
449
+ handled: true,
450
+ response: "Plan commands: /plan new <slug> [template] | /plan list | /plan open <slug> | /plan get <slug> | /plan add <slug> | /plan update <slug> <id> | /plan delete <slug> <id> | /plan comment <slug> [id] \"text\" | /plan comments <slug> [id] | /plan status <slug> <status> | /plan wait <slug> [--timeout N]. Run /plan <subcommand> for details.",
451
+ dialog: {
452
+ id: generateId(),
453
+ title: "Plan Commands",
454
+ command: "/plan",
455
+ component: "help",
456
+ data: {
457
+ commands: [
458
+ { cmd: "/plan new <slug> [template]", desc: "Create a new plan" },
459
+ { cmd: "/plan list", desc: "List all plans in the worktree" },
460
+ { cmd: "/plan open <slug>", desc: "Open a plan in the viewer" },
461
+ { cmd: "/plan get <slug>", desc: "Fetch the full canvas" },
462
+ { cmd: "/plan add <slug> --title T --type kind", desc: "Add an element to a plan" },
463
+ { cmd: "/plan update <slug> <id> [--x N --y N …]", desc: "Patch an existing element" },
464
+ { cmd: "/plan delete <slug> <id>", desc: "Remove an element" },
465
+ { cmd: "/plan comment <slug> [id] \"text\"", desc: "Add a comment" },
466
+ { cmd: "/plan comments <slug> [id]", desc: "Read comments on a plan" },
467
+ { cmd: "/plan status <slug> <status>", desc: "Set the plan's status" },
468
+ { cmd: "/plan wait <slug> [--timeout N]", desc: "Wait for feedback (deferred)" },
469
+ ],
470
+ templates: KNOWN_TEMPLATES,
471
+ statuses: PLAN_STATUSES,
472
+ },
473
+ },
474
+ };
475
+ }
476
+
477
+ // --- /plan new ------------------------------------------------------------
478
+
479
+ function handlePlanNew(args: string[], ctx: ParseContext): SlashCommandResult {
480
+ if (args.length === 0 || args[0] === "") {
481
+ return {
482
+ handled: true,
483
+ response: "Usage: /plan new <slug> [template]. Available templates: " + KNOWN_TEMPLATES.join(", ") + ".",
484
+ dialog: {
485
+ id: generateId(),
486
+ title: "Create New Plan",
487
+ command: "/plan new",
488
+ component: "plan-create",
489
+ data: {
490
+ templates: [...KNOWN_TEMPLATES],
491
+ defaultTemplate: ctx.currentSettings.defaultTemplate,
492
+ suggestedSlug: "",
493
+ },
494
+ },
495
+ };
496
+ }
497
+
498
+ const slug = args[0]!;
499
+ if (!isValidSlug(slug)) {
500
+ return {
501
+ handled: true,
502
+ response:
503
+ `Invalid slug "${slug}". Slug must be lowercase, may contain hyphens, ` +
504
+ `must start with an alphanumeric character, and be 1–64 characters.`,
505
+ };
506
+ }
507
+
508
+ // Optional second arg is the template name
509
+ let template: DefaultTemplate | null = null;
510
+ if (args.length >= 2 && args[1] !== "") {
511
+ const candidate = args[1]!.toLowerCase();
512
+ if (!isKnownTemplate(candidate)) {
513
+ return {
514
+ handled: true,
515
+ response:
516
+ `Unknown template "${args[1]}". Available: ${KNOWN_TEMPLATES.join(", ")}.`,
517
+ };
518
+ }
519
+ template = candidate;
520
+ }
521
+
522
+ const resolvedTemplate = template ?? ctx.currentSettings.defaultTemplate;
523
+
524
+ return {
525
+ handled: true,
526
+ response: `Created plan "${titleCase(slug)}" with the "${resolvedTemplate}" template. Use /plan open ${slug} to open it.`,
527
+ sideEffect: {
528
+ kind: "create_plan",
529
+ slug,
530
+ template,
531
+ },
532
+ settingsPatch: { lastUsedSlug: slug },
533
+ dialog: {
534
+ id: generateId(),
535
+ title: "Plan Created",
536
+ command: `/plan new ${slug}`,
537
+ component: "generic",
538
+ data: {
539
+ message: `Plan "${titleCase(slug)}" created with the "${resolvedTemplate}" template.`,
540
+ detail: `Use /plan open ${slug} to open it.`,
541
+ },
542
+ },
543
+ };
544
+ }
545
+
546
+ // --- /plan list ----------------------------------------------------------
547
+
548
+ function handlePlanList(ctx: ParseContext): SlashCommandResult {
549
+ const slugs = ctx.availablePlanSlugs ?? [];
550
+ if (slugs.length === 0) {
551
+ return {
552
+ handled: true,
553
+ response: "No plans found in the current worktree. Use /plan new <slug> to create one.",
554
+ sideEffect: { kind: "list_plans" },
555
+ dialog: {
556
+ id: generateId(),
557
+ title: "Plans",
558
+ command: "/plan list",
559
+ component: "plan-list",
560
+ data: {
561
+ plans: slugs,
562
+ count: slugs.length,
563
+ },
564
+ },
565
+ };
566
+ }
567
+ return {
568
+ handled: true,
569
+ response: `Found ${slugs.length} plan(s) (${slugs.length}): ${slugs.join(", ")}.`,
570
+ sideEffect: { kind: "list_plans" },
571
+ dialog: {
572
+ id: generateId(),
573
+ title: "Plans",
574
+ command: "/plan list",
575
+ component: "plan-list",
576
+ data: {
577
+ plans: slugs,
578
+ count: slugs.length,
579
+ },
580
+ },
581
+ };
582
+ }
583
+
584
+ // --- /plan open ----------------------------------------------------------
585
+
586
+ function handlePlanOpen(args: string[], ctx: ParseContext): SlashCommandResult {
587
+ if (args.length === 0 || args[0] === "") {
588
+ return {
589
+ handled: true,
590
+ response: "Usage: /plan open <slug>",
591
+ };
592
+ }
593
+
594
+ const slug = args[0]!;
595
+ if (!isValidSlug(slug)) {
596
+ return {
597
+ handled: true,
598
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
599
+ };
600
+ }
601
+
602
+ const port = ctx.defaultPort ?? 4321;
603
+ const url = `http://localhost:${port}/${slug}/`;
604
+
605
+ return {
606
+ handled: true,
607
+ response: `Opening plan "${slug}" at ${url}.`,
608
+ settingsPatch: { lastUsedSlug: slug },
609
+ sideEffect: {
610
+ kind: "open_plan_url",
611
+ slug,
612
+ },
613
+ dialog: {
614
+ id: generateId(),
615
+ title: "Opening Plan",
616
+ command: `/plan open ${slug}`,
617
+ component: "generic",
618
+ data: {
619
+ message: `Opening plan: ${slug}`,
620
+ url,
621
+ },
622
+ },
623
+ };
624
+ }
625
+
626
+ // --- /plan get -----------------------------------------------------------
627
+
628
+ function handlePlanGet(args: string[]): SlashCommandResult {
629
+ if (args.length === 0 || args[0] === "") {
630
+ return { handled: true, response: "Usage: /plan get <slug>" };
631
+ }
632
+ const slug = args[0]!;
633
+ if (!isValidSlug(slug)) {
634
+ return {
635
+ handled: true,
636
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
637
+ };
638
+ }
639
+ return {
640
+ handled: true,
641
+ response: `Fetching canvas for plan "${slug}"…`,
642
+ sideEffect: {
643
+ kind: "tool_invocation",
644
+ toolName: "bizar_plan_action",
645
+ args: { action: "get_canvas", planSlug: slug },
646
+ },
647
+ settingsPatch: { lastUsedSlug: slug },
648
+ dialog: {
649
+ id: generateId(),
650
+ title: "Plan Canvas",
651
+ command: `/plan get ${slug}`,
652
+ component: "generic",
653
+ data: {
654
+ message: `Fetching canvas for plan "${slug}"…`,
655
+ },
656
+ },
657
+ };
658
+ }
659
+
660
+ // --- /plan add -----------------------------------------------------------
661
+
662
+ const ELEMENT_FLAG_KEYS = [
663
+ "title",
664
+ "content",
665
+ "type",
666
+ "id",
667
+ ] as const;
668
+ type ElementFlagKey = (typeof ELEMENT_FLAG_KEYS)[number];
669
+
670
+ const ELEMENT_NUMERIC_FLAG_KEYS = ["x", "y", "width", "height"] as const;
671
+ type ElementNumericFlagKey = (typeof ELEMENT_NUMERIC_FLAG_KEYS)[number];
672
+
673
+ function readElementFlags(flags: Record<string, string>): Record<string, unknown> {
674
+ const out: Record<string, unknown> = {};
675
+ for (const k of ELEMENT_FLAG_KEYS) {
676
+ const v = flags[k];
677
+ if (v !== undefined && v !== "") out[k] = v;
678
+ }
679
+ for (const k of ELEMENT_NUMERIC_FLAG_KEYS) {
680
+ const v = flags[k];
681
+ if (v !== undefined && v !== "") {
682
+ const n = Number(v);
683
+ if (Number.isFinite(n)) out[k] = n;
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
689
+ function handlePlanAdd(args: string[]): SlashCommandResult {
690
+ if (args.length === 0 || args[0] === "") {
691
+ return {
692
+ handled: true,
693
+ response: 'Usage: /plan add <slug> --title "Title" --type task [--x N --y N --width N --height N --content "…"]',
694
+ };
695
+ }
696
+ const slug = args[0]!;
697
+ if (!isValidSlug(slug)) {
698
+ return {
699
+ handled: true,
700
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
701
+ };
702
+ }
703
+ const { flags } = parseFlags(args.slice(1));
704
+ const element = readElementFlags(flags);
705
+ if (Object.keys(element).length === 0) {
706
+ return {
707
+ handled: true,
708
+ response:
709
+ `Usage: /plan add ${slug} --title "Title" --type task [--x N --y N --width N --height N --content "…"]\n` +
710
+ `At least one of --title / --type / --content / --x / --y is required.`,
711
+ };
712
+ }
713
+ return {
714
+ handled: true,
715
+ response: "",
716
+ sideEffect: {
717
+ kind: "tool_invocation",
718
+ toolName: "bizar_plan_action",
719
+ args: { action: "add_element", planSlug: slug, element },
720
+ },
721
+ settingsPatch: { lastUsedSlug: slug },
722
+ dialog: {
723
+ id: generateId(),
724
+ title: "Element Added",
725
+ command: `/plan add ${slug}`,
726
+ component: "generic",
727
+ data: {
728
+ message: `Adding element to plan "${slug}"…`,
729
+ },
730
+ },
731
+ };
732
+ }
733
+
734
+ // --- /plan update --------------------------------------------------------
735
+
736
+ function handlePlanUpdate(args: string[]): SlashCommandResult {
737
+ if (args.length < 2 || args[0] === "" || args[1] === "") {
738
+ return {
739
+ handled: true,
740
+ response:
741
+ 'Usage: /plan update <slug> <elementId> [--x N --y N --title T --content C --width N --height N --type kind]',
742
+ };
743
+ }
744
+ const slug = args[0]!;
745
+ const elementId = args[1]!;
746
+ if (!isValidSlug(slug)) {
747
+ return {
748
+ handled: true,
749
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
750
+ };
751
+ }
752
+ if (elementId === "") {
753
+ return {
754
+ handled: true,
755
+ response: "Element id cannot be empty.",
756
+ };
757
+ }
758
+ const { flags } = parseFlags(args.slice(2));
759
+ const element = readElementFlags(flags);
760
+ if (Object.keys(element).length === 0) {
761
+ return {
762
+ handled: true,
763
+ response:
764
+ `Usage: /plan update ${slug} ${elementId} [--x N --y N --title T --content C --width N --height N --type kind]\n` +
765
+ `At least one update flag is required.`,
766
+ };
767
+ }
768
+ return {
769
+ handled: true,
770
+ response: "",
771
+ sideEffect: {
772
+ kind: "tool_invocation",
773
+ toolName: "bizar_plan_action",
774
+ args: { action: "update_element", planSlug: slug, elementId, element },
775
+ },
776
+ settingsPatch: { lastUsedSlug: slug },
777
+ dialog: {
778
+ id: generateId(),
779
+ title: "Element Updated",
780
+ command: `/plan update ${slug}`,
781
+ component: "generic",
782
+ data: {
783
+ message: `Updating element ${elementId} in plan "${slug}"…`,
784
+ },
785
+ },
786
+ };
787
+ }
788
+
789
+ // --- /plan delete --------------------------------------------------------
790
+
791
+ function handlePlanDelete(args: string[]): SlashCommandResult {
792
+ if (args.length < 2 || args[0] === "" || args[1] === "") {
793
+ return {
794
+ handled: true,
795
+ response: "Usage: /plan delete <slug> <elementId>",
796
+ };
797
+ }
798
+ const slug = args[0]!;
799
+ const elementId = args[1]!;
800
+ if (!isValidSlug(slug)) {
801
+ return {
802
+ handled: true,
803
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
804
+ };
805
+ }
806
+ return {
807
+ handled: true,
808
+ response: "",
809
+ sideEffect: {
810
+ kind: "tool_invocation",
811
+ toolName: "bizar_plan_action",
812
+ args: { action: "delete_element", planSlug: slug, elementId },
813
+ },
814
+ settingsPatch: { lastUsedSlug: slug },
815
+ dialog: {
816
+ id: generateId(),
817
+ title: "Element Deleted",
818
+ command: `/plan delete ${slug}`,
819
+ component: "generic",
820
+ data: {
821
+ message: `Deleting element ${elementId} from plan "${slug}"…`,
822
+ },
823
+ },
824
+ };
825
+ }
826
+
827
+ // --- /plan comment (add) -------------------------------------------------
828
+
829
+ function handlePlanComment(args: string[]): SlashCommandResult {
830
+ // /plan comment <slug> [elementId] "text"
831
+ // The text is the LAST positional arg. The slug is first. The middle
832
+ // arg (if any) is the elementId. We re-tokenize the raw arg string
833
+ // (NOT the tokenized form) so that quoted text is preserved as a
834
+ // single token. We've already tokenized at the parent level, so
835
+ // we re-tokenize the raw form here by joining back together.
836
+ // The parent passes a tokenized array, so we look at the trailing
837
+ // token and assume the text is the last non-flag argument. The
838
+ // parent already did quote-preserving tokenization, so trailing
839
+ // tokens after the slug/elementId are the text.
840
+ if (args.length < 2 || args[0] === "") {
841
+ return {
842
+ handled: true,
843
+ response: 'Usage: /plan comment <slug> [elementId] "text"',
844
+ };
845
+ }
846
+ const slug = args[0]!;
847
+ if (!isValidSlug(slug)) {
848
+ return {
849
+ handled: true,
850
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
851
+ };
852
+ }
853
+ // Disambiguate: if the second token is a flag, then there's no
854
+ // elementId and the rest is the text.
855
+ if (args[1]!.startsWith("--")) {
856
+ // No elementId; everything after slug is text.
857
+ const text = args.slice(1).join(" ").trim();
858
+ if (text === "") {
859
+ return { handled: true, response: "Comment text is required." };
860
+ }
861
+ return commentSideEffect(slug, null, text);
862
+ }
863
+ // If exactly 2 args, treat second as text (no elementId).
864
+ if (args.length === 2) {
865
+ return commentSideEffect(slug, null, args[1]!);
866
+ }
867
+ // 3+ args: slug, elementId, text. The text may itself be quoted
868
+ // already (the parent tokenizer preserves quoted strings), so we
869
+ // join the rest with spaces.
870
+ const elementId = args[1]!;
871
+ const text = args.slice(2).join(" ").trim();
872
+ if (text === "") {
873
+ return { handled: true, response: "Comment text is required." };
874
+ }
875
+ return commentSideEffect(slug, elementId, text);
876
+ }
877
+
878
+ function commentSideEffect(
879
+ slug: string,
880
+ elementId: string | null,
881
+ text: string,
882
+ ): SlashCommandResult {
883
+ return {
884
+ handled: true,
885
+ response: "",
886
+ sideEffect: {
887
+ kind: "tool_invocation",
888
+ toolName: "bizar_plan_action",
889
+ args: {
890
+ action: "add_comment",
891
+ planSlug: slug,
892
+ comment: {
893
+ elementId,
894
+ author: "user",
895
+ text,
896
+ },
897
+ },
898
+ },
899
+ settingsPatch: { lastUsedSlug: slug },
900
+ dialog: {
901
+ id: generateId(),
902
+ title: "Comment Added",
903
+ command: `/plan comment ${slug}`,
904
+ component: "generic",
905
+ data: {
906
+ message:
907
+ elementId === null
908
+ ? `Adding canvas-pinned comment to plan "${slug}"…`
909
+ : `Adding comment to element ${elementId} on plan "${slug}"…`,
910
+ },
911
+ },
912
+ };
913
+ }
914
+
915
+ // --- /plan comments (list) -----------------------------------------------
916
+
917
+ function handlePlanComments(args: string[]): SlashCommandResult {
918
+ if (args.length === 0 || args[0] === "") {
919
+ return { handled: true, response: "Usage: /plan comments <slug> [elementId]" };
920
+ }
921
+ const slug = args[0]!;
922
+ if (!isValidSlug(slug)) {
923
+ return {
924
+ handled: true,
925
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
926
+ };
927
+ }
928
+ const elementId = args.length >= 2 ? args[1]! : undefined;
929
+ return {
930
+ handled: true,
931
+ response: "",
932
+ sideEffect: {
933
+ kind: "tool_invocation",
934
+ toolName: "bizar_get_plan_comments",
935
+ args: elementId === undefined ? { planSlug: slug } : { planSlug: slug, elementId },
936
+ },
937
+ settingsPatch: { lastUsedSlug: slug },
938
+ dialog: {
939
+ id: generateId(),
940
+ title: "Comments",
941
+ command: `/plan comments ${slug}`,
942
+ component: "generic",
943
+ data: {
944
+ message:
945
+ elementId === undefined
946
+ ? `Reading comments on plan "${slug}"…`
947
+ : `Reading comments on element ${elementId} of plan "${slug}"…`,
948
+ },
949
+ },
950
+ };
951
+ }
952
+
953
+ // --- /plan status --------------------------------------------------------
954
+
955
+ function handlePlanStatus(args: string[]): SlashCommandResult {
956
+ if (args.length < 2 || args[0] === "" || args[1] === "") {
957
+ return {
958
+ handled: true,
959
+ response: `Usage: /plan status <slug> <status>\nAvailable: ${PLAN_STATUSES.join(", ")}`,
960
+ };
961
+ }
962
+ const slug = args[0]!;
963
+ if (!isValidSlug(slug)) {
964
+ return {
965
+ handled: true,
966
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
967
+ };
968
+ }
969
+ const status = args[1]!;
970
+ if (!isPlanStatus(status)) {
971
+ return {
972
+ handled: true,
973
+ response: `Invalid status "${status}". Available: ${PLAN_STATUSES.join(", ")}.`,
974
+ };
975
+ }
976
+ return {
977
+ handled: true,
978
+ response: "",
979
+ sideEffect: {
980
+ kind: "tool_invocation",
981
+ toolName: "bizar_plan_action",
982
+ args: { action: "set_status", planSlug: slug, status },
983
+ },
984
+ settingsPatch: { lastUsedSlug: slug },
985
+ dialog: {
986
+ id: generateId(),
987
+ title: "Status Updated",
988
+ command: `/plan status ${slug}`,
989
+ component: "generic",
990
+ data: {
991
+ message: `Setting plan "${slug}" status to "${status}"…`,
992
+ },
993
+ },
994
+ };
995
+ }
996
+
997
+ // --- /plan wait (deferred) -----------------------------------------------
998
+
999
+ function handlePlanWait(args: string[]): SlashCommandResult {
1000
+ if (args.length === 0 || args[0] === "") {
1001
+ return { handled: true, response: "Usage: /plan wait <slug> [--timeout N]" };
1002
+ }
1003
+ const slug = args[0]!;
1004
+ if (!isValidSlug(slug)) {
1005
+ return {
1006
+ handled: true,
1007
+ response: `Invalid slug "${slug}". Slug must match ^[a-z0-9][a-z0-9-]{0,63}$.`,
1008
+ };
1009
+ }
1010
+ // Per the audit: /plan wait is deferred from MVP. We acknowledge the
1011
+ // command and point the user at the real tool. The slash command
1012
+ // does NOT block — that's the hard-rule requirement.
1013
+ return {
1014
+ handled: true,
1015
+ response:
1016
+ `"/plan wait" is deferred from the v0.5.0 MVP.\n` +
1017
+ `Until the SSE-based command ships, the agent should call ` +
1018
+ `\`bizar_wait_for_feedback\` directly as a tool call. It polls every 2s ` +
1019
+ `and returns when the user adds a comment, approves/rejects the plan, ` +
1020
+ `or the timeout fires (default 10 min, max 30 min).\n` +
1021
+ `\n` +
1022
+ `For now, the LLM can call the tool itself — this is the path the ` +
1023
+ `visual-plan flow uses. A future release will wire /plan wait to an ` +
1024
+ `SSE push from the local viewer so the user does not see a polling ` +
1025
+ `loop.`,
1026
+ };
1027
+ }
1028
+
1029
+ // --- /bizar --------------------------------------------------------------
1030
+
1031
+ /**
1032
+ * v2.5.0 — `/bizar [args]` launches the dashboard or routes a sub-request.
1033
+ *
1034
+ * Behavior:
1035
+ * - `/bizar` (no args) — emits a `launch_dashboard` side-effect. The
1036
+ * executor spawns `bizar dash start` as a detached child
1037
+ * process, then the host surfaces the URL in the response.
1038
+ * - `/bizar <args>` — passes the args to the menu command file. Today
1039
+ * the menu routes intent (`/explain`, `/plan`, `/audit`, etc.); the
1040
+ * response is the menu's natural-language routing advice.
1041
+ *
1042
+ * Note: the menu text lives in `config/commands/bizar.md` and is shipped
1043
+ * via the CLI package. The plugin only handles the no-arg case for the
1044
+ * side-effect; with args we return a short pointer so the user knows
1045
+ * where the routing table lives.
1046
+ */
1047
+ function handleBizar(arg: string, ctx: ParseContext): SlashCommandResult {
1048
+ const trimmed = arg.trim();
1049
+
1050
+ if (trimmed === "") {
1051
+ const port = ctx.defaultPort ?? 4321;
1052
+ return {
1053
+ handled: true,
1054
+ response: "",
1055
+ sideEffect: {
1056
+ kind: "launch_dashboard",
1057
+ defaultPort: port,
1058
+ },
1059
+ dialog: {
1060
+ id: generateId(),
1061
+ title: "Dashboard",
1062
+ command: "/bizar",
1063
+ component: "generic",
1064
+ data: {
1065
+ message: "Dashboard launching in the background…",
1066
+ url: `http://localhost:${port}/`,
1067
+ },
1068
+ },
1069
+ };
1070
+ }
1071
+
1072
+ // With args, defer to the menu command file shipped with the CLI.
1073
+ return {
1074
+ handled: true,
1075
+ response: "",
1076
+ dialog: {
1077
+ id: generateId(),
1078
+ title: "Bizar Commands",
1079
+ command: `/bizar ${trimmed}`,
1080
+ component: "help",
1081
+ data: {
1082
+ commands: [
1083
+ { cmd: "/bizar", desc: "Launch the Bizar dashboard" },
1084
+ { cmd: "/bizar explain <question>", desc: "Read-only code Q&A" },
1085
+ { cmd: "/bizar plan <args>", desc: "Manage plans" },
1086
+ { cmd: "/bizar audit", desc: "Run security audit" },
1087
+ { cmd: "/bizar learn", desc: "Extract patterns from session" },
1088
+ { cmd: "/bizar init", desc: "Initialize .bizar/ in this project" },
1089
+ { cmd: "/bizar pr-review", desc: "PR review" },
1090
+ ],
1091
+ },
1092
+ },
1093
+ };
1094
+ }
1095
+
1096
+ // --- /kb -----------------------------------------------------------------
1097
+
1098
+ /**
1099
+ * v4.0.0 — `/kb` opens the Bizar Memory vault in Obsidian.
1100
+ *
1101
+ * Behavior:
1102
+ * - Routes through the `bizar_open_kb` tool via tool_invocation.
1103
+ * - The tool resolves the vault path from .bizar/memory.json and
1104
+ * spawns Obsidian (or xdg-open as fallback).
1105
+ * - If memory isn't initialized, the tool returns a helpful message.
1106
+ *
1107
+ * No subcommands. With args, returns a help pointer.
1108
+ */
1109
+ function handleKb(arg: string, _ctx: ParseContext): SlashCommandResult {
1110
+ const trimmed = arg.trim();
1111
+
1112
+ if (trimmed !== "") {
1113
+ return {
1114
+ handled: true,
1115
+ response:
1116
+ `Unknown argument: \`${trimmed}\`. \`/kb\` takes no arguments.\n\n` +
1117
+ `Use \`/kb\` to open the Bizar Memory vault in Obsidian.\n` +
1118
+ `Use \`bizar memory status\` to see vault details.`,
1119
+ };
1120
+ }
1121
+
1122
+ return {
1123
+ handled: true,
1124
+ response: "Opening Bizar Memory vault in Obsidian…",
1125
+ sideEffect: {
1126
+ kind: "tool_invocation",
1127
+ toolName: "bizar_open_kb",
1128
+ args: {},
1129
+ },
1130
+ };
1131
+ }
1132
+
1133
+ // --- /help ----------------------------------------------------------------
1134
+
1135
+ function helpResult(): SlashCommandResult {
1136
+ return {
1137
+ handled: true,
1138
+ response: "Available commands: /visual-plan [on|off|status], /plan new <slug> [template], /plan list, /plan open <slug>, /plan get <slug>, /plan add <slug>, /plan update <slug> <id>, /plan delete <slug> <id>, /plan comment <slug> [id] \"text\", /plan comments <slug> [id], /plan status <slug> <status>, /plan wait <slug> [--timeout N], /kb, /bizar, /bizar <args>, /help. See the dialog for full descriptions.",
1139
+ dialog: {
1140
+ id: generateId(),
1141
+ title: "Bizar Commands",
1142
+ command: "/help",
1143
+ component: "help",
1144
+ data: {
1145
+ commands: [
1146
+ { cmd: "/visual-plan [on|off|status]", desc: "Toggle or view visual plan mode" },
1147
+ { cmd: "/plan new <slug> [template]", desc: "Create a new plan" },
1148
+ { cmd: "/plan list", desc: "List all plans in the worktree" },
1149
+ { cmd: "/plan open <slug>", desc: "Open a plan in the viewer" },
1150
+ { cmd: "/plan get <slug>", desc: "Fetch the full canvas" },
1151
+ { cmd: "/plan add <slug> --title T --type kind", desc: "Add an element to a plan" },
1152
+ { cmd: "/plan update <slug> <id> [flags]", desc: "Patch an existing element" },
1153
+ { cmd: "/plan delete <slug> <id>", desc: "Remove an element" },
1154
+ { cmd: "/plan comment <slug> [id] \"text\"", desc: "Add a comment" },
1155
+ { cmd: "/plan comments <slug> [id]", desc: "Read comments on a plan" },
1156
+ { cmd: "/plan status <slug> <status>", desc: "Set the plan's status" },
1157
+ { cmd: "/plan wait <slug> [--timeout N]", desc: "Wait for feedback (deferred)" },
1158
+ { cmd: "/bizar", desc: "Launch the Bizar dashboard" },
1159
+ { cmd: "/bizar <args>", desc: "Route a request via the menu" },
1160
+ { cmd: "/kb", desc: "Open the Bizar Memory vault in Obsidian" },
1161
+ { cmd: "/help | /commands", desc: "Show this help" },
1162
+ ],
1163
+ templates: [...KNOWN_TEMPLATES],
1164
+ statuses: [...PLAN_STATUSES],
1165
+ },
1166
+ },
1167
+ };
1168
+ }