@polderlabs/bizar 3.23.0 → 4.0.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 (346) hide show
  1. package/README.md +72 -2
  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 +249 -0
  60. package/bizar-dash/src/server/memory-schema.mjs +145 -0
  61. package/bizar-dash/src/server/memory-secrets.mjs +84 -0
  62. package/bizar-dash/src/server/memory-store.mjs +424 -0
  63. package/bizar-dash/src/server/mod-security.mjs +398 -0
  64. package/bizar-dash/src/server/mods-loader.mjs +1067 -0
  65. package/bizar-dash/src/server/notifications-store.mjs +243 -0
  66. package/bizar-dash/src/server/obsidian-store.mjs +318 -0
  67. package/bizar-dash/src/server/opencode-runner.mjs +279 -0
  68. package/bizar-dash/src/server/pair-store.mjs +138 -0
  69. package/bizar-dash/src/server/projects-store.mjs +364 -0
  70. package/bizar-dash/src/server/providers-store.mjs +789 -0
  71. package/bizar-dash/src/server/routes/_shared.mjs +415 -0
  72. package/bizar-dash/src/server/routes/activity.mjs +110 -0
  73. package/bizar-dash/src/server/routes/agents.mjs +134 -0
  74. package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
  75. package/bizar-dash/src/server/routes/auth.mjs +69 -0
  76. package/bizar-dash/src/server/routes/background.mjs +165 -0
  77. package/bizar-dash/src/server/routes/chat.mjs +485 -0
  78. package/bizar-dash/src/server/routes/config.mjs +129 -0
  79. package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
  80. package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
  81. package/bizar-dash/src/server/routes/fs.mjs +429 -0
  82. package/bizar-dash/src/server/routes/history.mjs +78 -0
  83. package/bizar-dash/src/server/routes/memory.mjs +499 -0
  84. package/bizar-dash/src/server/routes/misc.mjs +89 -0
  85. package/bizar-dash/src/server/routes/mods.mjs +359 -0
  86. package/bizar-dash/src/server/routes/notifications.mjs +54 -0
  87. package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
  88. package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
  89. package/bizar-dash/src/server/routes/overview.mjs +91 -0
  90. package/bizar-dash/src/server/routes/pair.mjs +67 -0
  91. package/bizar-dash/src/server/routes/projects.mjs +175 -0
  92. package/bizar-dash/src/server/routes/providers.mjs +47 -0
  93. package/bizar-dash/src/server/routes/schedules.mjs +104 -0
  94. package/bizar-dash/src/server/routes/settings.mjs +51 -0
  95. package/bizar-dash/src/server/routes/skills.mjs +76 -0
  96. package/bizar-dash/src/server/routes/tasks.mjs +468 -0
  97. package/bizar-dash/src/server/routes/themes.mjs +51 -0
  98. package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
  99. package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
  100. package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
  101. package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
  102. package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
  103. package/bizar-dash/src/server/schedules-runner.mjs +369 -0
  104. package/bizar-dash/src/server/schedules-store.mjs +268 -0
  105. package/bizar-dash/src/server/search-store.mjs +220 -0
  106. package/bizar-dash/src/server/serve-info.mjs +684 -0
  107. package/bizar-dash/src/server/server.mjs +796 -0
  108. package/bizar-dash/src/server/settings-store.mjs +45 -0
  109. package/bizar-dash/src/server/skills-store.mjs +300 -0
  110. package/bizar-dash/src/server/state.mjs +437 -0
  111. package/bizar-dash/src/server/tailscale-store.mjs +113 -0
  112. package/bizar-dash/src/server/task-delegator.mjs +824 -0
  113. package/bizar-dash/src/server/tasks-store.mjs +508 -0
  114. package/bizar-dash/src/server/tui.mjs +923 -0
  115. package/bizar-dash/src/server/update-store.mjs +168 -0
  116. package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
  117. package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
  118. package/bizar-dash/src/server/watcher.mjs +81 -0
  119. package/bizar-dash/src/server/yaml.mjs +238 -0
  120. package/bizar-dash/src/web/App.tsx +718 -0
  121. package/bizar-dash/src/web/MobileApp.tsx +408 -0
  122. package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
  123. package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
  124. package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
  125. package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
  126. package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
  127. package/bizar-dash/src/web/components/Button.tsx +56 -0
  128. package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
  129. package/bizar-dash/src/web/components/Card.tsx +40 -0
  130. package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
  131. package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
  132. package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
  133. package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
  134. package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
  135. package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
  136. package/bizar-dash/src/web/components/Modal.tsx +215 -0
  137. package/bizar-dash/src/web/components/Notifications.tsx +241 -0
  138. package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
  139. package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
  140. package/bizar-dash/src/web/components/Spinner.tsx +19 -0
  141. package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
  142. package/bizar-dash/src/web/components/Tag.tsx +28 -0
  143. package/bizar-dash/src/web/components/Toast.tsx +150 -0
  144. package/bizar-dash/src/web/components/Topbar.tsx +368 -0
  145. package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
  146. package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
  147. package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
  148. package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
  149. package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
  150. package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
  151. package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
  152. package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
  153. package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
  154. package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
  155. package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
  156. package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
  157. package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
  158. package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
  159. package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
  160. package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
  161. package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
  162. package/bizar-dash/src/web/components/chat/index.ts +20 -0
  163. package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
  164. package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
  165. package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
  166. package/bizar-dash/src/web/index.html +37 -0
  167. package/bizar-dash/src/web/lib/api.ts +193 -0
  168. package/bizar-dash/src/web/lib/markdown.tsx +59 -0
  169. package/bizar-dash/src/web/lib/types.ts +668 -0
  170. package/bizar-dash/src/web/lib/utils.ts +114 -0
  171. package/bizar-dash/src/web/lib/ws.ts +176 -0
  172. package/bizar-dash/src/web/main.tsx +34 -0
  173. package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
  174. package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
  175. package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
  176. package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
  177. package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
  178. package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
  179. package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
  180. package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
  181. package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
  182. package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
  183. package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
  184. package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
  185. package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
  186. package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
  187. package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
  188. package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
  189. package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
  190. package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
  191. package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
  192. package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
  193. package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
  194. package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
  195. package/bizar-dash/src/web/mobile.html +20 -0
  196. package/bizar-dash/src/web/mobile.tsx +13 -0
  197. package/bizar-dash/src/web/styles/chat.css +198 -0
  198. package/bizar-dash/src/web/styles/glyphs.css +960 -0
  199. package/bizar-dash/src/web/styles/main.css +8855 -0
  200. package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
  201. package/bizar-dash/src/web/styles/mobile.css +1960 -0
  202. package/bizar-dash/src/web/views/Activity.tsx +1203 -0
  203. package/bizar-dash/src/web/views/Agents.tsx +701 -0
  204. package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
  205. package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
  206. package/bizar-dash/src/web/views/Chat.tsx +190 -0
  207. package/bizar-dash/src/web/views/Config.tsx +1603 -0
  208. package/bizar-dash/src/web/views/History.tsx +335 -0
  209. package/bizar-dash/src/web/views/ModView.tsx +207 -0
  210. package/bizar-dash/src/web/views/Mods.tsx +693 -0
  211. package/bizar-dash/src/web/views/Overview.tsx +812 -0
  212. package/bizar-dash/src/web/views/Providers.tsx +302 -0
  213. package/bizar-dash/src/web/views/Schedules.tsx +802 -0
  214. package/bizar-dash/src/web/views/Settings.tsx +1787 -0
  215. package/bizar-dash/src/web/views/Skills.tsx +431 -0
  216. package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
  217. package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
  218. package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
  219. package/bizar-dash/templates/mod/FORMAT.md +76 -0
  220. package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
  221. package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
  222. package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
  223. package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
  224. package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
  225. package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
  226. package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
  227. package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
  228. package/bizar-dash/tests/memory-git.test.mjs +138 -0
  229. package/bizar-dash/tests/memory-schema.test.mjs +198 -0
  230. package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
  231. package/bizar-dash/tests/memory-store.test.mjs +390 -0
  232. package/bizar-dash/tests/memory-sync.test.mjs +141 -0
  233. package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
  234. package/bizar-dash/tests/mod-security.test.mjs +273 -0
  235. package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
  236. package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
  237. package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
  238. package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
  239. package/bizar-dash/tests/path-safe.test.mjs +108 -0
  240. package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
  241. package/bizar-dash/tests/smoke-v2.mjs +212 -0
  242. package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
  243. package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
  244. package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
  245. package/bizar-dash/tests/yaml.test.mjs +128 -0
  246. package/bizar-dash/tsconfig.json +23 -0
  247. package/bizar-dash/vite.config.ts +27 -0
  248. package/cli/atomic.mjs +73 -0
  249. package/cli/bin.mjs +43 -46
  250. package/cli/copy.mjs +3 -0
  251. package/cli/dev-link.mjs +17 -1
  252. package/cli/init.mjs +135 -1
  253. package/cli/install.mjs +10 -23
  254. package/cli/memory-constants.mjs +59 -0
  255. package/cli/memory.mjs +757 -0
  256. package/cli/service.mjs +5 -6
  257. package/config/skills/obsidian/SKILL.md +213 -65
  258. package/install.sh +48 -18
  259. package/package.json +49 -14
  260. package/packages/sdk/LICENSE +21 -0
  261. package/packages/sdk/README.md +80 -0
  262. package/packages/sdk/src/client.ts +183 -0
  263. package/packages/sdk/src/errors.ts +120 -0
  264. package/packages/sdk/src/events.ts +153 -0
  265. package/packages/sdk/src/index.ts +63 -0
  266. package/packages/sdk/src/types.ts +176 -0
  267. package/packages/sdk/src/version.ts +4 -0
  268. package/packages/sdk/tests/client.test.ts +217 -0
  269. package/packages/sdk/tests/errors.test.ts +108 -0
  270. package/packages/sdk/tests/events.test.ts +139 -0
  271. package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
  272. package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
  273. package/packages/sdk/tsconfig.json +25 -0
  274. package/packages/sdk/vitest.config.ts +9 -0
  275. package/plugins/bizar/LICENSE +21 -0
  276. package/plugins/bizar/README.md +448 -0
  277. package/plugins/bizar/dist/index.js +29901 -0
  278. package/plugins/bizar/index.ts +1484 -0
  279. package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
  280. package/plugins/bizar/src/background-state.ts +551 -0
  281. package/plugins/bizar/src/background.ts +1250 -0
  282. package/plugins/bizar/src/commands-impl.ts +464 -0
  283. package/plugins/bizar/src/commands.ts +1127 -0
  284. package/plugins/bizar/src/dashboard-client.ts +233 -0
  285. package/plugins/bizar/src/event-stream.ts +606 -0
  286. package/plugins/bizar/src/fingerprint.ts +120 -0
  287. package/plugins/bizar/src/handoff.ts +79 -0
  288. package/plugins/bizar/src/http-client.ts +467 -0
  289. package/plugins/bizar/src/key-rotation.ts +218 -0
  290. package/plugins/bizar/src/logger.ts +144 -0
  291. package/plugins/bizar/src/loop.ts +176 -0
  292. package/plugins/bizar/src/opencode-runner.ts +390 -0
  293. package/plugins/bizar/src/options.ts +421 -0
  294. package/plugins/bizar/src/plan-fs.ts +323 -0
  295. package/plugins/bizar/src/reasoning-clean.ts +454 -0
  296. package/plugins/bizar/src/report.ts +178 -0
  297. package/plugins/bizar/src/research-prompt.ts +35 -0
  298. package/plugins/bizar/src/serve-info.ts +228 -0
  299. package/plugins/bizar/src/serve.ts +496 -0
  300. package/plugins/bizar/src/settings.ts +349 -0
  301. package/plugins/bizar/src/state.ts +298 -0
  302. package/plugins/bizar/src/tools/bg-collect.ts +104 -0
  303. package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
  304. package/plugins/bizar/src/tools/bg-kill.ts +87 -0
  305. package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
  306. package/plugins/bizar/src/tools/bg-status.ts +99 -0
  307. package/plugins/bizar/src/tools/plan-action.ts +767 -0
  308. package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
  309. package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
  310. package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
  311. package/plugins/bizar/tests/background-state.test.ts +277 -0
  312. package/plugins/bizar/tests/background.test.ts +402 -0
  313. package/plugins/bizar/tests/block.test.ts +195 -0
  314. package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
  315. package/plugins/bizar/tests/commands-impl.test.ts +442 -0
  316. package/plugins/bizar/tests/commands.test.ts +548 -0
  317. package/plugins/bizar/tests/config.test.ts +122 -0
  318. package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
  319. package/plugins/bizar/tests/dispose.test.ts +336 -0
  320. package/plugins/bizar/tests/event-stream.test.ts +409 -0
  321. package/plugins/bizar/tests/event.test.ts +262 -0
  322. package/plugins/bizar/tests/fingerprint.test.ts +162 -0
  323. package/plugins/bizar/tests/http-client.test.ts +404 -0
  324. package/plugins/bizar/tests/init-helpers.test.ts +203 -0
  325. package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
  326. package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
  327. package/plugins/bizar/tests/key-rotation.test.ts +396 -0
  328. package/plugins/bizar/tests/loop.test.ts +397 -0
  329. package/plugins/bizar/tests/options.test.ts +276 -0
  330. package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
  331. package/plugins/bizar/tests/serve.test.ts +339 -0
  332. package/plugins/bizar/tests/settings.test.ts +351 -0
  333. package/plugins/bizar/tests/stall-think.test.ts +750 -0
  334. package/plugins/bizar/tests/state.test.ts +276 -0
  335. package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
  336. package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
  337. package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
  338. package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
  339. package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
  340. package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
  341. package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
  342. package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
  343. package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
  344. package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
  345. package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
  346. package/plugins/bizar/tsconfig.json +29 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * tests/submit-feedback.test.mjs
3
+ *
4
+ * Tests for `artifactsStore.submitFeedback()` + the
5
+ * `POST /api/artifacts/:slug/submit` route (v3.22.0).
6
+ *
7
+ * Run with:
8
+ * node --test bizar-dash/tests/submit-feedback.test.mjs
9
+ *
10
+ * Covers:
11
+ * - submitFeedback() creates feedback.md
12
+ * - meta.json status is updated to 'review'
13
+ * - meta.json lastEdited is updated
14
+ * - commentCount / questionCount reflect on-disk state
15
+ * - the API endpoint returns ok=true with counts
16
+ * - 404 for nonexistent slug
17
+ * - 400 for invalid slug
18
+ * - feedback.md has YAML frontmatter + Free-placed comments + Open-question answers + Full MDX source sections
19
+ */
20
+
21
+ import { test } from 'node:test';
22
+ import assert from 'node:assert/strict';
23
+ import {
24
+ mkdtempSync,
25
+ writeFileSync,
26
+ existsSync,
27
+ readFileSync,
28
+ mkdirSync,
29
+ rmSync,
30
+ } from 'node:fs';
31
+ import { join } from 'node:path';
32
+ import { tmpdir } from 'node:os';
33
+
34
+ import express from 'express';
35
+ import { createServer } from 'node:http';
36
+
37
+ import { artifactsStore } from '../src/server/artifacts-store.mjs';
38
+ import { createArtifactsRouter } from '../src/server/routes/artifacts.mjs';
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Per-test isolated projectRoot + helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function makeProjectRoot() {
45
+ return mkdtempSync(join(tmpdir(), 'submit-feedback-'));
46
+ }
47
+
48
+ function seedArtifact(projectRoot, slug, { comments = [], answers = [], status = 'draft' } = {}) {
49
+ const dir = join(projectRoot, 'artifacts', slug);
50
+ mkdirSync(dir, { recursive: true });
51
+ const meta = {
52
+ slug,
53
+ title: `Test ${slug}`,
54
+ status,
55
+ author: 'drb0rk',
56
+ created: '2026-01-01T00:00:00.000Z',
57
+ lastEdited: '2026-01-01T00:00:00.000Z',
58
+ };
59
+ writeFileSync(join(dir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf8');
60
+ const canvas = {
61
+ schemaVersion: 2,
62
+ title: meta.title,
63
+ elements: [],
64
+ connections: [],
65
+ comments,
66
+ viewport: { x: 0, y: 0, zoom: 1 },
67
+ };
68
+ writeFileSync(join(dir, 'plan.json'), JSON.stringify(canvas, null, 2), 'utf8');
69
+ writeFileSync(join(dir, 'comments.json'), JSON.stringify(comments, null, 2), 'utf8');
70
+ writeFileSync(join(dir, 'artifact.mdx'), `---\ntitle: "${meta.title}"\nstatus: ${status}\n---\n\n# Test\n`, 'utf8');
71
+ return dir;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // 1. submitFeedback() — core behavior
76
+ // ---------------------------------------------------------------------------
77
+
78
+ test('submitFeedback creates feedback.md', () => {
79
+ const root = makeProjectRoot();
80
+ try {
81
+ const slug = 'glyph-feedback-create';
82
+ seedArtifact(root, slug, {
83
+ comments: [{ id: 'c1', elementId: null, author: 'drb0rk', text: 'Fix the button', x: 100, y: 200, created: '2026-01-01T00:00:00Z', thread: [] }],
84
+ });
85
+ const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
86
+ assert.equal(result.ok, true);
87
+ assert.equal(result.slug, slug);
88
+ const feedbackPath = join(root, 'artifacts', slug, 'feedback.md');
89
+ assert.ok(existsSync(feedbackPath), 'feedback.md should be created');
90
+ const content = readFileSync(feedbackPath, 'utf8');
91
+ assert.ok(content.includes('glyph: ' + slug), 'frontmatter glyph line');
92
+ assert.ok(content.includes('commentCount: 1'), 'commentCount in frontmatter');
93
+ assert.ok(content.includes('## Free-placed comments'), 'has free-placed comments section');
94
+ assert.ok(content.includes('Fix the button'), 'has comment text');
95
+ assert.ok(content.includes('(100, 200)'), 'has (x, y) coordinates');
96
+ assert.ok(content.includes('## Open-question answers'), 'has open-question answers section');
97
+ assert.ok(content.includes('## Full MDX source'), 'has full MDX source section');
98
+ } finally {
99
+ rmSync(root, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test('submitFeedback updates meta.json status to "review"', () => {
104
+ const root = makeProjectRoot();
105
+ try {
106
+ const slug = 'glyph-status-update';
107
+ seedArtifact(root, slug, { status: 'draft' });
108
+ const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
109
+ assert.equal(result.ok, true);
110
+ const meta = JSON.parse(readFileSync(join(root, 'artifacts', slug, 'meta.json'), 'utf8'));
111
+ assert.equal(meta.status, 'review', 'meta.status should be "review"');
112
+ } finally {
113
+ rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test('submitFeedback updates meta.json lastEdited', () => {
118
+ const root = makeProjectRoot();
119
+ try {
120
+ const slug = 'glyph-lastedited-update';
121
+ seedArtifact(root, slug);
122
+ const before = Date.now();
123
+ const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
124
+ const after = Date.now();
125
+ assert.equal(result.ok, true);
126
+ const meta = JSON.parse(readFileSync(join(root, 'artifacts', slug, 'meta.json'), 'utf8'));
127
+ const ts = Date.parse(meta.lastEdited);
128
+ assert.ok(Number.isFinite(ts), 'lastEdited should be a parseable timestamp');
129
+ assert.ok(ts >= before && ts <= after, `lastEdited ${meta.lastEdited} should be in [${before}, ${after}]`);
130
+ } finally {
131
+ rmSync(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ test('submitFeedback returns commentCount and questionCount', () => {
136
+ const root = makeProjectRoot();
137
+ try {
138
+ const slug = 'glyph-counts';
139
+ seedArtifact(root, slug, {
140
+ comments: [
141
+ { id: 'c1', elementId: null, author: 'drb0rk', text: 'Comment 1', x: 10, y: 20, created: '2026-01-01T00:00:00Z', thread: [] },
142
+ { id: 'c2', elementId: null, author: 'drb0rk', text: 'Comment 2', x: 30, y: 40, created: '2026-01-02T00:00:00Z', thread: [] },
143
+ { id: 'c3', elementId: null, author: 'drb0rk', text: 'Comment 3', x: 50, y: 60, created: '2026-01-03T00:00:00Z', thread: [] },
144
+ ],
145
+ });
146
+ const result = artifactsStore.submitFeedback(slug, {
147
+ answers: [
148
+ { questionId: 'q1', value: 'Answer 1' },
149
+ { questionId: 'q2', value: 'Answer 2' },
150
+ ],
151
+ }, root);
152
+ assert.equal(result.ok, true);
153
+ assert.equal(result.commentCount, 3);
154
+ assert.equal(result.questionCount, 2);
155
+ } finally {
156
+ rmSync(root, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ test('submitFeedback returns ok:false for nonexistent slug', () => {
161
+ const root = makeProjectRoot();
162
+ try {
163
+ const result = artifactsStore.submitFeedback('does-not-exist', { answers: [] }, root);
164
+ assert.equal(result.ok, false);
165
+ assert.equal(result.error, 'not_found');
166
+ } finally {
167
+ rmSync(root, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ test('submitFeedback returns ok:false for invalid slug', () => {
172
+ const root = makeProjectRoot();
173
+ try {
174
+ const result = artifactsStore.submitFeedback('INVALID', { answers: [] }, root);
175
+ assert.equal(result.ok, false);
176
+ assert.equal(result.error, 'invalid_slug');
177
+ } finally {
178
+ rmSync(root, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ test('submitFeedback writes Q/A section for question answers', () => {
183
+ const root = makeProjectRoot();
184
+ try {
185
+ const slug = 'glyph-qa-section';
186
+ seedArtifact(root, slug);
187
+ const result = artifactsStore.submitFeedback(slug, {
188
+ answers: [
189
+ { questionId: 'q-color', value: 'blue' },
190
+ { questionId: 'q-size', value: 'large' },
191
+ ],
192
+ }, root);
193
+ assert.equal(result.ok, true);
194
+ const content = readFileSync(result.feedbackFile, 'utf8');
195
+ assert.ok(content.includes('## Open-question answers'));
196
+ assert.ok(content.includes('q-color'), 'question id is in feedback file');
197
+ assert.ok(content.includes('A: blue'), 'answer value is in feedback file');
198
+ assert.ok(content.includes('q-size'));
199
+ assert.ok(content.includes('A: large'));
200
+ } finally {
201
+ rmSync(root, { recursive: true, force: true });
202
+ }
203
+ });
204
+
205
+ test('submitFeedback falls back to comments.json when canvas has none', () => {
206
+ const root = makeProjectRoot();
207
+ try {
208
+ const slug = 'glyph-comments-json-fallback';
209
+ // Seed canvas with empty comments array, comments.json with one comment.
210
+ seedArtifact(root, slug);
211
+ const dir = join(root, 'artifacts', slug);
212
+ // Replace plan.json with empty comments, populate comments.json.
213
+ const canvas = JSON.parse(readFileSync(join(dir, 'plan.json'), 'utf8'));
214
+ canvas.comments = [];
215
+ writeFileSync(join(dir, 'plan.json'), JSON.stringify(canvas, null, 2), 'utf8');
216
+ writeFileSync(
217
+ join(dir, 'comments.json'),
218
+ JSON.stringify([{ id: 'cx', elementId: null, author: 'drb0rk', text: 'fallback comment', x: 5, y: 6, created: '2026-01-01T00:00:00Z', thread: [] }]),
219
+ 'utf8',
220
+ );
221
+ const result = artifactsStore.submitFeedback(slug, { answers: [] }, root);
222
+ assert.equal(result.ok, true);
223
+ assert.equal(result.commentCount, 1, 'should pick up the fallback comment');
224
+ const content = readFileSync(result.feedbackFile, 'utf8');
225
+ assert.ok(content.includes('fallback comment'));
226
+ } finally {
227
+ rmSync(root, { recursive: true, force: true });
228
+ }
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // 2. POST /api/artifacts/:slug/submit — HTTP layer
233
+ // ---------------------------------------------------------------------------
234
+
235
+ async function startServer(projectRoot) {
236
+ const app = express();
237
+ app.use(express.json({ limit: '2mb' }));
238
+ const router = createArtifactsRouter({
239
+ state: {},
240
+ broadcast: () => {},
241
+ projectRoot,
242
+ });
243
+ app.use('/api', router);
244
+ const server = createServer(app);
245
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
246
+ const port = server.address().port;
247
+ return {
248
+ server,
249
+ baseUrl: `http://127.0.0.1:${port}`,
250
+ close: () => new Promise((resolve) => server.close(resolve)),
251
+ };
252
+ }
253
+
254
+ test('POST /api/artifacts/:slug/submit returns ok=true with counts', async () => {
255
+ const root = makeProjectRoot();
256
+ try {
257
+ const slug = 'glyph-http-ok';
258
+ seedArtifact(root, slug, {
259
+ comments: [
260
+ { id: 'c1', elementId: null, author: 'drb0rk', text: 'A', x: 1, y: 1, created: '2026-01-01T00:00:00Z', thread: [] },
261
+ { id: 'c2', elementId: null, author: 'drb0rk', text: 'B', x: 2, y: 2, created: '2026-01-02T00:00:00Z', thread: [] },
262
+ ],
263
+ });
264
+ const { baseUrl, close } = await startServer(root);
265
+ try {
266
+ const res = await fetch(`${baseUrl}/api/artifacts/${slug}/submit`, {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ answers: [] }),
270
+ });
271
+ assert.equal(res.status, 200, 'HTTP 200');
272
+ const body = await res.json();
273
+ assert.equal(body.ok, true);
274
+ assert.equal(body.slug, slug);
275
+ assert.equal(body.commentCount, 2);
276
+ assert.equal(body.questionCount, 0);
277
+ assert.ok(typeof body.feedbackFile === 'string');
278
+ assert.ok(existsSync(body.feedbackFile), 'feedbackFile path returned must exist');
279
+ } finally {
280
+ await close();
281
+ }
282
+ } finally {
283
+ rmSync(root, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ test('POST /api/artifacts/:slug/submit returns 404 for nonexistent slug', async () => {
288
+ const root = makeProjectRoot();
289
+ try {
290
+ const { baseUrl, close } = await startServer(root);
291
+ try {
292
+ const res = await fetch(`${baseUrl}/api/artifacts/does-not-exist/submit`, {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({ answers: [] }),
296
+ });
297
+ assert.equal(res.status, 404);
298
+ const body = await res.json();
299
+ assert.equal(body.error, 'not_found');
300
+ } finally {
301
+ await close();
302
+ }
303
+ } finally {
304
+ rmSync(root, { recursive: true, force: true });
305
+ }
306
+ });
307
+
308
+ test('POST /api/artifacts/:slug/submit returns 400 for invalid slug', async () => {
309
+ const root = makeProjectRoot();
310
+ try {
311
+ const { baseUrl, close } = await startServer(root);
312
+ try {
313
+ const res = await fetch(`${baseUrl}/api/artifacts/INVALID/submit`, {
314
+ method: 'POST',
315
+ headers: { 'Content-Type': 'application/json' },
316
+ body: JSON.stringify({ answers: [] }),
317
+ });
318
+ assert.equal(res.status, 400);
319
+ const body = await res.json();
320
+ assert.equal(body.error, 'invalid_slug');
321
+ } finally {
322
+ await close();
323
+ }
324
+ } finally {
325
+ rmSync(root, { recursive: true, force: true });
326
+ }
327
+ });
328
+
329
+ test('POST /api/artifacts/:slug/submit persists feedback.md on disk', async () => {
330
+ const root = makeProjectRoot();
331
+ try {
332
+ const slug = 'glyph-http-disk';
333
+ seedArtifact(root, slug);
334
+ const { baseUrl, close } = await startServer(root);
335
+ try {
336
+ const res = await fetch(`${baseUrl}/api/artifacts/${slug}/submit`, {
337
+ method: 'POST',
338
+ headers: { 'Content-Type': 'application/json' },
339
+ body: JSON.stringify({ answers: [] }),
340
+ });
341
+ assert.equal(res.status, 200);
342
+ const feedbackPath = join(root, 'artifacts', slug, 'feedback.md');
343
+ assert.ok(existsSync(feedbackPath), 'feedback.md should exist on disk');
344
+ const content = readFileSync(feedbackPath, 'utf8');
345
+ assert.ok(content.startsWith('---'), 'starts with YAML frontmatter');
346
+ assert.ok(content.includes('## Full MDX source'), 'contains MDX source section');
347
+ } finally {
348
+ await close();
349
+ }
350
+ } finally {
351
+ rmSync(root, { recursive: true, force: true });
352
+ }
353
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * tests/tmux-wrap.test.mjs
3
+ *
4
+ * Regression tests for the bug where the dashboard's tmux wrap
5
+ * (`task-delegator.mjs:605`, `bg-retry.mjs:392`) tailed a phantom
6
+ * log file (`<worktree>/.bizar/opencode.log` or
7
+ * `<worktree>/.opencode/log/<id>.log`) — paths nothing in the
8
+ * system writes to.
9
+ *
10
+ * The fix uses `getActualBgLogPath()` (path-safe.mjs) which returns
11
+ * the path the plugin's `LogWriter` actually writes to:
12
+ * `~/.cache/bizar/logs/<sessionId>.log`.
13
+ *
14
+ * These tests assert the new behavior by simulating the dispatch
15
+ * code path against an injected log file. They do NOT spawn a real
16
+ * tmux session (the test environment may not have tmux).
17
+ *
18
+ * Run with: node --test tests/tmux-wrap.test.mjs
19
+ */
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import { mkdtempSync, writeFileSync, existsSync, appendFileSync, readFileSync } from 'node:fs';
23
+ import { join, resolve as pathResolve } from 'node:path';
24
+ import { tmpdir } from 'node:os';
25
+ import { spawnSync } from 'node:child_process';
26
+
27
+ import { getActualBgLogPath } from '../src/server/lib/path-safe.mjs';
28
+
29
+ const HAS_TMUX = (() => {
30
+ try {
31
+ const r = spawnSync('which', ['tmux'], { encoding: 'utf8' });
32
+ return r.status === 0 && r.stdout.trim().length > 0;
33
+ } catch {
34
+ return false;
35
+ }
36
+ })();
37
+
38
+ /**
39
+ * Simulate the tmux spawn exactly the way `background-store.mjs:278-310`
40
+ * does. Returns the spawned session name on success, or null.
41
+ */
42
+ function spawnTmuxFor(sessionName, logFile, cwd) {
43
+ if (!HAS_TMUX) return null;
44
+ const tmuxArgs = [
45
+ 'new-session', '-d', '-s', sessionName,
46
+ '-x', '220', '-y', '50',
47
+ ];
48
+ if (cwd) tmuxArgs.push('-c', cwd);
49
+ tmuxArgs.push('tail', '-n', '200', '-F', logFile);
50
+ const r = spawnSync('tmux', tmuxArgs, { stdio: 'pipe', timeout: 5_000 });
51
+ if (r.status !== 0) return null;
52
+ return sessionName;
53
+ }
54
+
55
+ function killTmuxSession(name) {
56
+ if (!HAS_TMUX) return;
57
+ spawnSync('tmux', ['kill-session', '-t', name], { stdio: 'pipe' });
58
+ }
59
+
60
+ test('tmux wrap points at the actual LogWriter path, not the phantom .bizar/opencode.log', () => {
61
+ const sessionId = 'ses_wrap_test';
62
+ // The dispatch code in task-delegator.mjs:605 calls
63
+ // getActualBgLogPath({ sessionId }) to get the tmux tail target.
64
+ const logFile = getActualBgLogPath({ sessionId });
65
+
66
+ // The historical phantom path (v3.11.0 and earlier):
67
+ const phantomPath = pathResolve(process.cwd(), '.bizar', 'opencode.log');
68
+
69
+ assert.notEqual(
70
+ logFile,
71
+ phantomPath,
72
+ 'tmux wrap must NOT tail the phantom .bizar/opencode.log path',
73
+ );
74
+ // The actual path is the LogWriter's output.
75
+ assert.ok(
76
+ logFile.endsWith(`${sessionId}.log`),
77
+ `expected log file to be <sessionId>.log, got ${logFile}`,
78
+ );
79
+ });
80
+
81
+ test('tmux wrap tail target actually exists after LogWriter writes to it (end-to-end smoke)', { skip: !HAS_TMUX }, () => {
82
+ // Make a fake LogWriter output by directly writing to the path
83
+ // the tmux wrap tails. If the paths don't match, the tmux pane
84
+ // will show "No such file or directory" — same as the v3.11.0 bug.
85
+ const dir = mkdtempSync(join(tmpdir(), 'tmux-wrap-'));
86
+ try {
87
+ process.env.BIZAR_LOG_DIR = dir;
88
+ const sessionId = 'ses_e2e';
89
+ const logFile = getActualBgLogPath({ sessionId });
90
+ // Pre-create the log file with one line, then append another
91
+ // after tmux is watching. The capture-pane should reflect both.
92
+ writeFileSync(logFile, 'first line\n', 'utf8');
93
+
94
+ const sessionName = 'bgr_e2e_smoke';
95
+ const created = spawnTmuxFor(sessionName, logFile, dir);
96
+ if (!created) {
97
+ // tmux not present in this environment — skip the rest.
98
+ return;
99
+ }
100
+
101
+ try {
102
+ // Give tail a moment to start watching.
103
+ spawnSync('sleep', ['0.3']);
104
+ appendFileSync(logFile, 'second line after tmux is watching\n', 'utf8');
105
+ // Give tail a moment to pick it up.
106
+ spawnSync('sleep', ['0.3']);
107
+
108
+ const capture = spawnSync(
109
+ 'tmux', ['capture-pane', '-t', sessionName, '-p', '-S', '-50'],
110
+ { encoding: 'utf8', timeout: 5_000 },
111
+ );
112
+ assert.equal(capture.status, 0, 'tmux capture-pane failed');
113
+ const out = capture.stdout || '';
114
+ // The pane must show at least one of the lines we wrote. The
115
+ // pre-fix phantom path would show "cannot open ... for reading".
116
+ assert.ok(
117
+ out.includes('first line') || out.includes('second line'),
118
+ `expected pane to show our log content, got: ${out}`,
119
+ );
120
+ assert.ok(
121
+ !out.includes('cannot open'),
122
+ `pane must not show phantom-file error, got: ${out}`,
123
+ );
124
+ } finally {
125
+ killTmuxSession(sessionName);
126
+ }
127
+ } finally {
128
+ delete process.env.BIZAR_LOG_DIR;
129
+ }
130
+ });
131
+
132
+ test('getActualBgLogPath falls back to ~/.cache/bizar/logs by default', () => {
133
+ // Important: when BIZAR_LOG_DIR is not set, the path must point
134
+ // at the same location the plugin's LogWriter (default logDir
135
+ // ~/.cache/bizar/logs) writes to. If these drift, the bug
136
+ // regresses silently.
137
+ const before = process.env.BIZAR_LOG_DIR;
138
+ delete process.env.BIZAR_LOG_DIR;
139
+ try {
140
+ const p = getActualBgLogPath({ sessionId: 'ses_default' });
141
+ assert.equal(p, pathResolve(process.env.HOME || '/tmp', '.cache', 'bizar', 'logs', 'ses_default.log'));
142
+ } finally {
143
+ if (before) process.env.BIZAR_LOG_DIR = before;
144
+ }
145
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * tests/yaml.test.mjs
3
+ *
4
+ * Tests for the YAML frontmatter parser.
5
+ */
6
+
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert';
9
+ import { parseFrontmatter, serializeFrontmatter, parseBlock, serializeBlock } from '../src/server/yaml.mjs';
10
+
11
+ describe('parseFrontmatter', () => {
12
+ it('parses empty frontmatter', () => {
13
+ const { frontmatter, body } = parseFrontmatter('hello world');
14
+ assert.deepStrictEqual(frontmatter, {});
15
+ assert.strictEqual(body, 'hello world');
16
+ });
17
+
18
+ it('parses scalar fields', () => {
19
+ const raw = `---\ntitle: Test Note\ncount: 42\nactive: true\n---\n\nBody content`;
20
+ const { frontmatter, body } = parseFrontmatter(raw);
21
+ assert.strictEqual(frontmatter.title, 'Test Note');
22
+ assert.strictEqual(frontmatter.count, 42);
23
+ assert.strictEqual(frontmatter.active, true);
24
+ assert.strictEqual(body, 'Body content');
25
+ });
26
+
27
+ it('parses integer and float', () => {
28
+ const raw = `---\na: 42\nb: 3.14\n---\n`;
29
+ const { frontmatter } = parseFrontmatter(raw);
30
+ assert.strictEqual(frontmatter.a, 42);
31
+ assert.strictEqual(frontmatter.b, 3.14);
32
+ });
33
+
34
+ it('parses boolean values', () => {
35
+ const raw = `---\na: true\nb: false\nc: yes\nd: no\n---\n`;
36
+ const { frontmatter } = parseFrontmatter(raw);
37
+ assert.strictEqual(frontmatter.a, true);
38
+ assert.strictEqual(frontmatter.b, false);
39
+ assert.strictEqual(frontmatter.c, true);
40
+ assert.strictEqual(frontmatter.d, false);
41
+ });
42
+
43
+ it('parses inline list', () => {
44
+ const raw = `---\ntags: [memory, architecture, planning]\n---\n`;
45
+ const { frontmatter } = parseFrontmatter(raw);
46
+ assert.deepStrictEqual(frontmatter.tags, ['memory', 'architecture', 'planning']);
47
+ });
48
+
49
+ it('parses block list', () => {
50
+ const raw = `---\ntags:\n - memory\n - architecture\n - planning\n---\n`;
51
+ const { frontmatter } = parseFrontmatter(raw);
52
+ assert.deepStrictEqual(frontmatter.tags, ['memory', 'architecture', 'planning']);
53
+ });
54
+
55
+ it('parses multi-line literal block', () => {
56
+ const raw = `---\nbody: |\n line one\n line two\n line three\n---\n`;
57
+ const { frontmatter } = parseFrontmatter(raw);
58
+ assert.strictEqual(frontmatter.body, 'line one\nline two\nline three');
59
+ });
60
+
61
+ it('ignores comment lines', () => {
62
+ const raw = `---\n# this is a comment\ntitle: Test\n# another comment\n---\n`;
63
+ const { frontmatter } = parseFrontmatter(raw);
64
+ assert.strictEqual(frontmatter.title, 'Test');
65
+ assert(!('title # another comment' in frontmatter));
66
+ });
67
+
68
+ it('parses quoted strings', () => {
69
+ const raw = `---\ntitle: "Hello World"\nsubtitle: 'Subtitle Here'\n---\n`;
70
+ const { frontmatter } = parseFrontmatter(raw);
71
+ assert.strictEqual(frontmatter.title, 'Hello World');
72
+ assert.strictEqual(frontmatter.subtitle, 'Subtitle Here');
73
+ });
74
+
75
+ it('roundtrips through serialize + parse', () => {
76
+ const original = { title: 'Test', count: 42, tags: ['a', 'b'] };
77
+ const serialized = serializeBlock(original);
78
+ const reparsed = parseBlock(serialized);
79
+ assert.strictEqual(reparsed.title, 'Test');
80
+ assert.strictEqual(reparsed.count, 42);
81
+ assert.deepStrictEqual(reparsed.tags, ['a', 'b']);
82
+ });
83
+ });
84
+
85
+ describe('serializeFrontmatter', () => {
86
+ it('serializes scalars', () => {
87
+ const fm = { title: 'My Note', count: 10, active: true };
88
+ const out = serializeFrontmatter(fm);
89
+ assert.ok(out.includes('title: My Note'));
90
+ assert.ok(out.includes('count: 10'));
91
+ assert.ok(out.includes('active: true'));
92
+ });
93
+
94
+ it('serializes arrays as inline lists when possible', () => {
95
+ const fm = { tags: ['memory', 'arch'] };
96
+ const out = serializeFrontmatter(fm);
97
+ assert.ok(out.includes('tags: [memory, arch]'));
98
+ });
99
+
100
+ it('serializes multi-line strings as literal blocks', () => {
101
+ const fm = { body: 'line1\nline2\nline3' };
102
+ const out = serializeFrontmatter(fm);
103
+ assert.ok(out.includes('body: |'));
104
+ assert.ok(out.includes(' line1'));
105
+ });
106
+
107
+ it('empty frontmatter returns empty string', () => {
108
+ assert.strictEqual(serializeFrontmatter({}), '');
109
+ });
110
+ });
111
+
112
+ describe('parseBlock', () => {
113
+ it('parses a simple key-value block', () => {
114
+ const result = parseBlock('title: Hello\nstatus: active');
115
+ assert.strictEqual(result.title, 'Hello');
116
+ assert.strictEqual(result.status, 'active');
117
+ });
118
+
119
+ it('handles empty block', () => {
120
+ const result = parseBlock('');
121
+ assert.deepStrictEqual(result, {});
122
+ });
123
+
124
+ it('parses ISO date strings as-is', () => {
125
+ const result = parseBlock('created: 2026-01-15T10:30:00Z');
126
+ assert.strictEqual(result.created, '2026-01-15T10:30:00Z');
127
+ });
128
+ });
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noUnusedLocals": false,
10
+ "noUnusedParameters": false,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "allowSyntheticDefaultImports": true,
18
+ "useDefineForClassFields": true,
19
+ "types": ["node"]
20
+ },
21
+ "include": ["src/web/**/*"],
22
+ "exclude": ["node_modules", "dist"]
23
+ }
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, resolve } from 'node:path';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ export default defineConfig({
9
+ plugins: [react()],
10
+ root: resolve(__dirname, 'src/web'),
11
+ base: '/',
12
+ build: {
13
+ outDir: resolve(__dirname, 'dist'),
14
+ emptyOutDir: true,
15
+ sourcemap: true,
16
+ rollupOptions: {
17
+ input: {
18
+ main: resolve(__dirname, 'src/web/index.html'),
19
+ mobile: resolve(__dirname, 'src/web/mobile.html'),
20
+ },
21
+ },
22
+ },
23
+ server: {
24
+ port: 5174,
25
+ strictPort: false,
26
+ },
27
+ });