@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,145 @@
1
+ /**
2
+ * src/server/memory-schema.mjs
3
+ *
4
+ * Validates frontmatter against the Bizar Memory Schema. The schema requires
5
+ * a set of standard fields (memory_id, type, project_id, status, confidence,
6
+ * created, updated, tags) with enumerated values for type, status, and
7
+ * confidence.
8
+ */
9
+
10
+ export const REQUIRED_FIELDS = [
11
+ 'memory_id',
12
+ 'type',
13
+ 'project_id',
14
+ 'status',
15
+ 'confidence',
16
+ 'created',
17
+ 'updated',
18
+ 'tags',
19
+ ];
20
+
21
+ export const VALID_TYPES = [
22
+ 'project_overview',
23
+ 'architecture_decision',
24
+ 'coding_convention',
25
+ 'bug_pattern',
26
+ 'command',
27
+ 'api_contract',
28
+ 'dependency_note',
29
+ 'environment_fact',
30
+ 'task_summary',
31
+ 'session_summary',
32
+ 'user_preference',
33
+ ];
34
+
35
+ export const VALID_STATUSES = [
36
+ 'active',
37
+ 'superseded',
38
+ 'stale',
39
+ 'conflict',
40
+ 'draft',
41
+ 'archived',
42
+ ];
43
+
44
+ export const VALID_CONFIDENCES = ['verified', 'inferred', 'speculative'];
45
+
46
+ /**
47
+ * Validate a note's frontmatter and body against the schema.
48
+ *
49
+ * @param {Record<string, unknown>} frontmatter
50
+ * @param {string} body
51
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
52
+ */
53
+ export function validateNote(frontmatter, body) {
54
+ const errors = [];
55
+ const warnings = [];
56
+
57
+ if (!frontmatter || typeof frontmatter !== 'object') {
58
+ errors.push('frontmatter must be a plain object');
59
+ return { valid: false, errors, warnings };
60
+ }
61
+
62
+ // Required fields
63
+ for (const field of REQUIRED_FIELDS) {
64
+ if (!(field in frontmatter) || frontmatter[field] === undefined || frontmatter[field] === null || frontmatter[field] === '') {
65
+ errors.push(`missing required field: ${field}`);
66
+ }
67
+ }
68
+
69
+ // Type enum
70
+ if (frontmatter.type && !VALID_TYPES.includes(frontmatter.type)) {
71
+ errors.push(`invalid type: '${frontmatter.type}'. Must be one of: ${VALID_TYPES.join(', ')}`);
72
+ }
73
+
74
+ // Status enum
75
+ if (frontmatter.status && !VALID_STATUSES.includes(frontmatter.status)) {
76
+ errors.push(`invalid status: '${frontmatter.status}'. Must be one of: ${VALID_STATUSES.join(', ')}`);
77
+ }
78
+
79
+ // Confidence enum
80
+ if (frontmatter.confidence && !VALID_CONFIDENCES.includes(frontmatter.confidence)) {
81
+ errors.push(`invalid confidence: '${frontmatter.confidence}'. Must be one of: ${VALID_CONFIDENCES.join(', ')}`);
82
+ }
83
+
84
+ // Tags must be an array
85
+ if (frontmatter.tags !== undefined && !Array.isArray(frontmatter.tags)) {
86
+ errors.push('tags must be an array');
87
+ }
88
+
89
+ // memory_id format check: should be non-empty string
90
+ if (frontmatter.memory_id && typeof frontmatter.memory_id !== 'string') {
91
+ errors.push('memory_id must be a string');
92
+ }
93
+
94
+ // project_id format check
95
+ if (frontmatter.project_id && typeof frontmatter.project_id !== 'string') {
96
+ errors.push('project_id must be a string');
97
+ }
98
+
99
+ // ISO-8601 date fields
100
+ for (const dateField of ['created', 'updated']) {
101
+ if (frontmatter[dateField]) {
102
+ const val = String(frontmatter[dateField]);
103
+ if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/.test(val)) {
104
+ warnings.push(`${dateField} is not a valid ISO-8601 date: '${val}'`);
105
+ }
106
+ }
107
+ }
108
+
109
+ // Body presence check
110
+ if (!body || !body.trim()) {
111
+ warnings.push('body is empty — notes should have substantive content');
112
+ }
113
+
114
+ return {
115
+ valid: errors.length === 0,
116
+ errors,
117
+ warnings,
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Build a default frontmatter object from a partial override.
123
+ * Fills in all missing required fields with sensible defaults.
124
+ *
125
+ * @param {Partial<Record<string, unknown>>} partial
126
+ * @returns {Record<string, unknown>}
127
+ */
128
+ export function defaultFrontmatter(partial = {}) {
129
+ const now = new Date().toISOString();
130
+ const timestamp = Date.now();
131
+ const type = partial.type || 'session_summary';
132
+ const memoryId = partial.memory_id || `${type}_${timestamp}`;
133
+
134
+ return {
135
+ memory_id: memoryId,
136
+ type: type,
137
+ project_id: partial.project_id || '',
138
+ status: partial.status || 'draft',
139
+ confidence: partial.confidence || 'inferred',
140
+ created: partial.created || now,
141
+ updated: partial.updated || now,
142
+ tags: Array.isArray(partial.tags) ? partial.tags : [],
143
+ ...partial,
144
+ };
145
+ }
@@ -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,471 @@
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
+ import * as memoryGit from './memory-git.mjs';
23
+
24
+ const HOME = homedir();
25
+ const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
26
+
27
+ /**
28
+ * Path safety: reject any relPath that would escape the vault root.
29
+ *
30
+ * @param {string} vaultRoot
31
+ * @param {string} relPath
32
+ * @returns {string | null} — resolved absolute path, or null if unsafe
33
+ */
34
+ function resolveSafe(vaultRoot, relPath) {
35
+ if (!relPath || relPath.includes('\0')) return null;
36
+ const abs = pathResolve(vaultRoot, relPath);
37
+ const rel = relative(vaultRoot, abs);
38
+ if (rel.startsWith('..') || abs !== pathResolve(abs)) return null;
39
+ return abs;
40
+ }
41
+
42
+ /**
43
+ * Read the per-project memory config file (.bizar/memory.json).
44
+ *
45
+ * @param {string} projectRoot
46
+ * @returns {{ exists: boolean, config: Record<string, unknown>, error?: string }}
47
+ */
48
+ export function loadConfig(projectRoot) {
49
+ const configPath = join(projectRoot, '.bizar', 'memory.json');
50
+ const raw = safeReadJSON(configPath, null);
51
+ if (raw === null) {
52
+ return {
53
+ exists: false,
54
+ config: {
55
+ mode: 'local-only',
56
+ projectId: projectRoot.split('/').pop() || 'unknown',
57
+ repoName: '',
58
+ },
59
+ };
60
+ }
61
+ return { exists: true, config: raw };
62
+ }
63
+
64
+ /**
65
+ * Write the per-project memory config file.
66
+ *
67
+ * @param {string} projectRoot
68
+ * @param {Record<string, unknown>} config
69
+ * @returns {{ ok: boolean, error?: string }}
70
+ */
71
+ export function saveConfig(projectRoot, config) {
72
+ try {
73
+ const configDir = join(projectRoot, '.bizar');
74
+ if (!existsSync(configDir)) {
75
+ mkdirSync(configDir, { recursive: true });
76
+ }
77
+ const configPath = join(configDir, 'memory.json');
78
+ atomicWriteJson(configPath, config);
79
+ return { ok: true };
80
+ } catch (err) {
81
+ return { ok: false, error: err.message };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Resolve the effective vault root, namespaces, and related paths based on mode.
87
+ *
88
+ * @param {string} projectRoot
89
+ * @returns {{ mode: string, projectId: string, vaultRoot: string, repoPath: string | null, configDir: string, gitRemote: string | null, branch: string, lightragDir: string, namespaces: object }}
90
+ */
91
+ export function resolveVault(projectRoot) {
92
+ const { config } = loadConfig(projectRoot);
93
+ const mr = config.memoryRepo || {};
94
+ const mode = mr.mode || config.mode || 'local-only';
95
+ const projectId = config.projectId || projectRoot.split('/').pop() || 'unknown';
96
+ const branch = mr.branch || config.branch || 'main';
97
+ const gitRemote = mr.remote || config.gitRemote || null;
98
+
99
+ let vaultRoot;
100
+ let repoPath;
101
+ if (mode === 'local-only') {
102
+ vaultRoot = join(projectRoot, '.obsidian');
103
+ repoPath = null;
104
+ } else {
105
+ // managed or linked — path may be absolute, ~-relative, or a sibling of projectRoot
106
+ const rawPath = mr.path || config.repoName || join(projectRoot, '.bizar', 'memory');
107
+ const expanded = rawPath.startsWith('~') ? join(HOME, rawPath.slice(1)) : rawPath;
108
+ repoPath = expanded;
109
+ vaultRoot = join(expanded, 'projects', projectId);
110
+ }
111
+
112
+ const configDir = join(projectRoot, '.bizar');
113
+ const lightragDir = join(projectRoot, '.bizar', 'memory-cache');
114
+ const namespaces = config.namespaces || {
115
+ project: `projects/${projectId}`,
116
+ global: 'global/bizar',
117
+ user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
118
+ };
119
+
120
+ return { mode, projectId, vaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
121
+ }
122
+
123
+ /**
124
+ * Initialize the vault for a project. In local-only mode, creates .obsidian/
125
+ * and standard subdirectories. In managed mode, the shared repo is assumed to
126
+ * already exist; this function only ensures the .bizar/memory.json is set up.
127
+ *
128
+ * NOTE: The project namespace directory (projects/<id>/) is NOT created here.
129
+ * It is created lazily on first writeNote (F7 invariant).
130
+ *
131
+ * @param {string} projectRoot
132
+ * @returns {{ ok: boolean, vaultRoot: string, created: string[], error?: string }}
133
+ */
134
+ export function initVault(projectRoot) {
135
+ const { config } = loadConfig(projectRoot);
136
+ const mr = config.memoryRepo || {};
137
+ const mode = mr.mode || config.mode || 'local-only';
138
+ const created = [];
139
+
140
+ if (mode === 'local-only') {
141
+ const vaultRoot = join(projectRoot, '.obsidian');
142
+ const subdirs = ['daily', 'decisions', 'patterns', 'api', 'tasks', 'global', 'users'];
143
+ if (!existsSync(vaultRoot)) {
144
+ mkdirSync(vaultRoot, { recursive: true });
145
+ created.push('.obsidian/');
146
+ }
147
+ for (const sub of subdirs) {
148
+ const subPath = join(vaultRoot, sub);
149
+ if (!existsSync(subPath)) {
150
+ mkdirSync(subPath, { recursive: true });
151
+ created.push(`.obsidian/${sub}/`);
152
+ }
153
+ }
154
+ return { ok: true, vaultRoot, created };
155
+ } else {
156
+ // managed / linked — create the shared repo at repoPath with `git init`
157
+ const { repoPath, vaultRoot } = resolveVault(projectRoot);
158
+ if (!existsSync(repoPath)) {
159
+ mkdirSync(repoPath, { recursive: true });
160
+ created.push(repoPath);
161
+ }
162
+ // git init if not already a git repo
163
+ const isGit = existsSync(join(repoPath, '.git'));
164
+ if (!isGit) {
165
+ try {
166
+ execFileSync('git', ['init', '-b', 'main'], { cwd: repoPath, stdio: 'pipe' });
167
+ created.push(`${repoPath}/.git/`);
168
+ } catch (err) {
169
+ return { ok: false, vaultRoot, created, error: `git init failed: ${err.message}` };
170
+ }
171
+ // Write and commit a .gitignore for runtime artifacts
172
+ const gitignorePath = join(repoPath, '.gitignore');
173
+ if (!existsSync(gitignorePath)) {
174
+ writeFileSync(gitignorePath,
175
+ '# Lock files — runtime state, not memory\n' +
176
+ '**/.sync.lock\n' +
177
+ '**/*.pid\n' +
178
+ '**/*.log\n' +
179
+ '**/.DS_Store\n' +
180
+ '\n' +
181
+ '# Local-only metadata\n' +
182
+ '**/.bizar-memory/cache/\n' +
183
+ '**/.bizar-memory/index-ledger.local.json\n',
184
+ 'utf8');
185
+ created.push(`${repoPath}/.gitignore`);
186
+ try {
187
+ execFileSync('git', ['add', '.gitignore'], { cwd: repoPath });
188
+ execFileSync('git', ['commit', '-m', 'chore: initial .gitignore for memory repo'], {
189
+ cwd: repoPath,
190
+ env: {
191
+ ...process.env,
192
+ GIT_AUTHOR_NAME: 'Bizar Memory',
193
+ GIT_AUTHOR_EMAIL: 'bizar-memory@local',
194
+ GIT_COMMITTER_NAME: 'Bizar Memory',
195
+ GIT_COMMITTER_EMAIL: 'bizar-memory@local',
196
+ },
197
+ });
198
+ } catch (e) {
199
+ // best-effort; not fatal
200
+ }
201
+ }
202
+ }
203
+ // Ensure projects/, global/, users/ exist so writeNote can find them without
204
+ // creating the project-specific namespace (F7 invariant — only the projects/<id>
205
+ // subdirectory is created lazily on first writeNote)
206
+ const projectsDir = join(repoPath, 'projects');
207
+ if (!existsSync(projectsDir)) {
208
+ mkdirSync(projectsDir, { recursive: true });
209
+ created.push(`${repoPath}/projects/`);
210
+ }
211
+ const globalDir = join(repoPath, 'global', 'bizar');
212
+ if (!existsSync(globalDir)) {
213
+ mkdirSync(globalDir, { recursive: true });
214
+ created.push(`${repoPath}/global/bizar/`);
215
+ }
216
+ const usersDir = join(repoPath, 'users');
217
+ if (!existsSync(usersDir)) {
218
+ mkdirSync(usersDir, { recursive: true });
219
+ created.push(`${repoPath}/users/`);
220
+ }
221
+ return { ok: true, vaultRoot, created };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * List all notes in the vault. Returns enriched note metadata.
227
+ *
228
+ * @param {string} projectRoot
229
+ * @param {{ namespace?: string }} [opts]
230
+ * @returns {Array<{ relPath: string, mtime: number, size: number, frontmatter: Record<string, unknown>, body: string, schemaValid: boolean }>}
231
+ */
232
+ export function listNotes(projectRoot, opts = {}) {
233
+ const { vaultRoot } = resolveVault(projectRoot);
234
+ if (!existsSync(vaultRoot)) return [];
235
+
236
+ const namespace = opts.namespace || '';
237
+ const searchRoot = namespace ? join(vaultRoot, namespace) : vaultRoot;
238
+ if (!existsSync(searchRoot)) return [];
239
+
240
+ const out = [];
241
+ function walk(dir, prefix) {
242
+ let entries;
243
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
244
+ catch { return; }
245
+ for (const e of entries) {
246
+ const full = join(dir, e.name);
247
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
248
+ if (e.isDirectory()) {
249
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
250
+ walk(full, rel);
251
+ } else if (e.name.endsWith('.md')) {
252
+ try {
253
+ const st = statSync(full);
254
+ const raw = readFileSync(full, 'utf8');
255
+ const { frontmatter, body } = parseFrontmatter(raw);
256
+ const { valid } = validateNote(frontmatter, body);
257
+ out.push({ relPath: rel, mtime: st.mtimeMs, size: st.size, frontmatter, body, schemaValid: valid });
258
+ } catch { /* skip unreadable */ }
259
+ }
260
+ }
261
+ }
262
+ walk(searchRoot, '');
263
+ return out.sort((a, b) => b.mtime - a.mtime);
264
+ }
265
+
266
+ /**
267
+ * Read a single note by relative path. Returns null if not found.
268
+ *
269
+ * @param {string} projectRoot
270
+ * @param {string} relPath
271
+ * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean } | null}
272
+ */
273
+ export function readNote(projectRoot, relPath) {
274
+ const { vaultRoot } = resolveVault(projectRoot);
275
+ const filePath = resolveSafe(vaultRoot, relPath);
276
+ if (!filePath || !existsSync(filePath)) return null;
277
+ try {
278
+ const raw = readFileSync(filePath, 'utf8');
279
+ const st = statSync(filePath);
280
+ const { frontmatter, body } = parseFrontmatter(raw);
281
+ const { valid } = validateNote(frontmatter, body);
282
+ return { relPath, frontmatter, body, raw, mtime: st.mtimeMs, size: st.size, schemaValid: valid };
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Write a note. Validates frontmatter, scans for secrets, writes the file.
290
+ *
291
+ * @param {string} projectRoot
292
+ * @param {string} relPath
293
+ * @param {{ frontmatter: Record<string, unknown>, body: string }} note
294
+ * @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean }}
295
+ * @throws {Error} if schema validation fails or HIGH-severity secret found
296
+ */
297
+ export function writeNote(projectRoot, relPath, { frontmatter, body }) {
298
+ const { vaultRoot } = resolveVault(projectRoot);
299
+
300
+ // Validate frontmatter
301
+ const { valid, errors, warnings } = validateNote(frontmatter, body);
302
+ if (!valid) {
303
+ const err = new Error(`schema validation failed: ${errors.join('; ')}`);
304
+ err.code = 'SCHEMA_VALIDATION_FAILED';
305
+ throw err;
306
+ }
307
+
308
+ // Scan for secrets
309
+ const secretResult = scanSecrets(body);
310
+ if (hasHighFindings(secretResult)) {
311
+ const snippets = secretResult.findings
312
+ .filter((f) => f.severity === 'HIGH')
313
+ .map((f) => `line ${f.line}: ${f.snippet}`)
314
+ .join('\n');
315
+ const err = new Error(`HIGH-severity secret detected:\n${snippets}`);
316
+ err.code = 'SECRET_DETECTED';
317
+ err.findings = secretResult.findings;
318
+ throw err;
319
+ }
320
+
321
+ // Build file content
322
+ const filePath = resolveSafe(vaultRoot, relPath);
323
+ if (!filePath) {
324
+ throw new Error(`Invalid note path: ${relPath}`);
325
+ }
326
+
327
+ // Lazily create the project namespace directory (F7 — not created at initVault)
328
+ const nsDir = dirname(filePath);
329
+ if (!existsSync(nsDir)) {
330
+ mkdirSync(nsDir, { recursive: true });
331
+ }
332
+
333
+ const yamlBlock = serializeFrontmatter(frontmatter);
334
+ const content = `---\n${yamlBlock}\n---\n\n${body}`;
335
+ writeFileSync(filePath, content, 'utf8');
336
+
337
+ // Auto-commit the written file to git (if configured).
338
+ // The lock serializes commits, not writes — concurrent writes will each
339
+ // trigger their own commit, which is the intended behaviour.
340
+ (() => {
341
+ try {
342
+ if (!memoryGit.isGitInstalled()) return;
343
+ const { config } = loadConfig(projectRoot);
344
+ if (config.mode === 'local-only') return;
345
+ if (config.git?.autoCommitOnMemoryWrite !== true) return;
346
+ const lockResult = memoryGit.acquireLock(vaultRoot);
347
+ if (!lockResult || lockResult.error) return; // skip if locked or error
348
+ try {
349
+ memoryGit.addFile(vaultRoot, relPath);
350
+ const summary = frontmatter.title || relPath;
351
+ const message = (config.git.commitMessageTemplate || 'memory(BizarHarness): {summary}')
352
+ .replace('{summary}', summary);
353
+ memoryGit.commit(vaultRoot, message, { author: config.git.commitAuthor });
354
+ } finally {
355
+ lockResult.release();
356
+ }
357
+ } catch (err) {
358
+ // Log but never re-throw — the write already succeeded; a failed
359
+ // auto-commit is recoverable on next write or manual commit.
360
+ console.error('[memory-store] autoCommitOnMemoryWrite failed:', err?.message || err);
361
+ }
362
+ })();
363
+
364
+ const st = statSync(filePath);
365
+ return {
366
+ relPath,
367
+ frontmatter,
368
+ body,
369
+ raw: content,
370
+ mtime: st.mtimeMs,
371
+ size: st.size,
372
+ schemaValid: true,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Delete a note by relative path.
378
+ *
379
+ * @param {string} projectRoot
380
+ * @param {string} relPath
381
+ * @returns {boolean}
382
+ */
383
+ export function deleteNote(projectRoot, relPath) {
384
+ const { vaultRoot } = resolveVault(projectRoot);
385
+ const filePath = resolveSafe(vaultRoot, relPath);
386
+ if (!filePath || !existsSync(filePath)) return false;
387
+ unlinkSync(filePath);
388
+ return true;
389
+ }
390
+
391
+ /**
392
+ * Full-text search across vault notes.
393
+ *
394
+ * @param {string} projectRoot
395
+ * @param {string} query
396
+ * @param {{ limit?: number }} [opts]
397
+ * @returns {Array<{ relPath: string, snippet: string, score: number, mtime: number }>}
398
+ */
399
+ export function searchVault(projectRoot, query, { limit = 25 } = {}) {
400
+ if (!query || typeof query !== 'string') return [];
401
+ const q = query.toLowerCase();
402
+ const tokens = q.split(/\s+/).filter((t) => t.length >= 2);
403
+ if (tokens.length === 0) return [];
404
+
405
+ const results = [];
406
+ for (const note of listNotes(projectRoot)) {
407
+ const lower = (note.body + ' ' + JSON.stringify(note.frontmatter)).toLowerCase();
408
+ let score = 0;
409
+ for (const t of tokens) {
410
+ if (lower.includes(t)) score += (lower.split(t).length - 1);
411
+ }
412
+ if (score === 0) continue;
413
+ const idx = lower.indexOf(tokens[0]);
414
+ const start = Math.max(0, idx - 60);
415
+ const end = Math.min(lower.length, idx + 160);
416
+ const snippet = (start > 0 ? '…' : '') + lower.slice(start, end).replace(/\s+/g, ' ').trim();
417
+ results.push({ relPath: note.relPath, snippet, score, mtime: note.mtime });
418
+ }
419
+
420
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
421
+ }
422
+
423
+ /**
424
+ * Scan a single note for secrets.
425
+ *
426
+ * @param {string} projectRoot
427
+ * @param {string} relPath
428
+ * @returns {{ safe: boolean, findings: Array<{ id: string, severity: string, line: number, snippet: string }> }}
429
+ */
430
+ export function scanForSecrets(projectRoot, relPath) {
431
+ const note = readNote(projectRoot, relPath);
432
+ if (!note) return { safe: true, findings: [] };
433
+ return scanSecrets(note.body);
434
+ }
435
+
436
+ /**
437
+ * Validate all notes in the vault against the schema.
438
+ *
439
+ * @param {string} projectRoot
440
+ * @returns {Array<{ relPath: string, errors: string[] }>}
441
+ */
442
+ export function validateAll(projectRoot) {
443
+ const notes = listNotes(projectRoot);
444
+ const results = [];
445
+ for (const note of notes) {
446
+ const { errors } = validateNote(note.frontmatter, note.body);
447
+ if (errors.length > 0) {
448
+ results.push({ relPath: note.relPath, errors });
449
+ }
450
+ }
451
+ return results;
452
+ }
453
+
454
+ // ── LightRAG integration (v4.1.0) ──────────────────────────────────────────
455
+ //
456
+ // Re-export the LightRAG orchestrator from the memory-store module so the
457
+ // dashboard, CLI, and tests have a single import surface. The actual
458
+ // implementation lives in `memory-lightrag.mjs`.
459
+
460
+ export {
461
+ resolveLightRAGConfig,
462
+ isInstalled as isLightRAGInstalled,
463
+ isRunning as isLightRAGRunning,
464
+ startServer as startLightRAG,
465
+ stopServer as stopLightRAG,
466
+ ensureRunning as ensureLightRAGRunning,
467
+ insertNote as insertLightRAGNote,
468
+ insertAllNotes as insertLightRAGAll,
469
+ reindexVault,
470
+ query as queryLightRAG,
471
+ } from './memory-lightrag.mjs';