@polderlabs/bizar 3.23.1 → 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,84 @@
1
+ /**
2
+ * src/server/memory-secrets.mjs
3
+ *
4
+ * Regex-based secret scanner for structured long-term memory notes.
5
+ * Detects HIGH and MEDIUM severity patterns (API keys, tokens, private keys,
6
+ * absolute paths, private IPv4 addresses). Designed to run before every
7
+ * commit to prevent accidental secret leakage.
8
+ */
9
+
10
+ /**
11
+ * All secret patterns. severity is 'HIGH' or 'MEDIUM'.
12
+ *
13
+ * @type {Array<{ id: string, severity: 'HIGH' | 'MEDIUM', pattern: RegExp }>}
14
+ */
15
+ export const SECRET_PATTERNS = [
16
+ // HIGH severity
17
+ { id: 'private_key_pem', severity: 'HIGH', pattern: /-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----/ },
18
+ { id: 'aws_access_key', severity: 'HIGH', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
19
+ { id: 'aws_secret_key', severity: 'HIGH', pattern: /\baws_secret_access_key\s*[:=]\s*[A-Za-z0-9\/+=]{40}\b/ },
20
+ { id: 'github_pat_classic', severity: 'HIGH', pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
21
+ { id: 'github_pat_fine', severity: 'HIGH', pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
22
+ { id: 'slack_token', severity: 'HIGH', pattern: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/ },
23
+ { id: 'stripe_live', severity: 'HIGH', pattern: /\bsk_live_[0-9a-zA-Z]{24,}\b/ },
24
+ { id: 'stripe_test', severity: 'HIGH', pattern: /\bsk_test_[0-9a-zA-Z]{24,}\b/ },
25
+ // Bearer token: refined to require base64-like chars after "Bearer "
26
+ // and terminate on whitespace, punctuation, or end-of-string
27
+ {
28
+ id: 'bearer_token',
29
+ severity: 'HIGH',
30
+ pattern: /\bBearer\s+[A-Za-z0-9+\/=_\-]{20,}(?:\s|$|[,;.])/,
31
+ },
32
+ // MEDIUM severity
33
+ {
34
+ id: 'api_key_assignment',
35
+ severity: 'MEDIUM',
36
+ // Non-greedy {16,}? so the end-of-line assertion has something to match
37
+ pattern: /\b(api[_-]?key|apikey|secret)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{16,}?['"]?(?:\s*$|[,;.\n])/im,
38
+ },
39
+ { id: 'absolute_path', severity: 'MEDIUM', pattern: /\/(Users|home)\/[A-Za-z0-9._-]+/ },
40
+ { id: 'private_ipv4', severity: 'MEDIUM', pattern: /\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)\b/ },
41
+ ];
42
+
43
+ /**
44
+ * Scan text for secret patterns. Returns a structured scan result.
45
+ *
46
+ * @param {string} text — raw text to scan (usually the full body of a note)
47
+ * @returns {{ safe: boolean, findings: Array<{ id: string, severity: 'HIGH' | 'MEDIUM', line: number, snippet: string }> }}
48
+ */
49
+ export function scan(text) {
50
+ if (typeof text !== 'string') return { safe: true, findings: [] };
51
+
52
+ const findings = [];
53
+ const lines = text.split('\n');
54
+
55
+ for (const { id, severity, pattern } of SECRET_PATTERNS) {
56
+ // Reset lastIndex each iteration for global patterns
57
+ pattern.lastIndex = 0;
58
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
59
+ const line = lines[lineNum];
60
+ pattern.lastIndex = 0;
61
+ const match = pattern.exec(line);
62
+ if (match) {
63
+ const snippet = line.slice(Math.max(0, match.index - 20), match.index + match[0].length + 20);
64
+ findings.push({ id, severity, line: lineNum + 1, snippet });
65
+ // Only report first match per pattern per line
66
+ }
67
+ }
68
+ }
69
+
70
+ return {
71
+ safe: findings.filter((f) => f.severity === 'HIGH').length === 0,
72
+ findings,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Returns true if the scan result contains any HIGH-severity findings.
78
+ *
79
+ * @param {{ safe: boolean, findings: Array<{ severity: string }> }} result
80
+ * @returns {boolean}
81
+ */
82
+ export function hasHighFindings(result) {
83
+ return result.findings.some((f) => f.severity === 'HIGH');
84
+ }
@@ -0,0 +1,424 @@
1
+ /**
2
+ * src/server/memory-store.mjs
3
+ *
4
+ * MarkdownMemoryStore — reads/writes Obsidian-compatible `.md` notes with YAML
5
+ * frontmatter for the Bizar Memory Service. Supports three modes:
6
+ * local-only — vault at <projectRoot>/.obsidian/
7
+ * managed — vault at ~/.local/share/bizar/memory/<repoName>/projects/<projectId>/
8
+ * linked — alias for managed
9
+ *
10
+ * The project namespace directory is created LAZY on first writeNote, not at
11
+ * initVault time (F7 invariant).
12
+ */
13
+
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
15
+ import { execFileSync } from 'node:child_process';
16
+ import { join, dirname, relative, resolve as pathResolve, sep } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { parseFrontmatter, serializeFrontmatter } from './yaml.mjs';
19
+ import { validateNote } from './memory-schema.mjs';
20
+ import { scan as scanSecrets, hasHighFindings } from './memory-secrets.mjs';
21
+ import { atomicWriteJson, safeReadJSON, safeReadText } from '../../../cli/atomic.mjs';
22
+
23
+ const HOME = homedir();
24
+ const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
25
+
26
+ /**
27
+ * Path safety: reject any relPath that would escape the vault root.
28
+ *
29
+ * @param {string} vaultRoot
30
+ * @param {string} relPath
31
+ * @returns {string | null} — resolved absolute path, or null if unsafe
32
+ */
33
+ function resolveSafe(vaultRoot, relPath) {
34
+ if (!relPath || relPath.includes('\0')) return null;
35
+ const abs = pathResolve(vaultRoot, relPath);
36
+ const rel = relative(vaultRoot, abs);
37
+ if (rel.startsWith('..') || abs !== pathResolve(abs)) return null;
38
+ return abs;
39
+ }
40
+
41
+ /**
42
+ * Read the per-project memory config file (.bizar/memory.json).
43
+ *
44
+ * @param {string} projectRoot
45
+ * @returns {{ exists: boolean, config: Record<string, unknown>, error?: string }}
46
+ */
47
+ export function loadConfig(projectRoot) {
48
+ const configPath = join(projectRoot, '.bizar', 'memory.json');
49
+ const raw = safeReadJSON(configPath, null);
50
+ if (raw === null) {
51
+ return {
52
+ exists: false,
53
+ config: {
54
+ mode: 'local-only',
55
+ projectId: projectRoot.split('/').pop() || 'unknown',
56
+ repoName: '',
57
+ },
58
+ };
59
+ }
60
+ return { exists: true, config: raw };
61
+ }
62
+
63
+ /**
64
+ * Write the per-project memory config file.
65
+ *
66
+ * @param {string} projectRoot
67
+ * @param {Record<string, unknown>} config
68
+ * @returns {{ ok: boolean, error?: string }}
69
+ */
70
+ export function saveConfig(projectRoot, config) {
71
+ try {
72
+ const configDir = join(projectRoot, '.bizar');
73
+ if (!existsSync(configDir)) {
74
+ mkdirSync(configDir, { recursive: true });
75
+ }
76
+ const configPath = join(configDir, 'memory.json');
77
+ atomicWriteJson(configPath, config);
78
+ return { ok: true };
79
+ } catch (err) {
80
+ return { ok: false, error: err.message };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Resolve the effective vault root, namespaces, and related paths based on mode.
86
+ *
87
+ * @param {string} projectRoot
88
+ * @returns {{ mode: string, projectId: string, vaultRoot: string, repoPath: string | null, configDir: string, gitRemote: string | null, branch: string, lightragDir: string, namespaces: object }}
89
+ */
90
+ export function resolveVault(projectRoot) {
91
+ const { config } = loadConfig(projectRoot);
92
+ const mr = config.memoryRepo || {};
93
+ const mode = mr.mode || config.mode || 'local-only';
94
+ const projectId = config.projectId || projectRoot.split('/').pop() || 'unknown';
95
+ const branch = mr.branch || config.branch || 'main';
96
+ const gitRemote = mr.remote || config.gitRemote || null;
97
+
98
+ let vaultRoot;
99
+ let repoPath;
100
+ if (mode === 'local-only') {
101
+ vaultRoot = join(projectRoot, '.obsidian');
102
+ repoPath = null;
103
+ } else {
104
+ // managed or linked — path may be absolute, ~-relative, or a sibling of projectRoot
105
+ const rawPath = mr.path || config.repoName || join(projectRoot, '.bizar', 'memory');
106
+ const expanded = rawPath.startsWith('~') ? join(HOME, rawPath.slice(1)) : rawPath;
107
+ repoPath = expanded;
108
+ vaultRoot = join(expanded, 'projects', projectId);
109
+ }
110
+
111
+ const configDir = join(projectRoot, '.bizar');
112
+ const lightragDir = join(projectRoot, '.bizar', 'memory-cache');
113
+ const namespaces = config.namespaces || {
114
+ project: `projects/${projectId}`,
115
+ global: 'global/bizar',
116
+ user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
117
+ };
118
+
119
+ return { mode, projectId, vaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
120
+ }
121
+
122
+ /**
123
+ * Initialize the vault for a project. In local-only mode, creates .obsidian/
124
+ * and standard subdirectories. In managed mode, the shared repo is assumed to
125
+ * already exist; this function only ensures the .bizar/memory.json is set up.
126
+ *
127
+ * NOTE: The project namespace directory (projects/<id>/) is NOT created here.
128
+ * It is created lazily on first writeNote (F7 invariant).
129
+ *
130
+ * @param {string} projectRoot
131
+ * @returns {{ ok: boolean, vaultRoot: string, created: string[], error?: string }}
132
+ */
133
+ export function initVault(projectRoot) {
134
+ const { config } = loadConfig(projectRoot);
135
+ const mr = config.memoryRepo || {};
136
+ const mode = mr.mode || config.mode || 'local-only';
137
+ const created = [];
138
+
139
+ if (mode === 'local-only') {
140
+ const vaultRoot = join(projectRoot, '.obsidian');
141
+ const subdirs = ['daily', 'decisions', 'patterns', 'api', 'tasks', 'global', 'users'];
142
+ if (!existsSync(vaultRoot)) {
143
+ mkdirSync(vaultRoot, { recursive: true });
144
+ created.push('.obsidian/');
145
+ }
146
+ for (const sub of subdirs) {
147
+ const subPath = join(vaultRoot, sub);
148
+ if (!existsSync(subPath)) {
149
+ mkdirSync(subPath, { recursive: true });
150
+ created.push(`.obsidian/${sub}/`);
151
+ }
152
+ }
153
+ return { ok: true, vaultRoot, created };
154
+ } else {
155
+ // managed / linked — create the shared repo at repoPath with `git init`
156
+ const { repoPath, vaultRoot } = resolveVault(projectRoot);
157
+ if (!existsSync(repoPath)) {
158
+ mkdirSync(repoPath, { recursive: true });
159
+ created.push(repoPath);
160
+ }
161
+ // git init if not already a git repo
162
+ const isGit = existsSync(join(repoPath, '.git'));
163
+ if (!isGit) {
164
+ try {
165
+ execFileSync('git', ['init', '-b', 'main'], { cwd: repoPath, stdio: 'pipe' });
166
+ created.push(`${repoPath}/.git/`);
167
+ } catch (err) {
168
+ return { ok: false, vaultRoot, created, error: `git init failed: ${err.message}` };
169
+ }
170
+ // Write and commit a .gitignore for runtime artifacts
171
+ const gitignorePath = join(repoPath, '.gitignore');
172
+ if (!existsSync(gitignorePath)) {
173
+ writeFileSync(gitignorePath,
174
+ '# Lock files — runtime state, not memory\n' +
175
+ '**/.sync.lock\n' +
176
+ '**/*.pid\n' +
177
+ '**/*.log\n' +
178
+ '**/.DS_Store\n' +
179
+ '\n' +
180
+ '# Local-only metadata\n' +
181
+ '**/.bizar-memory/cache/\n' +
182
+ '**/.bizar-memory/index-ledger.local.json\n',
183
+ 'utf8');
184
+ created.push(`${repoPath}/.gitignore`);
185
+ try {
186
+ execFileSync('git', ['add', '.gitignore'], { cwd: repoPath });
187
+ execFileSync('git', ['commit', '-m', 'chore: initial .gitignore for memory repo'], {
188
+ cwd: repoPath,
189
+ env: {
190
+ ...process.env,
191
+ GIT_AUTHOR_NAME: 'Bizar Memory',
192
+ GIT_AUTHOR_EMAIL: 'bizar-memory@local',
193
+ GIT_COMMITTER_NAME: 'Bizar Memory',
194
+ GIT_COMMITTER_EMAIL: 'bizar-memory@local',
195
+ },
196
+ });
197
+ } catch (e) {
198
+ // best-effort; not fatal
199
+ }
200
+ }
201
+ }
202
+ // Ensure projects/, global/, users/ exist so writeNote can find them without
203
+ // creating the project-specific namespace (F7 invariant — only the projects/<id>
204
+ // subdirectory is created lazily on first writeNote)
205
+ const projectsDir = join(repoPath, 'projects');
206
+ if (!existsSync(projectsDir)) {
207
+ mkdirSync(projectsDir, { recursive: true });
208
+ created.push(`${repoPath}/projects/`);
209
+ }
210
+ const globalDir = join(repoPath, 'global', 'bizar');
211
+ if (!existsSync(globalDir)) {
212
+ mkdirSync(globalDir, { recursive: true });
213
+ created.push(`${repoPath}/global/bizar/`);
214
+ }
215
+ const usersDir = join(repoPath, 'users');
216
+ if (!existsSync(usersDir)) {
217
+ mkdirSync(usersDir, { recursive: true });
218
+ created.push(`${repoPath}/users/`);
219
+ }
220
+ return { ok: true, vaultRoot, created };
221
+ }
222
+ }
223
+
224
+ /**
225
+ * List all notes in the vault. Returns enriched note metadata.
226
+ *
227
+ * @param {string} projectRoot
228
+ * @param {{ namespace?: string }} [opts]
229
+ * @returns {Array<{ relPath: string, mtime: number, size: number, frontmatter: Record<string, unknown>, body: string, schemaValid: boolean }>}
230
+ */
231
+ export function listNotes(projectRoot, opts = {}) {
232
+ const { vaultRoot } = resolveVault(projectRoot);
233
+ if (!existsSync(vaultRoot)) return [];
234
+
235
+ const namespace = opts.namespace || '';
236
+ const searchRoot = namespace ? join(vaultRoot, namespace) : vaultRoot;
237
+ if (!existsSync(searchRoot)) return [];
238
+
239
+ const out = [];
240
+ function walk(dir, prefix) {
241
+ let entries;
242
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
243
+ catch { return; }
244
+ for (const e of entries) {
245
+ const full = join(dir, e.name);
246
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
247
+ if (e.isDirectory()) {
248
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
249
+ walk(full, rel);
250
+ } else if (e.name.endsWith('.md')) {
251
+ try {
252
+ const st = statSync(full);
253
+ const raw = readFileSync(full, 'utf8');
254
+ const { frontmatter, body } = parseFrontmatter(raw);
255
+ const { valid } = validateNote(frontmatter, body);
256
+ out.push({ relPath: rel, mtime: st.mtimeMs, size: st.size, frontmatter, body, schemaValid: valid });
257
+ } catch { /* skip unreadable */ }
258
+ }
259
+ }
260
+ }
261
+ walk(searchRoot, '');
262
+ return out.sort((a, b) => b.mtime - a.mtime);
263
+ }
264
+
265
+ /**
266
+ * Read a single note by relative path. Returns null if not found.
267
+ *
268
+ * @param {string} projectRoot
269
+ * @param {string} relPath
270
+ * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean } | null}
271
+ */
272
+ export function readNote(projectRoot, relPath) {
273
+ const { vaultRoot } = resolveVault(projectRoot);
274
+ const filePath = resolveSafe(vaultRoot, relPath);
275
+ if (!filePath || !existsSync(filePath)) return null;
276
+ try {
277
+ const raw = readFileSync(filePath, 'utf8');
278
+ const st = statSync(filePath);
279
+ const { frontmatter, body } = parseFrontmatter(raw);
280
+ const { valid } = validateNote(frontmatter, body);
281
+ return { relPath, frontmatter, body, raw, mtime: st.mtimeMs, size: st.size, schemaValid: valid };
282
+ } catch {
283
+ return null;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Write a note. Validates frontmatter, scans for secrets, writes the file.
289
+ *
290
+ * @param {string} projectRoot
291
+ * @param {string} relPath
292
+ * @param {{ frontmatter: Record<string, unknown>, body: string }} note
293
+ * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean }}
294
+ * @throws {Error} if schema validation fails or HIGH-severity secret found
295
+ */
296
+ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
297
+ const { vaultRoot } = resolveVault(projectRoot);
298
+
299
+ // Validate frontmatter
300
+ const { valid, errors, warnings } = validateNote(frontmatter, body);
301
+ if (!valid) {
302
+ const err = new Error(`schema validation failed: ${errors.join('; ')}`);
303
+ err.code = 'SCHEMA_VALIDATION_FAILED';
304
+ throw err;
305
+ }
306
+
307
+ // Scan for secrets
308
+ const secretResult = scanSecrets(body);
309
+ if (hasHighFindings(secretResult)) {
310
+ const snippets = secretResult.findings
311
+ .filter((f) => f.severity === 'HIGH')
312
+ .map((f) => `line ${f.line}: ${f.snippet}`)
313
+ .join('\n');
314
+ const err = new Error(`HIGH-severity secret detected:\n${snippets}`);
315
+ err.code = 'SECRET_DETECTED';
316
+ err.findings = secretResult.findings;
317
+ throw err;
318
+ }
319
+
320
+ // Build file content
321
+ const filePath = resolveSafe(vaultRoot, relPath);
322
+ if (!filePath) {
323
+ throw new Error(`Invalid note path: ${relPath}`);
324
+ }
325
+
326
+ // Lazily create the project namespace directory (F7 — not created at initVault)
327
+ const nsDir = dirname(filePath);
328
+ if (!existsSync(nsDir)) {
329
+ mkdirSync(nsDir, { recursive: true });
330
+ }
331
+
332
+ const yamlBlock = serializeFrontmatter(frontmatter);
333
+ const content = `---\n${yamlBlock}\n---\n\n${body}`;
334
+ writeFileSync(filePath, content, 'utf8');
335
+
336
+ const st = statSync(filePath);
337
+ return {
338
+ relPath,
339
+ frontmatter,
340
+ body,
341
+ raw: content,
342
+ mtime: st.mtimeMs,
343
+ size: st.size,
344
+ schemaValid: true,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Delete a note by relative path.
350
+ *
351
+ * @param {string} projectRoot
352
+ * @param {string} relPath
353
+ * @returns {boolean}
354
+ */
355
+ export function deleteNote(projectRoot, relPath) {
356
+ const { vaultRoot } = resolveVault(projectRoot);
357
+ const filePath = resolveSafe(vaultRoot, relPath);
358
+ if (!filePath || !existsSync(filePath)) return false;
359
+ unlinkSync(filePath);
360
+ return true;
361
+ }
362
+
363
+ /**
364
+ * Full-text search across vault notes.
365
+ *
366
+ * @param {string} projectRoot
367
+ * @param {string} query
368
+ * @param {{ limit?: number }} [opts]
369
+ * @returns {Array<{ relPath: string, snippet: string, score: number, mtime: number }>}
370
+ */
371
+ export function searchVault(projectRoot, query, { limit = 25 } = {}) {
372
+ if (!query || typeof query !== 'string') return [];
373
+ const q = query.toLowerCase();
374
+ const tokens = q.split(/\s+/).filter((t) => t.length >= 2);
375
+ if (tokens.length === 0) return [];
376
+
377
+ const results = [];
378
+ for (const note of listNotes(projectRoot)) {
379
+ const lower = (note.body + ' ' + JSON.stringify(note.frontmatter)).toLowerCase();
380
+ let score = 0;
381
+ for (const t of tokens) {
382
+ if (lower.includes(t)) score += (lower.split(t).length - 1);
383
+ }
384
+ if (score === 0) continue;
385
+ const idx = lower.indexOf(tokens[0]);
386
+ const start = Math.max(0, idx - 60);
387
+ const end = Math.min(lower.length, idx + 160);
388
+ const snippet = (start > 0 ? '…' : '') + lower.slice(start, end).replace(/\s+/g, ' ').trim();
389
+ results.push({ relPath: note.relPath, snippet, score, mtime: note.mtime });
390
+ }
391
+
392
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
393
+ }
394
+
395
+ /**
396
+ * Scan a single note for secrets.
397
+ *
398
+ * @param {string} projectRoot
399
+ * @param {string} relPath
400
+ * @returns {{ safe: boolean, findings: Array<{ id: string, severity: string, line: number, snippet: string }> }}
401
+ */
402
+ export function scanForSecrets(projectRoot, relPath) {
403
+ const note = readNote(projectRoot, relPath);
404
+ if (!note) return { safe: true, findings: [] };
405
+ return scanSecrets(note.body);
406
+ }
407
+
408
+ /**
409
+ * Validate all notes in the vault against the schema.
410
+ *
411
+ * @param {string} projectRoot
412
+ * @returns {Array<{ relPath: string, errors: string[] }>}
413
+ */
414
+ export function validateAll(projectRoot) {
415
+ const notes = listNotes(projectRoot);
416
+ const results = [];
417
+ for (const note of notes) {
418
+ const { errors } = validateNote(note.frontmatter, note.body);
419
+ if (errors.length > 0) {
420
+ results.push({ relPath: note.relPath, errors });
421
+ }
422
+ }
423
+ return results;
424
+ }