@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,357 @@
1
+ /**
2
+ * mod-upgrade.node.test.mjs — Node-compatible test runner for the v3.20.5
3
+ * mod upgrade flow.
4
+ *
5
+ * Run with: node --test tests/mod-upgrade.node.test.mjs
6
+ *
7
+ * What this verifies:
8
+ * - upgradeFromRegistry() refuses if the mod is not installed
9
+ * - upgradeFromRegistry() backs up the existing folder when requested
10
+ * - upgradeFromRegistry() rolls back on install failure (backup exists)
11
+ * - The POST /api/mods/:id/upgrade route returns 404 for missing mods
12
+ * - The POST /api/mods/:id/upgrade route returns 200 with the new mod
13
+ * when the mod is upgraded end-to-end (with a stubbed fetchRegistry)
14
+ * - The /api/mods/:id/views/* route serves files from <mod>/views with
15
+ * application/javascript content-type
16
+ * - The /api/mods/:id/views/* route refuses non-JS-family extensions
17
+ */
18
+ import { describe, it, before, after, beforeEach } from 'node:test';
19
+ import assert from 'node:assert/strict';
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ rmSync,
24
+ writeFileSync,
25
+ readFileSync,
26
+ } from 'node:fs';
27
+ import { join, resolve } from 'node:path';
28
+ import { tmpdir } from 'node:os';
29
+ import http from 'node:http';
30
+
31
+ // ── Sandbox HOME before importing the loader ────────────────────────────
32
+ const SANDBOX_HOME = join(tmpdir(), `bizar-upgrade-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
33
+ process.env.HOME = SANDBOX_HOME;
34
+ process.env.XDG_CONFIG_HOME = join(SANDBOX_HOME, '.config');
35
+
36
+ const REPO = resolve(import.meta.dirname, '..');
37
+ const LOADER = join(REPO, 'src/server/mods-loader.mjs');
38
+ const ROUTES = join(REPO, 'src/server/routes/mods.mjs');
39
+
40
+ const loaderModule = await import(LOADER);
41
+ const { modsLoader } = loaderModule;
42
+ const { createModsRouter } = await import(ROUTES);
43
+
44
+ const TEST_MOD_ID = 'samplemod';
45
+ const OTHER_MOD_ID = 'othermod';
46
+
47
+ function buildTestMod(id, version = '1.0.0') {
48
+ // Build the source folder in a separate "sources" dir — installFromPath
49
+ // copies the source folder INTO ~/.config/bizar/mods/<id>, so the source
50
+ // must not be the destination.
51
+ const sourceDir = join(SANDBOX_HOME, 'sources', id);
52
+ rmSync(sourceDir, { recursive: true, force: true });
53
+ mkdirSync(sourceDir, { recursive: true });
54
+ writeFileSync(join(sourceDir, 'mod.json'), JSON.stringify({
55
+ id,
56
+ name: `Test ${id}`,
57
+ version,
58
+ description: `A test mod (${id})`,
59
+ permissions: [],
60
+ }, null, 2));
61
+ mkdirSync(join(sourceDir, 'agents'), { recursive: true });
62
+ writeFileSync(join(sourceDir, 'agents', 'thor.md'), `# Thor override for ${id}\n`);
63
+ mkdirSync(join(sourceDir, 'commands'), { recursive: true });
64
+ writeFileSync(join(sourceDir, 'commands', 'plan.md'), `# Plan command for ${id}\n`);
65
+ writeFileSync(join(sourceDir, 'INSTRUCTIONS.md'), `# INSTRUCTIONS for ${id}\n`);
66
+ return sourceDir;
67
+ }
68
+
69
+ function buildViewFile(id, filename, content) {
70
+ // Views live in the INSTALLED mod folder, not the source.
71
+ const viewsDir = join(SANDBOX_HOME, '.config/bizar/mods', id, 'views');
72
+ mkdirSync(viewsDir, { recursive: true });
73
+ writeFileSync(join(viewsDir, filename), content);
74
+ writeFileSync(join(viewsDir, 'registry.json'), JSON.stringify({
75
+ views: [{ id: 'sample', label: 'Sample', component: filename }],
76
+ }, null, 2));
77
+ }
78
+
79
+ /**
80
+ * Build a fake registry "mod" folder on disk and return a function that
81
+ * installs it directly into ~/.config/bizar/mods/<id> when called. This
82
+ * lets tests exercise upgradeFromRegistry() without hitting the network.
83
+ */
84
+ function buildFakeRegistryMod(id, version) {
85
+ const regDir = join(SANDBOX_HOME, 'fake-registry', id, version);
86
+ rmSync(regDir, { recursive: true, force: true });
87
+ mkdirSync(regDir, { recursive: true });
88
+ writeFileSync(join(regDir, 'mod.json'), JSON.stringify({
89
+ id,
90
+ name: `Test ${id}`,
91
+ version,
92
+ description: `Registry version ${version}`,
93
+ permissions: [],
94
+ }, null, 2));
95
+ mkdirSync(join(regDir, 'agents'), { recursive: true });
96
+ writeFileSync(join(regDir, 'agents', 'thor.md'), `# Thor v${version} for ${id}\n`);
97
+ mkdirSync(join(regDir, 'commands'), { recursive: true });
98
+ writeFileSync(join(regDir, 'commands', 'plan.md'), `# Plan v${version} for ${id}\n`);
99
+ writeFileSync(join(regDir, 'INSTRUCTIONS.md'), `# INSTRUCTIONS v${version} for ${id}\n`);
100
+ return regDir;
101
+ }
102
+
103
+ /**
104
+ * Patch modsLoader.installFromRegistry to skip the network fetch and
105
+ * install from the local fake-registry folder. Returns a restore fn.
106
+ */
107
+ function stubInstallFromRegistry(id, latestVersion, regDir) {
108
+ const realFetch = modsLoader.fetchRegistry;
109
+ const realInstall = modsLoader.installFromRegistry;
110
+ modsLoader.fetchRegistry = async () => ({
111
+ version: 99,
112
+ updatedAt: new Date().toISOString(),
113
+ mods: [{ id, name: `Test ${id}`, latest: latestVersion }],
114
+ });
115
+ modsLoader.installFromRegistry = async (rid, opts = {}) => {
116
+ assert.equal(rid, id, 'installFromRegistry called with wrong id');
117
+ const target = join(SANDBOX_HOME, '.config/bizar/mods', id);
118
+ if (existsSync(target)) {
119
+ throw new Error(`mod "${id}" already installed (stub check)`);
120
+ }
121
+ const { cpSync } = await import('node:fs');
122
+ mkdirSync(target, { recursive: true });
123
+ cpSync(regDir, target, { recursive: true });
124
+ // Mirror installFromUrl's behaviour: install instructions into opencode config.
125
+ const { modsLoader: _ignored } = await import(LOADER);
126
+ const innerLoader = await import(LOADER);
127
+ // Call the internal installModInstructions by going through reinstallInstructions
128
+ innerLoader.modsLoader.reinstallInstructions(id);
129
+ return innerLoader.modsLoader.get(id);
130
+ };
131
+ return () => {
132
+ modsLoader.fetchRegistry = realFetch;
133
+ modsLoader.installFromRegistry = realInstall;
134
+ };
135
+ }
136
+
137
+ before(() => {
138
+ mkdirSync(join(SANDBOX_HOME, '.config/opencode/agents'), { recursive: true });
139
+ mkdirSync(join(SANDBOX_HOME, '.config/opencode/commands'), { recursive: true });
140
+ mkdirSync(join(SANDBOX_HOME, '.opencode/skills'), { recursive: true });
141
+ mkdirSync(join(SANDBOX_HOME, '.config/bizar/mods'), { recursive: true });
142
+ // Pin the registry URL to a local stub we'll override per test.
143
+ });
144
+
145
+ after(() => {
146
+ try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
147
+ });
148
+
149
+ beforeEach(() => {
150
+ // Clear installed mods + opencode instruction prefixes between tests.
151
+ for (const prefix of [TEST_MOD_ID, OTHER_MOD_ID]) {
152
+ const modDir = join(SANDBOX_HOME, '.config/bizar/mods', prefix);
153
+ rmSync(modDir, { recursive: true, force: true });
154
+ }
155
+ // Reset real-fetch state by patching fetchRegistry to a controllable stub.
156
+ });
157
+
158
+ // ── 1. upgradeFromRegistry() — basic rejection ─────────────────────────
159
+
160
+ describe('upgradeFromRegistry — preconditions', () => {
161
+ it('throws when the mod is not installed', async () => {
162
+ await assert.rejects(
163
+ () => modsLoader.upgradeFromRegistry('never-installed-xyz', { url: 'file:///nope' }),
164
+ /not installed/i,
165
+ );
166
+ });
167
+ });
168
+
169
+ // ── 2. upgradeFromRegistry() — happy path with a stubbed registry ─────
170
+
171
+ describe('upgradeFromRegistry — end-to-end with stubbed registry', () => {
172
+ it('upgrades in place when the registry returns a newer version', async () => {
173
+ // Install v1.0.0 from a source folder.
174
+ const v1Source = buildTestMod(TEST_MOD_ID, '1.0.0');
175
+ const installed = modsLoader.installFromPath(v1Source);
176
+ assert.equal(installed.version, '1.0.0');
177
+
178
+ // Build a fake registry entry for v2.0.0 and stub installFromRegistry
179
+ // to copy from the local fake-registry folder instead of fetching.
180
+ const v2RegDir = buildFakeRegistryMod(TEST_MOD_ID, '2.0.0');
181
+ const restore = stubInstallFromRegistry(TEST_MOD_ID, '2.0.0', v2RegDir);
182
+ try {
183
+ const result = await modsLoader.upgradeFromRegistry(TEST_MOD_ID, {});
184
+ assert.equal(result.from, '1.0.0');
185
+ assert.equal(result.to, '2.0.0');
186
+ assert.equal(result.mod.id, TEST_MOD_ID);
187
+ assert.equal(result.mod.version, '2.0.0');
188
+ assert.equal(result.backupPath, null);
189
+ // The new instructions are installed in opencode config under the
190
+ // `<modId>__` prefix.
191
+ assert.ok(
192
+ existsSync(join(SANDBOX_HOME, '.config/opencode/agents', `${TEST_MOD_ID}__thor.md`)),
193
+ 'agents/<modId>__thor.md should exist after upgrade',
194
+ );
195
+ assert.ok(
196
+ existsSync(join(SANDBOX_HOME, '.config/opencode/commands', `${TEST_MOD_ID}__plan.md`)),
197
+ 'commands/<modId>__plan.md should exist after upgrade',
198
+ );
199
+ } finally {
200
+ restore();
201
+ }
202
+ });
203
+
204
+ it('creates a backup when { backup: true }', async () => {
205
+ const v1Source = buildTestMod(TEST_MOD_ID, '1.0.0');
206
+ modsLoader.installFromPath(v1Source);
207
+
208
+ const v2RegDir = buildFakeRegistryMod(TEST_MOD_ID, '2.5.0');
209
+ const restore = stubInstallFromRegistry(TEST_MOD_ID, '2.5.0', v2RegDir);
210
+ try {
211
+ const result = await modsLoader.upgradeFromRegistry(TEST_MOD_ID, { backup: true });
212
+ assert.ok(result.backupPath, 'backupPath should be set when backup:true');
213
+ assert.ok(existsSync(result.backupPath), 'backup folder should exist on disk');
214
+ assert.ok(
215
+ existsSync(join(result.backupPath, 'mod.json')),
216
+ 'backup should contain the original mod.json',
217
+ );
218
+ const backupManifest = JSON.parse(readFileSync(join(result.backupPath, 'mod.json'), 'utf8'));
219
+ assert.equal(backupManifest.version, '1.0.0', 'backup should have the OLD version');
220
+ } finally {
221
+ restore();
222
+ }
223
+ });
224
+ });
225
+
226
+ // ── 3. POST /api/mods/:id/upgrade route ────────────────────────────────
227
+
228
+ async function startTestServer() {
229
+ const express = (await import('express')).default;
230
+ const app = express();
231
+ app.use(express.json({ limit: '2mb' }));
232
+ app.use('/api', createModsRouter());
233
+ const server = http.createServer(app);
234
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
235
+ const { port } = server.address();
236
+ return {
237
+ server,
238
+ baseUrl: `http://127.0.0.1:${port}`,
239
+ close: () => new Promise((resolve) => server.close(resolve)),
240
+ };
241
+ }
242
+
243
+ function request(baseUrl, path, { method = 'GET', body } = {}) {
244
+ return new Promise((resolve, reject) => {
245
+ const url = new URL(path, baseUrl);
246
+ const data = body ? JSON.stringify(body) : null;
247
+ const req = http.request({
248
+ method,
249
+ hostname: url.hostname,
250
+ port: url.port,
251
+ path: url.pathname + url.search,
252
+ headers: data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {},
253
+ }, (res) => {
254
+ let chunks = '';
255
+ res.on('data', (c) => chunks += c);
256
+ res.on('end', () => {
257
+ let json = null;
258
+ try { json = chunks ? JSON.parse(chunks) : null; } catch { /* keep raw */ }
259
+ resolve({ status: res.statusCode, headers: res.headers, body: chunks, json });
260
+ });
261
+ });
262
+ req.on('error', reject);
263
+ if (data) req.write(data);
264
+ req.end();
265
+ });
266
+ }
267
+
268
+ describe('POST /api/mods/:id/upgrade — HTTP', () => {
269
+ let serverHandle;
270
+
271
+ before(async () => { serverHandle = await startTestServer(); });
272
+ after(async () => { await serverHandle.close(); });
273
+
274
+ it('returns 404 when the mod is not installed', async () => {
275
+ const res = await request(serverHandle.baseUrl, '/api/mods/never-installed/upgrade', {
276
+ method: 'POST', body: {},
277
+ });
278
+ assert.equal(res.status, 404);
279
+ assert.equal(res.json.error, 'upgrade_failed');
280
+ });
281
+
282
+ it('upgrades and returns from/to versions (with stubbed fetchRegistry)', async () => {
283
+ const v1Source = buildTestMod(OTHER_MOD_ID, '1.0.0');
284
+ modsLoader.installFromPath(v1Source);
285
+
286
+ const v2RegDir = buildFakeRegistryMod(OTHER_MOD_ID, '1.5.0');
287
+ const restore = stubInstallFromRegistry(OTHER_MOD_ID, '1.5.0', v2RegDir);
288
+ try {
289
+ const res = await request(
290
+ serverHandle.baseUrl,
291
+ `/api/mods/${OTHER_MOD_ID}/upgrade`,
292
+ { method: 'POST', body: {} },
293
+ );
294
+ assert.equal(res.status, 200, `unexpected status ${res.status}: ${res.body}`);
295
+ assert.equal(res.json.ok, true);
296
+ assert.equal(res.json.from, '1.0.0');
297
+ assert.equal(res.json.to, '1.5.0');
298
+ assert.equal(res.json.modId, OTHER_MOD_ID);
299
+ assert.equal(res.json.mod.version, '1.5.0');
300
+ } finally {
301
+ restore();
302
+ }
303
+ });
304
+ });
305
+
306
+ // ── 4. /api/mods/:id/views/* — content-type + path safety ──────────────
307
+
308
+ describe('GET /api/mods/:id/views/*', () => {
309
+ let serverHandle;
310
+ before(async () => { serverHandle = await startTestServer(); });
311
+ after(async () => { await serverHandle.close(); });
312
+
313
+ it('serves .js view files with application/javascript content-type', async () => {
314
+ const source = buildTestMod('viewmod', '1.0.0');
315
+ modsLoader.installFromPath(source);
316
+ buildViewFile('viewmod', 'SampleView.js',
317
+ 'export default function SampleView() { return null; }\n');
318
+
319
+ const res = await request(serverHandle.baseUrl, '/api/mods/viewmod/views/SampleView.js');
320
+ assert.equal(res.status, 200);
321
+ assert.match(res.headers['content-type'] || '', /application\/javascript/);
322
+ assert.match(res.body, /export default function SampleView/);
323
+ });
324
+
325
+ it('rejects non-JS-family extensions', async () => {
326
+ const source = buildTestMod('viewmod2', '1.0.0');
327
+ modsLoader.installFromPath(source);
328
+ // Try to fetch a .html file from views/ — should be forbidden.
329
+ const viewsDir = join(SANDBOX_HOME, '.config/bizar/mods', 'viewmod2', 'views');
330
+ mkdirSync(viewsDir, { recursive: true });
331
+ writeFileSync(join(viewsDir, 'index.html'), '<!doctype html>');
332
+
333
+ const res = await request(serverHandle.baseUrl, '/api/mods/viewmod2/views/index.html');
334
+ assert.equal(res.status, 403);
335
+ });
336
+
337
+ it('returns 404 for missing files', async () => {
338
+ const source = buildTestMod('viewmod3', '1.0.0');
339
+ modsLoader.installFromPath(source);
340
+
341
+ const res = await request(serverHandle.baseUrl, '/api/mods/viewmod3/views/Missing.js');
342
+ assert.equal(res.status, 404);
343
+ });
344
+
345
+ it('refuses paths that escape the views/ directory', async () => {
346
+ const source = buildTestMod('viewmod4', '1.0.0');
347
+ modsLoader.installFromPath(source);
348
+ const res = await request(serverHandle.baseUrl, '/api/mods/viewmod4/views/..%2Fmod.json');
349
+ assert.ok([400, 403, 404].includes(res.status),
350
+ `expected 400/403/404 for path traversal, got ${res.status}`);
351
+ });
352
+
353
+ it('returns 404 for views of a non-installed mod', async () => {
354
+ const res = await request(serverHandle.baseUrl, '/api/mods/never-here/views/SampleView.js');
355
+ assert.equal(res.status, 404);
356
+ });
357
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * no-agent-browser.node.test.mjs — regression test for the v3.20.7 cleanup.
3
+ *
4
+ * The `agent-browser` MCP tool was the previous browser-automation path.
5
+ * v3.20.7 replaced it with `browser-harness` (the Python tool from
6
+ * https://github.com/browser-use/browser-harness). This test ensures
7
+ * no agent-browser references leak back into the shipped config or
8
+ * the install/bootstrap scripts.
9
+ *
10
+ * Allowed exceptions:
11
+ * - .bizar/AGENTS_SELF_IMPROVEMENT.md (historical rule entry documenting
12
+ * the v3.20.7 migration)
13
+ * - CHANGELOG.md (historical release notes)
14
+ *
15
+ * Run with: node --test tests/no-agent-browser.node.test.mjs
16
+ */
17
+ import { describe, it } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
20
+ import { join, resolve } from 'node:path';
21
+
22
+ const REPO = resolve(import.meta.dirname, '..', '..');
23
+
24
+ const SCAN_DIRS = [
25
+ 'config',
26
+ 'cli',
27
+ 'bizar-dash/src',
28
+ 'install.sh',
29
+ ];
30
+ const SCAN_EXTENSIONS = ['.mjs', '.ts', '.tsx', '.json', '.sh', '.md', '.mdx', '.md.template'];
31
+ const ALLOW_FILES = new Set([
32
+ '.bizar/AGENTS_SELF_IMPROVEMENT.md', // historical rule entry
33
+ 'CHANGELOG.md', // historical release notes
34
+ ]);
35
+ const PATTERNS = [
36
+ /agent[-_]?browser/i, // agent-browser, agent_browser, agentBrowser
37
+ /agent_browser_/i, // agent_browser_open etc.
38
+ ];
39
+
40
+ function isAllowed(relPath) {
41
+ if (ALLOW_FILES.has(relPath)) return true;
42
+ return false;
43
+ }
44
+
45
+ function* walkFiles(root) {
46
+ const stat = statSync(root);
47
+ if (stat.isFile()) {
48
+ if (SCAN_EXTENSIONS.some((ext) => root.endsWith(ext))) yield root;
49
+ return;
50
+ }
51
+ if (!stat.isDirectory()) return;
52
+ // Skip noise
53
+ if (root.includes('node_modules')) return;
54
+ if (root.includes('.git')) return;
55
+ for (const entry of readdirSync(root)) {
56
+ yield* walkFiles(join(root, entry));
57
+ }
58
+ }
59
+
60
+ describe('no agent-browser references in shipped code', () => {
61
+ const violations = [];
62
+
63
+ for (const target of SCAN_DIRS) {
64
+ const full = join(REPO, target);
65
+ let exists = true;
66
+ try { statSync(full); } catch { exists = false; }
67
+ if (!exists) continue;
68
+ for (const file of walkFiles(full)) {
69
+ const rel = file.slice(REPO.length + 1);
70
+ if (isAllowed(rel)) continue;
71
+ let text;
72
+ try { text = readFileSync(file, 'utf8'); } catch { continue; }
73
+ for (const pat of PATTERNS) {
74
+ const m = text.match(pat);
75
+ if (m) {
76
+ violations.push({ file: rel, match: m[0] });
77
+ break; // one violation per file is enough
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ it('reports zero violations', () => {
84
+ if (violations.length > 0) {
85
+ const msg = violations
86
+ .map((v) => ` ${v.file}: matched "${v.match}"`)
87
+ .join('\n');
88
+ assert.fail(`agent-browser references found in shipped code:\n${msg}`);
89
+ }
90
+ });
91
+
92
+ it('documents the explicit allow-list', () => {
93
+ // Sanity: the allow-list itself shouldn't be empty (we have real
94
+ // historical references that should be preserved).
95
+ assert.ok(ALLOW_FILES.size >= 2,
96
+ 'expected ALLOW_FILES to include at least AGENTS_SELF_IMPROVEMENT.md + CHANGELOG.md');
97
+ });
98
+ });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * tests/obsidian-back-compat.test.mjs
3
+ *
4
+ * Tests for the backward-compatible obsidian.mjs rewrite.
5
+ * Verifies that the new router returns the same JSON shapes as the original.
6
+ */
7
+
8
+ import { describe, it, beforeEach, afterEach } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { mkdirSync, rmSync } from 'node:fs';
13
+ import http from 'node:http';
14
+ import express from 'express';
15
+
16
+ const SR = '../src/server'; // from tests/ → bizarre-dash/src/server
17
+
18
+ async function get(url, port) {
19
+ return new Promise((resolve, reject) => {
20
+ const req = http.request({ hostname: 'localhost', port, path: url, method: 'GET' }, (res) => {
21
+ let data = '';
22
+ res.on('data', (c) => { data += c; });
23
+ res.on('end', () => {
24
+ try { data = JSON.parse(data); } catch { /* not JSON */ }
25
+ resolve({ status: res.statusCode, body: data });
26
+ });
27
+ });
28
+ req.on('error', reject);
29
+ req.end();
30
+ });
31
+ }
32
+
33
+ async function post(url, port, payload) {
34
+ return new Promise((resolve, reject) => {
35
+ const body = JSON.stringify(payload);
36
+ const req = http.request(
37
+ { hostname: 'localhost', port, path: url, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
38
+ (res) => {
39
+ let data = '';
40
+ res.on('data', (c) => { data += c; });
41
+ res.on('end', () => {
42
+ try { data = JSON.parse(data); } catch { /* not JSON */ }
43
+ resolve({ status: res.statusCode, body: data });
44
+ });
45
+ },
46
+ );
47
+ req.on('error', reject);
48
+ req.write(body);
49
+ req.end();
50
+ });
51
+ }
52
+
53
+ describe('obsidian-back-compat', () => {
54
+ let projectRoot;
55
+ let server;
56
+ let port;
57
+
58
+ beforeEach(() => {
59
+ projectRoot = join(tmpdir(), `obc-${Date.now()}-${Math.random().toString(36).slice(2)}`);
60
+ mkdirSync(projectRoot, { recursive: true });
61
+ mkdirSync(join(projectRoot, '.bizar'), { recursive: true });
62
+ });
63
+
64
+ afterEach(async () => {
65
+ if (server) {
66
+ await new Promise((r) => server.close(r));
67
+ server = null;
68
+ }
69
+ try { rmSync(projectRoot, { recursive: true, force: true }); } catch { /* ignore */ }
70
+ });
71
+
72
+ // ── With memory.json (memory service mode) ─────────────────────────────────
73
+
74
+ describe('memory service mode (memory.json exists)', () => {
75
+ it('GET /obsidian returns vaultStats shape', async () => {
76
+ const ms = await import(`${SR}/memory-store.mjs`);
77
+ ms.saveConfig(projectRoot, {
78
+ version: 1, backend: 'bizar-local', projectId: 'test',
79
+ memoryRepo: { mode: 'local-only', path: join(projectRoot, '.obsidian'), remote: null, branch: 'main', namespace: null },
80
+ namespaces: { project: 'projects/test', global: 'global/bizar', user: 'users/local' },
81
+ lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
82
+ git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(test): {summary}' },
83
+ });
84
+ ms.initVault(projectRoot);
85
+ ms.writeNote(projectRoot, 'test-note.md', {
86
+ frontmatter: { memory_id: 't1', type: 'session_summary', project_id: 'test', status: 'active', confidence: 'verified', created: new Date().toISOString(), updated: new Date().toISOString(), tags: [] },
87
+ body: 'Test body',
88
+ });
89
+
90
+ const app = express();
91
+ app.use(express.json());
92
+ const { createObsidianRouter } = await import(`${SR}/routes/obsidian.mjs`);
93
+ app.use(await createObsidianRouter({ projectRoot }));
94
+ server = app.listen(0);
95
+ port = server.address().port;
96
+
97
+ const { status, body } = await get('/obsidian', port);
98
+ assert.strictEqual(status, 200);
99
+ assert.strictEqual(typeof body.exists, 'boolean');
100
+ assert.strictEqual(typeof body.vaultDir, 'string');
101
+ assert.strictEqual(typeof body.noteCount, 'number');
102
+ assert.strictEqual(typeof body.totalSize, 'number');
103
+ });
104
+
105
+ it('GET /obsidian/notes returns legacy shape { notes: [{ path, relPath, mtime, size }]}', async () => {
106
+ const ms = await import(`${SR}/memory-store.mjs`);
107
+ ms.saveConfig(projectRoot, {
108
+ version: 1, backend: 'bizar-local', projectId: 'test',
109
+ memoryRepo: { mode: 'local-only', path: join(projectRoot, '.obsidian'), remote: null, branch: 'main', namespace: null },
110
+ namespaces: { project: 'projects/test', global: 'global/bizar', user: 'users/local' },
111
+ lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
112
+ git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(test): {summary}' },
113
+ });
114
+ ms.initVault(projectRoot);
115
+ ms.writeNote(projectRoot, 'lnt.md', {
116
+ frontmatter: { memory_id: 'lnt1', type: 'session_summary', project_id: 'test', status: 'active', confidence: 'verified', created: new Date().toISOString(), updated: new Date().toISOString(), tags: [] },
117
+ body: 'body',
118
+ });
119
+
120
+ const app = express();
121
+ app.use(express.json());
122
+ const { createObsidianRouter } = await import(`${SR}/routes/obsidian.mjs`);
123
+ app.use(await createObsidianRouter({ projectRoot }));
124
+ server = app.listen(0);
125
+ port = server.address().port;
126
+
127
+ const { status, body } = await get('/obsidian/notes', port);
128
+ assert.strictEqual(status, 200);
129
+ assert.ok(Array.isArray(body.notes));
130
+ assert.ok(body.notes.length > 0);
131
+ const note = body.notes[0];
132
+ assert.strictEqual(typeof note.path, 'string');
133
+ assert.strictEqual(typeof note.relPath, 'string');
134
+ assert.strictEqual(typeof note.mtime, 'number');
135
+ assert.strictEqual(typeof note.size, 'number');
136
+ assert.strictEqual(note.frontmatter, undefined); // NOT rich shape
137
+ });
138
+
139
+ it('POST /obsidian/notes returns 201 with FULL note shape', async () => {
140
+ const ms = await import(`${SR}/memory-store.mjs`);
141
+ ms.saveConfig(projectRoot, {
142
+ version: 1, backend: 'bizar-local', projectId: 'test',
143
+ memoryRepo: { mode: 'local-only', path: join(projectRoot, '.obsidian'), remote: null, branch: 'main', namespace: null },
144
+ namespaces: { project: 'projects/test', global: 'global/bizar', user: 'users/local' },
145
+ lightrag: { enabled: false, host: '127.0.0.1', port: 9621, workingDir: join(projectRoot, '.bizar', 'lightrag') },
146
+ git: { autoPullOnSessionStart: false, autoCommitOnMemoryWrite: false, autoPushOnSessionEnd: false, commitAuthor: 'Bizar Memory <bizar-memory@local>', commitMessageTemplate: 'memory(test): {summary}' },
147
+ });
148
+ ms.initVault(projectRoot);
149
+
150
+ const app = express();
151
+ app.use(express.json());
152
+ const { createObsidianRouter } = await import(`${SR}/routes/obsidian.mjs`);
153
+ app.use(await createObsidianRouter({ projectRoot }));
154
+ server = app.listen(0);
155
+ port = server.address().port;
156
+
157
+ const { status, body } = await post('/obsidian/notes', port, {
158
+ path: 'post-test.md',
159
+ frontmatter: { memory_id: 'pt1', type: 'session_summary', project_id: 'test', status: 'active', confidence: 'verified', created: new Date().toISOString(), updated: new Date().toISOString(), tags: [] },
160
+ body: 'Posted body content',
161
+ });
162
+
163
+ assert.strictEqual(status, 201);
164
+ assert.strictEqual(typeof body.relPath, 'string');
165
+ assert.strictEqual(typeof body.frontmatter, 'object');
166
+ assert.strictEqual(typeof body.body, 'string');
167
+ assert.strictEqual(typeof body.raw, 'string');
168
+ assert.strictEqual(typeof body.mtime, 'number');
169
+ assert.strictEqual(typeof body.size, 'number');
170
+ assert.strictEqual(body.relPath, 'post-test.md');
171
+ });
172
+ });
173
+
174
+ // ── Without memory.json (legacy mode) ───────────────────────────────────────
175
+
176
+ describe('legacy mode (no memory.json)', () => {
177
+ it('GET /obsidian returns legacy vaultStats shape', async () => {
178
+ const os = await import(`${SR}/obsidian-store.mjs`);
179
+ os.initObsidianVault(projectRoot);
180
+ os.writeVaultNote(projectRoot, 'legacy.md', {
181
+ frontmatter: { title: 'Legacy Note' },
182
+ body: 'Legacy body',
183
+ });
184
+
185
+ const app = express();
186
+ app.use(express.json());
187
+ const { createObsidianRouter } = await import(`${SR}/routes/obsidian.mjs`);
188
+ app.use(await createObsidianRouter({ projectRoot }));
189
+ server = app.listen(0);
190
+ port = server.address().port;
191
+
192
+ const { status, body } = await get('/obsidian', port);
193
+ assert.strictEqual(status, 200);
194
+ assert.strictEqual(body.exists, true);
195
+ assert.strictEqual(typeof body.vaultDir, 'string');
196
+ assert.strictEqual(typeof body.noteCount, 'number');
197
+ });
198
+ });
199
+ });