@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,243 @@
1
+ /**
2
+ * src/server/notifications-store.mjs
3
+ *
4
+ * v3.3.0 — Per-user notification stream.
5
+ *
6
+ * Storage:
7
+ * ~/.config/bizar/notifications.jsonl (append-only log)
8
+ *
9
+ * Each entry:
10
+ * { id, ts, severity, message, source, read, meta? }
11
+ *
12
+ * The store also keeps a Set of "read" ids in memory and writes them
13
+ * to ~/.config/bizar/notifications.read.json so the read state
14
+ * survives restarts.
15
+ *
16
+ * Reading is "append-only" but the read state is destructive. That's
17
+ * the standard pattern for this kind of activity log.
18
+ */
19
+ import {
20
+ existsSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ renameSync,
24
+ appendFileSync,
25
+ mkdirSync,
26
+ statSync,
27
+ } from 'node:fs';
28
+ import { join, dirname } from 'node:path';
29
+ import { homedir } from 'node:os';
30
+ import { randomBytes } from 'node:crypto';
31
+
32
+ const HOME = homedir();
33
+ const BIZAR_HOME = join(HOME, '.config', 'bizar');
34
+ const LOG_FILE = join(BIZAR_HOME, 'notifications.jsonl');
35
+ const READ_FILE = join(BIZAR_HOME, 'notifications.read.json');
36
+
37
+ const VALID_SEVERITY = ['info', 'success', 'warning', 'error'];
38
+ const MAX_KEPT = 1000;
39
+
40
+ function atomicWriteText(filePath, text) {
41
+ const tmp = `${filePath}.tmp.${process.pid}`;
42
+ writeFileSync(tmp, text, 'utf8');
43
+ renameSync(tmp, filePath);
44
+ }
45
+
46
+ function atomicWriteJson(filePath, data) {
47
+ atomicWriteText(filePath, JSON.stringify(data, null, 2) + '\n');
48
+ }
49
+
50
+ function genId() {
51
+ return `n_${Date.now().toString(36)}${randomBytes(3).toString('hex')}`;
52
+ }
53
+
54
+ function ensureLogFile() {
55
+ mkdirSync(dirname(LOG_FILE), { recursive: true });
56
+ if (!existsSync(LOG_FILE)) {
57
+ writeFileSync(LOG_FILE, '', 'utf8');
58
+ }
59
+ }
60
+
61
+ function safeParse(line) {
62
+ try {
63
+ return JSON.parse(line);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function loadReadSet() {
70
+ try {
71
+ if (!existsSync(READ_FILE)) return new Set();
72
+ const raw = JSON.parse(readFileSync(READ_FILE, 'utf8'));
73
+ if (Array.isArray(raw)) return new Set(raw);
74
+ return new Set();
75
+ } catch {
76
+ return new Set();
77
+ }
78
+ }
79
+
80
+ function saveReadSet(set) {
81
+ try {
82
+ mkdirSync(dirname(READ_FILE), { recursive: true });
83
+ atomicWriteJson(READ_FILE, Array.from(set));
84
+ } catch {
85
+ /* best-effort */
86
+ }
87
+ }
88
+
89
+ let _readSet = null;
90
+ function getReadSet() {
91
+ if (_readSet === null) _readSet = loadReadSet();
92
+ return _readSet;
93
+ }
94
+
95
+ export const notificationsStore = {
96
+ LOG_FILE,
97
+ READ_FILE,
98
+
99
+ /**
100
+ * Append a new notification. Returns the record. If `broadcast` is
101
+ * provided the new event is also pushed to all WS clients.
102
+ */
103
+ add(input, { broadcast } = {}) {
104
+ if (!input || typeof input !== 'object') {
105
+ throw new Error('notification input required');
106
+ }
107
+ const severity = VALID_SEVERITY.includes(input.severity) ? input.severity : 'info';
108
+ const record = {
109
+ id: input.id || genId(),
110
+ ts: input.ts || new Date().toISOString(),
111
+ severity,
112
+ message: (input.message || '').toString().slice(0, 1000),
113
+ source: (input.source || 'system').toString().slice(0, 100),
114
+ title: (input.title || '').toString().slice(0, 200),
115
+ link: input.link || null,
116
+ read: false,
117
+ meta: input.meta && typeof input.meta === 'object' ? input.meta : null,
118
+ };
119
+ try {
120
+ ensureLogFile();
121
+ appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
122
+ // Rotate when the log gets big.
123
+ try {
124
+ const st = statSync(LOG_FILE);
125
+ if (st.size > 2 * 1024 * 1024) {
126
+ const lines = readFileSync(LOG_FILE, 'utf8').split(/\r?\n/);
127
+ const kept = lines.slice(-MAX_KEPT).join('\n') + '\n';
128
+ atomicWriteText(LOG_FILE, kept);
129
+ }
130
+ } catch {
131
+ /* best-effort */
132
+ }
133
+ if (typeof broadcast === 'function') {
134
+ broadcast({ type: 'notification:new', notification: record });
135
+ }
136
+ } catch (err) {
137
+ console.error('[notifications] append failed:', err.message);
138
+ }
139
+ return record;
140
+ },
141
+
142
+ /**
143
+ * List notifications, newest first. Supports an optional `unread` flag
144
+ * to filter to unread only, and an optional `limit`.
145
+ */
146
+ list({ unread = false, limit = 200 } = {}) {
147
+ try {
148
+ ensureLogFile();
149
+ const text = readFileSync(LOG_FILE, 'utf8');
150
+ if (!text.trim()) return [];
151
+ const lines = text.split(/\r?\n/).filter(Boolean);
152
+ const readSet = getReadSet();
153
+ const out = [];
154
+ for (let i = lines.length - 1; i >= 0 && out.length < Math.max(1, limit); i--) {
155
+ const p = safeParse(lines[i]);
156
+ if (!p) continue;
157
+ const isRead = readSet.has(p.id);
158
+ if (unread && isRead) continue;
159
+ out.push({ ...p, read: isRead });
160
+ }
161
+ return out;
162
+ } catch {
163
+ return [];
164
+ }
165
+ },
166
+
167
+ /** Stats: total, unread, latest ts, severity counts. */
168
+ stats() {
169
+ try {
170
+ ensureLogFile();
171
+ const text = readFileSync(LOG_FILE, 'utf8');
172
+ const lines = text.split(/\r?\n/).filter(Boolean);
173
+ const readSet = getReadSet();
174
+ const counts = { info: 0, success: 0, warning: 0, error: 0 };
175
+ let unread = 0;
176
+ let lastTs = null;
177
+ for (const line of lines) {
178
+ const p = safeParse(line);
179
+ if (!p) continue;
180
+ counts[p.severity] = (counts[p.severity] || 0) + 1;
181
+ if (!readSet.has(p.id)) unread += 1;
182
+ if (!lastTs || p.ts > lastTs) lastTs = p.ts;
183
+ }
184
+ return {
185
+ total: lines.length,
186
+ unread,
187
+ lastTs,
188
+ counts,
189
+ };
190
+ } catch {
191
+ return { total: 0, unread: 0, lastTs: null, counts: {} };
192
+ }
193
+ },
194
+
195
+ /** Mark a single id as read. */
196
+ markRead(id) {
197
+ if (!id) return false;
198
+ const set = getReadSet();
199
+ set.add(id);
200
+ saveReadSet(set);
201
+ return true;
202
+ },
203
+
204
+ /** Mark every notification as read. */
205
+ markAllRead() {
206
+ try {
207
+ ensureLogFile();
208
+ const text = readFileSync(LOG_FILE, 'utf8');
209
+ const lines = text.split(/\r?\n/).filter(Boolean);
210
+ const set = getReadSet();
211
+ for (const line of lines) {
212
+ const p = safeParse(line);
213
+ if (p && p.id) set.add(p.id);
214
+ }
215
+ saveReadSet(set);
216
+ return set.size;
217
+ } catch {
218
+ return 0;
219
+ }
220
+ },
221
+
222
+ /** Remove a notification from the log (e.g. user dismisses). */
223
+ remove(id) {
224
+ if (!id) return false;
225
+ try {
226
+ ensureLogFile();
227
+ const text = readFileSync(LOG_FILE, 'utf8');
228
+ const lines = text.split(/\r?\n/).filter(Boolean);
229
+ const kept = lines.filter((l) => {
230
+ const p = safeParse(l);
231
+ return p && p.id !== id;
232
+ });
233
+ if (kept.length === lines.length) return false;
234
+ atomicWriteText(LOG_FILE, kept.join('\n') + (kept.length ? '\n' : ''));
235
+ const set = getReadSet();
236
+ set.delete(id);
237
+ saveReadSet(set);
238
+ return true;
239
+ } catch {
240
+ return false;
241
+ }
242
+ },
243
+ };
@@ -0,0 +1,318 @@
1
+ /**
2
+ * src/server/obsidian-store.mjs
3
+ *
4
+ * Per-project Obsidian vault for long-term agent memory. Each project
5
+ * gets an `.obsidian/` directory inside its worktree root. Notes are
6
+ * plain Markdown files git-tracked so the vault survives across clones
7
+ * and machines. Obsidian itself can open the directory as a vault;
8
+ * the agent sees the same files via the API.
9
+ *
10
+ * Conventions
11
+ * ───────────
12
+ * .obsidian/
13
+ * ├── vault.json vault metadata (Obsidian format)
14
+ * ├── README.md project-level overview (rendered in dashboard)
15
+ * ├── INDEX.md auto-generated index of all notes
16
+ * ├── daily/ daily journal entries
17
+ * │ └── YYYY-MM-DD.md
18
+ * ├── decisions/ architecture / design decisions
19
+ * ├── patterns/ code patterns learned
20
+ * ├── api/ API contracts discovered
21
+ * └── tasks/ per-task learnings
22
+ *
23
+ * Files are markdown with YAML frontmatter (id, title, tags, created, updated).
24
+ * The dashboard exposes CRUD + search via REST.
25
+ *
26
+ * Why Obsidian and not just SQLite?
27
+ * ──────────────────────────────────
28
+ * 1. Git-trackable, portable, human-readable
29
+ * 2. Renders in Obsidian.app for the user to browse
30
+ * 3. Plugins (graph view, backlinks, dataview) work out of the box
31
+ * 4. Plain markdown so any tool (cat, grep, IDE) can read it
32
+ * 5. Backlinks emerge naturally as agents cross-link notes
33
+ */
34
+ import {
35
+ existsSync,
36
+ readFileSync,
37
+ writeFileSync,
38
+ readdirSync,
39
+ mkdirSync,
40
+ unlinkSync,
41
+ statSync,
42
+ rmSync,
43
+ } from 'node:fs';
44
+ import { join, dirname, relative, resolve } from 'node:path';
45
+
46
+ /**
47
+ * @param {string} projectRoot — absolute path to the project's working dir
48
+ * @returns {string} absolute path to the project's `.obsidian/` dir
49
+ */
50
+ export function obsidianVaultDir(projectRoot) {
51
+ return join(projectRoot, '.obsidian');
52
+ }
53
+
54
+ /** Standard subdirectories inside every Obsidian vault. */
55
+ const STANDARD_SUBDIRS = ['daily', 'decisions', 'patterns', 'api', 'tasks'];
56
+
57
+ /**
58
+ * Initialize the vault for a project. Creates `.obsidian/`, the standard
59
+ * subdirectories, a README.md, and a vault.json. Idempotent.
60
+ *
61
+ * @returns {{ vaultDir: string, created: string[] }}
62
+ */
63
+ export function initObsidianVault(projectRoot) {
64
+ const vaultDir = obsidianVaultDir(projectRoot);
65
+ const created = [];
66
+ if (!existsSync(vaultDir)) {
67
+ mkdirSync(vaultDir, { recursive: true });
68
+ created.push('.obsidian/');
69
+ }
70
+ for (const sub of STANDARD_SUBDIRS) {
71
+ const subPath = join(vaultDir, sub);
72
+ if (!existsSync(subPath)) {
73
+ mkdirSync(subPath, { recursive: true });
74
+ created.push(`.obsidian/${sub}/`);
75
+ }
76
+ }
77
+ const readmePath = join(vaultDir, 'README.md');
78
+ if (!existsSync(readmePath)) {
79
+ writeFileSync(
80
+ readmePath,
81
+ `# ${projectRoot.split('/').pop() || 'Project'} — Obsidian Vault\n\n` +
82
+ `Long-term memory for this project's AI agents.\n\n` +
83
+ `## Structure\n\n` +
84
+ `- \`daily/\` — daily journal entries (one file per day)\n` +
85
+ `- \`decisions/\` — architecture / design decisions\n` +
86
+ `- \`patterns/\` — code patterns learned\n` +
87
+ `- \`api/\` — API contracts discovered\n` +
88
+ `- \`tasks/\` — per-task learnings\n\n` +
89
+ `All notes are plain markdown with YAML frontmatter.\n` +
90
+ `Agents maintain this vault automatically.\n`,
91
+ 'utf8',
92
+ );
93
+ created.push('.obsidian/README.md');
94
+ }
95
+ const vaultJsonPath = join(vaultDir, 'vault.json');
96
+ if (!existsSync(vaultJsonPath)) {
97
+ writeFileSync(
98
+ vaultJsonPath,
99
+ JSON.stringify(
100
+ {
101
+ name: projectRoot.split('/').pop() || 'Project',
102
+ version: '1.0.0',
103
+ format: 'obsidian-vault',
104
+ createdAt: new Date().toISOString(),
105
+ managedBy: 'bizar-dash',
106
+ },
107
+ null,
108
+ 2,
109
+ ),
110
+ 'utf8',
111
+ );
112
+ created.push('.obsidian/vault.json');
113
+ }
114
+ return { vaultDir, created };
115
+ }
116
+
117
+ /** Parse a markdown file's YAML frontmatter. Returns { frontmatter, body }. */
118
+ function parseFrontmatter(raw) {
119
+ if (!raw.startsWith('---')) return { frontmatter: {}, body: raw };
120
+ const end = raw.indexOf('\n---', 3);
121
+ if (end === -1) return { frontmatter: {}, body: raw };
122
+ const fmBlock = raw.slice(3, end).trim();
123
+ const body = raw.slice(end + 4).replace(/^\n/, '');
124
+ const frontmatter = {};
125
+ for (const line of fmBlock.split('\n')) {
126
+ const m = line.match(/^([\w-]+):\s*(.*)$/);
127
+ if (m) frontmatter[m[1]] = m[2].replace(/^["']|["']$/g, '');
128
+ }
129
+ return { frontmatter, body };
130
+ }
131
+
132
+ function serializeFrontmatter(fm) {
133
+ const lines = ['---'];
134
+ for (const [k, v] of Object.entries(fm)) {
135
+ if (v === undefined || v === null) continue;
136
+ lines.push(`${k}: ${typeof v === 'string' && v.includes(':') ? `"${v}"` : v}`);
137
+ }
138
+ lines.push('---', '');
139
+ return lines.join('\n');
140
+ }
141
+
142
+ /**
143
+ * Recursively walk the vault directory and return all .md files.
144
+ * @returns {Array<{ path: string, relPath: string, mtime: number, size: number }>}
145
+ */
146
+ export function listVaultNotes(projectRoot) {
147
+ const vaultDir = obsidianVaultDir(projectRoot);
148
+ if (!existsSync(vaultDir)) return [];
149
+ const out = [];
150
+ function walk(dir, prefix) {
151
+ let entries;
152
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
153
+ catch { return; }
154
+ for (const e of entries) {
155
+ const full = join(dir, e.name);
156
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
157
+ if (e.isDirectory()) {
158
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
159
+ walk(full, rel);
160
+ } else if (e.name.endsWith('.md')) {
161
+ try {
162
+ const st = statSync(full);
163
+ out.push({ path: full, relPath: rel, mtime: st.mtimeMs, size: st.size });
164
+ } catch { /* ignore */ }
165
+ }
166
+ }
167
+ }
168
+ walk(vaultDir, '');
169
+ return out.sort((a, b) => b.mtime - a.mtime);
170
+ }
171
+
172
+ /**
173
+ * Read a single note by its path relative to the vault root.
174
+ * Returns { relPath, frontmatter, body, raw, mtime, size } or null.
175
+ */
176
+ export function readVaultNote(projectRoot, relPath) {
177
+ const vaultDir = obsidianVaultDir(projectRoot);
178
+ const filePath = resolveSafe(vaultDir, relPath);
179
+ if (!filePath || !existsSync(filePath)) return null;
180
+ const raw = readFileSync(filePath, 'utf8');
181
+ const st = statSync(filePath);
182
+ const { frontmatter, body } = parseFrontmatter(raw);
183
+ return {
184
+ relPath,
185
+ frontmatter,
186
+ body,
187
+ raw,
188
+ mtime: st.mtimeMs,
189
+ size: st.size,
190
+ };
191
+ }
192
+
193
+ /** Path safety: never let a note path escape the vault dir. */
194
+ function resolveSafe(vaultDir, relPath) {
195
+ if (!relPath || relPath.includes('\0')) return null;
196
+ const abs = resolve(vaultDir, relPath);
197
+ const rel = relative(vaultDir, abs);
198
+ if (rel.startsWith('..') || abs !== resolve(abs)) return null;
199
+ return abs;
200
+ }
201
+
202
+ /**
203
+ * Create or update a note. `relPath` is relative to the vault root,
204
+ * e.g. "daily/2026-06-25.md" or "decisions/use-svelte.md".
205
+ * `frontmatter` is a key/value map; `body` is the markdown content.
206
+ */
207
+ export function writeVaultNote(projectRoot, relPath, { frontmatter = {}, body = '' } = {}) {
208
+ const vaultDir = obsidianVaultDir(projectRoot);
209
+ if (!existsSync(vaultDir)) initObsidianVault(projectRoot);
210
+ const filePath = resolveSafe(vaultDir, relPath);
211
+ if (!filePath) throw new Error(`Invalid note path: ${relPath}`);
212
+ mkdirSync(dirname(filePath), { recursive: true });
213
+ const fm = {
214
+ id: relPath.replace(/\.md$/, '').replace(/[\\/]/g, '-'),
215
+ title: frontmatter.title || relPath.replace(/\.md$/, '').split('/').pop(),
216
+ tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.join(', ') : (frontmatter.tags || ''),
217
+ created: frontmatter.created || new Date().toISOString(),
218
+ updated: new Date().toISOString(),
219
+ ...frontmatter,
220
+ };
221
+ delete fm.id; delete fm.title; delete fm.tags; delete fm.created; delete fm.updated;
222
+ // Restore the canonical fields explicitly
223
+ const finalFm = {
224
+ id: relPath.replace(/\.md$/, '').replace(/[\\/]/g, '-'),
225
+ title: frontmatter.title || relPath.replace(/\.md$/, '').split('/').pop(),
226
+ tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.join(', ') : (frontmatter.tags || ''),
227
+ created: frontmatter.created || new Date().toISOString(),
228
+ updated: new Date().toISOString(),
229
+ ...frontmatter,
230
+ };
231
+ const content = serializeFrontmatter(finalFm) + '\n' + body;
232
+ writeFileSync(filePath, content, 'utf8');
233
+ return readVaultNote(projectRoot, relPath);
234
+ }
235
+
236
+ /** Delete a note by relative path. */
237
+ export function deleteVaultNote(projectRoot, relPath) {
238
+ const filePath = resolveSafe(obsidianVaultDir(projectRoot), relPath);
239
+ if (!filePath || !existsSync(filePath)) return false;
240
+ unlinkSync(filePath);
241
+ return true;
242
+ }
243
+
244
+ /** Recursive search across all notes. Returns matching {relPath, snippet, score}. */
245
+ export function searchVault(projectRoot, query, { limit = 25 } = {}) {
246
+ if (!query || typeof query !== 'string') return [];
247
+ const q = query.toLowerCase();
248
+ const tokens = q.split(/\s+/).filter((t) => t.length >= 2);
249
+ if (tokens.length === 0) return [];
250
+ const results = [];
251
+ for (const note of listVaultNotes(projectRoot)) {
252
+ let raw;
253
+ try { raw = readFileSync(note.path, 'utf8'); } catch { continue; }
254
+ const lower = raw.toLowerCase();
255
+ let score = 0;
256
+ for (const t of tokens) {
257
+ if (lower.includes(t)) score += (lower.split(t).length - 1);
258
+ }
259
+ if (score === 0) continue;
260
+ // Snippet around first match
261
+ const idx = lower.indexOf(tokens[0]);
262
+ const start = Math.max(0, idx - 60);
263
+ const end = Math.min(raw.length, idx + 160);
264
+ const snippet = (start > 0 ? '…' : '') + raw.slice(start, end).replace(/\s+/g, ' ').trim();
265
+ results.push({ relPath: note.relPath, snippet, score, mtime: note.mtime });
266
+ if (results.length >= limit * 4) break;
267
+ }
268
+ return results.sort((a, b) => b.score - a.score).slice(0, limit);
269
+ }
270
+
271
+ /** Rebuild the auto-generated INDEX.md listing all notes grouped by folder. */
272
+ export function rebuildIndex(projectRoot) {
273
+ const vaultDir = obsidianVaultDir(projectRoot);
274
+ if (!existsSync(vaultDir)) return null;
275
+ const notes = listVaultNotes(projectRoot);
276
+ const grouped = {};
277
+ for (const n of notes) {
278
+ const folder = n.relPath.includes('/') ? n.relPath.split('/')[0] : '_root';
279
+ if (!grouped[folder]) grouped[folder] = [];
280
+ grouped[folder].push(n);
281
+ }
282
+ const lines = ['---', 'title: Index', `updated: ${new Date().toISOString()}`, '---', '',
283
+ '# Index', '', 'Auto-generated by the Obsidian vault integration.', ''];
284
+ for (const folder of Object.keys(grouped).sort()) {
285
+ lines.push(`## ${folder}`, '');
286
+ for (const n of grouped[folder].slice(0, 50)) {
287
+ lines.push(`- [[${n.relPath.replace(/\.md$/, '')}]]`);
288
+ }
289
+ if (grouped[folder].length > 50) {
290
+ lines.push(`- _…and ${grouped[folder].length - 50} more_`);
291
+ }
292
+ lines.push('');
293
+ }
294
+ const indexPath = join(vaultDir, 'INDEX.md');
295
+ writeFileSync(indexPath, lines.join('\n'), 'utf8');
296
+ return indexPath;
297
+ }
298
+
299
+ /** Vault summary stats. */
300
+ export function vaultStats(projectRoot) {
301
+ const vaultDir = obsidianVaultDir(projectRoot);
302
+ const exists = existsSync(vaultDir);
303
+ if (!exists) {
304
+ return { exists: false, vaultDir, noteCount: 0, totalSize: 0 };
305
+ }
306
+ const notes = listVaultNotes(projectRoot);
307
+ const totalSize = notes.reduce((acc, n) => acc + n.size, 0);
308
+ const folders = new Set(notes.map((n) => n.relPath.split('/')[0]));
309
+ return {
310
+ exists: true,
311
+ vaultDir,
312
+ noteCount: notes.length,
313
+ totalSize,
314
+ folderCount: folders.size,
315
+ folders: [...folders].sort(),
316
+ lastModified: notes[0]?.mtime || null,
317
+ };
318
+ }