@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,359 @@
1
+ /**
2
+ * src/server/routes/mods.mjs
3
+ *
4
+ * /api/mods — list
5
+ * /api/mods/registry — fetch available mods from public registry
6
+ * /api/mods/audit — tail the mod audit log
7
+ * /api/mods/:id — metadata
8
+ * /api/mods (POST) — install from path OR registry id
9
+ * /api/mods/:id (PUT) — enable/disable
10
+ * /api/mods/:id (DELETE) — uninstall
11
+ * /api/mods/:id/files — file tree
12
+ * /api/mods/:id/mod-file/* (GET) — read single file (named route to avoid :id/* clash)
13
+ * /api/mods/:id/mod-file/* (PUT) — write single file
14
+ * /api/mods/views — mod view registry
15
+ * /api/mods/:id/mod-web/* — serve files from mod's web/ dir
16
+ * /api/mods/:id/instructions — list installed instruction files (v3.20)
17
+ * /api/mods/:id/instructions/reinstall — force-reinstall instructions (v3.20)
18
+ * /api/mods/:id/upgrade — upgrade an installed mod from the registry (v3.20.5)
19
+ * /api/mods/:id/views/* — serve a mod-shipped React view (v3.20.5)
20
+ *
21
+ * Express-ordering note: /mods/views MUST come before /mods/:id/* —
22
+ * actually only the literal /mods (without :id) takes precedence
23
+ * over /mods/:id, but /mods/views is the same shape as /mods/:id so
24
+ * we declare it before /mods/:id to be safe.
25
+ */
26
+ import { Router } from 'express';
27
+ import { existsSync, readFileSync, statSync } from 'node:fs';
28
+ import { resolve, relative, join } from 'node:path';
29
+ import { homedir } from 'node:os';
30
+ import { modsLoader } from '../mods-loader.mjs';
31
+ import { wrap } from './_shared.mjs';
32
+
33
+ /**
34
+ * Read a file at an absolute path or under $HOME. Returns null if
35
+ * the file doesn't exist or can't be read. The path argument may
36
+ * use `~` as a home-relative shortcut (resolved against homedir()).
37
+ */
38
+ function tryRead(p) {
39
+ try {
40
+ const full = p.startsWith('~/') || p === '~' ? join(homedir(), p.slice(1)) : p;
41
+ if (!existsSync(full)) return null;
42
+ return readFileSync(full, 'utf8');
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @returns {import('express').Router}
50
+ */
51
+ export function createModsRouter() {
52
+ const router = Router();
53
+
54
+ router.get('/mods', wrap(async (_req, res) => {
55
+ res.json({ mods: modsLoader.list() });
56
+ }));
57
+
58
+ // /mods/registry MUST come before /mods/:id — otherwise Express would
59
+ // match "registry" as an :id value.
60
+ router.get('/mods/registry', wrap(async (_req, res) => {
61
+ const installed = new Set(modsLoader.list().map((m) => m.id));
62
+ try {
63
+ const registry = await modsLoader.fetchRegistry();
64
+ // Annotate each entry with installed flag and (if installed) the
65
+ // local version so the dashboard can show upgrade hints.
66
+ const mods = (registry.mods || []).map((m) => {
67
+ const local = installed.has(m.id) ? modsLoader.get(m.id) : null;
68
+ return {
69
+ ...m,
70
+ installed: local !== null,
71
+ installedVersion: local ? local.version : null,
72
+ upgradeAvailable:
73
+ local && local.version !== m.latest ? m.latest : null,
74
+ };
75
+ });
76
+ res.json({
77
+ registry: {
78
+ version: registry.version,
79
+ updatedAt: registry.updatedAt,
80
+ source: modsLoader.getRegistryUrl(),
81
+ },
82
+ mods,
83
+ });
84
+ } catch (err) {
85
+ res.status(502).json({
86
+ error: 'registry_unreachable',
87
+ message: err.message,
88
+ registryUrl: modsLoader.getRegistryUrl(),
89
+ });
90
+ }
91
+ }));
92
+
93
+ // /mods/audit — tail the mod audit log. Filters by mod id if provided.
94
+ router.get('/mods/audit', wrap(async (req, res) => {
95
+ const logPath = join(homedir(), '.cache', 'bizar', 'logs', 'mod-audit.log');
96
+ const modFilter = req.query?.mod;
97
+ const limit = Math.min(parseInt(req.query?.limit || '200', 10) || 200, 2000);
98
+ if (!existsSync(logPath)) {
99
+ res.json({ entries: [], logPath, modFilter });
100
+ return;
101
+ }
102
+ let text;
103
+ try {
104
+ text = readFileSync(logPath, 'utf8');
105
+ } catch (err) {
106
+ res.status(500).json({ error: 'read_failed', message: err.message });
107
+ return;
108
+ }
109
+ const lines = text.split(/\r?\n/).filter(Boolean).reverse();
110
+ const out = [];
111
+ for (const line of lines) {
112
+ if (out.length >= limit) break;
113
+ try {
114
+ const entry = JSON.parse(line);
115
+ if (modFilter && entry.mod !== modFilter) continue;
116
+ out.push(entry);
117
+ } catch {
118
+ /* skip malformed lines */
119
+ }
120
+ }
121
+ res.json({ entries: out.reverse(), logPath, modFilter });
122
+ }));
123
+
124
+ // /mods/views MUST come before /mods/:id — otherwise Express would
125
+ // match "views" as an :id value.
126
+ router.get('/mods/views', wrap(async (_req, res) => {
127
+ const views = modsLoader.listModViews();
128
+ res.json({ views });
129
+ }));
130
+
131
+ router.get('/mods/:id', wrap(async (req, res) => {
132
+ const mod = modsLoader.get(req.params.id);
133
+ if (!mod) {
134
+ res.status(404).json({ error: 'not_found' });
135
+ return;
136
+ }
137
+ res.json(mod);
138
+ }));
139
+
140
+ router.post('/mods', wrap(async (req, res) => {
141
+ // Two install paths:
142
+ // { path: "/local/path" } — install from a local directory
143
+ // { id: "registry-id" } — install from the public registry
144
+ if (req.body?.id) {
145
+ try {
146
+ const mod = await modsLoader.installFromRegistry(req.body.id);
147
+ res.status(201).json(mod);
148
+ } catch (err) {
149
+ res.status(500).json({ error: 'install_failed', message: err.message });
150
+ }
151
+ return;
152
+ }
153
+ const path = req.body?.path;
154
+ if (!path) {
155
+ res.status(400).json({
156
+ error: 'bad_request',
157
+ message: 'either "id" (registry install) or "path" (local install) is required',
158
+ });
159
+ return;
160
+ }
161
+ try {
162
+ const mod = modsLoader.installFromPath(path);
163
+ res.status(201).json(mod);
164
+ } catch (err) {
165
+ res.status(500).json({ error: 'install_failed', message: err.message });
166
+ }
167
+ }));
168
+
169
+ router.put('/mods/:id', wrap(async (req, res) => {
170
+ const mod = modsLoader.setEnabled(req.params.id, !!req.body?.enabled);
171
+ if (!mod) {
172
+ res.status(404).json({ error: 'not_found' });
173
+ return;
174
+ }
175
+ res.json(mod);
176
+ }));
177
+
178
+ router.delete('/mods/:id', wrap(async (req, res) => {
179
+ const ok = modsLoader.uninstall(req.params.id);
180
+ if (!ok) {
181
+ res.status(404).json({ error: 'not_found' });
182
+ return;
183
+ }
184
+ res.status(204).end();
185
+ }));
186
+
187
+ router.get('/mods/:id/files', wrap(async (req, res) => {
188
+ res.json({ files: modsLoader.listFiles(req.params.id) });
189
+ }));
190
+
191
+ // NOTE: we use /mods/:id/mod-file/* (named route) to avoid conflicting
192
+ // with mod route mounting at /api/mods/:id/*. The wildcard in /* was
193
+ // too greedy and captured paths like /mods/test-mod/hello.
194
+ router.get('/mods/:id/mod-file/*', wrap(async (req, res) => {
195
+ const rel = req.params[0] || '';
196
+ const content = modsLoader.readFile(req.params.id, rel);
197
+ if (content === null) {
198
+ res.status(404).json({ error: 'not_found' });
199
+ return;
200
+ }
201
+ res.type('text/plain').send(content);
202
+ }));
203
+
204
+ router.put('/mods/:id/mod-file/*', wrap(async (req, res) => {
205
+ const rel = req.params[0] || '';
206
+ const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body, null, 2);
207
+ modsLoader.writeFile(req.params.id, rel, body);
208
+ res.json({ ok: true });
209
+ }));
210
+
211
+ // ── v3.20 — Mod instructions (agents/commands/skills installed by this mod)
212
+ router.get('/mods/:id/instructions', wrap(async (req, res) => {
213
+ const mod = modsLoader.get(req.params.id);
214
+ if (!mod) {
215
+ res.status(404).json({ error: 'not_found' });
216
+ return;
217
+ }
218
+ const installed = modsLoader.listModInstructions(req.params.id);
219
+ // For each installed file, try to read its content so the UI can
220
+ // preview it without an extra round-trip.
221
+ const annotated = {
222
+ agents: installed.agents.map((f) => ({
223
+ filename: f,
224
+ path: join('agents', f),
225
+ fullPath: `~/.config/opencode/agents/${f}`,
226
+ content: tryRead(`~/.config/opencode/agents/${f}`),
227
+ })),
228
+ commands: installed.commands.map((f) => ({
229
+ filename: f,
230
+ path: join('commands', f),
231
+ fullPath: `~/.config/opencode/commands/${f}`,
232
+ content: tryRead(`~/.config/opencode/commands/${f}`),
233
+ })),
234
+ skills: installed.skills.map((d) => ({
235
+ name: d,
236
+ fullPath: `~/.opencode/skills/${d}`,
237
+ content: tryRead(`~/.opencode/skills/${d}/SKILL.md`),
238
+ })),
239
+ };
240
+ res.json({
241
+ modId: req.params.id,
242
+ modName: mod.name,
243
+ total: annotated.agents.length + annotated.commands.length + annotated.skills.length,
244
+ ...annotated,
245
+ });
246
+ }));
247
+
248
+ // v3.20 — Force-reinstall the mod's instructions into opencode config
249
+ // (useful after editing files inside the mod's agents/commands/skills).
250
+ router.post('/mods/:id/instructions/reinstall', wrap(async (req, res) => {
251
+ const counts = modsLoader.reinstallInstructions(req.params.id);
252
+ if (counts === null) {
253
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not installed` });
254
+ return;
255
+ }
256
+ res.json({ ok: true, counts });
257
+ }));
258
+
259
+ // v3.20.5 — Upgrade an installed mod from the registry. Body opts:
260
+ // { backup?: boolean, url?: string }
261
+ // backup: if true, copy the existing mod folder into
262
+ // ~/.config/bizar/mods/.backup/<id>-<timestamp>/ before replacing.
263
+ // url: optional override for the registry URL.
264
+ // Returns { from, to, backupPath?, mod } on success.
265
+ router.post('/mods/:id/upgrade', wrap(async (req, res) => {
266
+ try {
267
+ const result = await modsLoader.upgradeFromRegistry(req.params.id, {
268
+ backup: !!req.body?.backup,
269
+ url: typeof req.body?.url === 'string' ? req.body.url : undefined,
270
+ });
271
+ res.json({
272
+ ok: true,
273
+ modId: req.params.id,
274
+ from: result.from,
275
+ to: result.to,
276
+ backupPath: result.backupPath,
277
+ mod: result.mod,
278
+ });
279
+ } catch (err) {
280
+ const code = /not installed/i.test(err.message) ? 404 : 500;
281
+ res.status(code).json({ error: 'upgrade_failed', message: err.message });
282
+ }
283
+ }));
284
+
285
+ // ── /api/mods/:id/mod-web/* and /api/mods/:id/web/* ────────────────
286
+ // Serve files from each mod's web/ directory (for iframe embedding).
287
+ // Two aliases — `mod-web` (explicit, won't collide with mod routes) and
288
+ // bare `web` (intuitive, what users actually type). They serve identical
289
+ // content so the mod author can use either.
290
+ const webHandler = wrap(async (req, res) => {
291
+ const mod = modsLoader.get(req.params.id);
292
+ if (!mod || !mod.enabled) {
293
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
294
+ return;
295
+ }
296
+ const rel = req.params[0] || '';
297
+ const webRoot = resolve(mod.path, 'web');
298
+ const filePath = resolve(webRoot, rel);
299
+ const relPath = relative(webRoot, filePath);
300
+ if (relPath.startsWith('..') || relPath === '') {
301
+ res.status(403).json({ error: 'forbidden' });
302
+ return;
303
+ }
304
+ if (!existsSync(filePath)) {
305
+ res.status(404).json({ error: 'not_found', message: `file not found: ${rel}` });
306
+ return;
307
+ }
308
+ res.sendFile(filePath);
309
+ });
310
+ router.get('/mods/:id/mod-web/*', webHandler);
311
+ router.get('/mods/:id/web/*', webHandler);
312
+
313
+ // v3.20.5 — Serve a mod-shipped React view as an ES module. Mods
314
+ // declare view components in views/registry.json (e.g.
315
+ // { id: "graph", component: "GraphView.js" }
316
+ // ) and ship the built file at views/<component>. The dashboard
317
+ // dynamically imports it on demand and renders it inside ModView.
318
+ //
319
+ // The file is served with Content-Type: application/javascript so
320
+ // the browser treats it as a real ES module. Mods are expected to
321
+ // ship pre-built code (no JSX) and use React.createElement (or htm
322
+ // with React.createElement) for composition. React itself is loaded
323
+ // as part of the dashboard bundle — see /assets/*.js on first load.
324
+ const viewsHandler = wrap(async (req, res) => {
325
+ const mod = modsLoader.get(req.params.id);
326
+ if (!mod || !mod.enabled) {
327
+ res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
328
+ return;
329
+ }
330
+ const rel = req.params[0] || '';
331
+ if (!rel || rel.endsWith('/')) {
332
+ res.status(400).json({ error: 'bad_request', message: 'file path required' });
333
+ return;
334
+ }
335
+ // Allow only .js / .mjs / .jsx / .tsx files — anything else is
336
+ // a misconfiguration or attack attempt.
337
+ if (!/\.(js|mjs|jsx|tsx)$/i.test(rel)) {
338
+ res.status(403).json({ error: 'forbidden', message: 'only JS-family files may be served as views' });
339
+ return;
340
+ }
341
+ const viewsRoot = resolve(mod.path, 'views');
342
+ const filePath = resolve(viewsRoot, rel);
343
+ const relPath = relative(viewsRoot, filePath);
344
+ if (relPath.startsWith('..') || relPath === '') {
345
+ res.status(403).json({ error: 'forbidden' });
346
+ return;
347
+ }
348
+ if (!existsSync(filePath)) {
349
+ res.status(404).json({ error: 'not_found', message: `view not found: ${rel}` });
350
+ return;
351
+ }
352
+ res.set('Content-Type', 'application/javascript; charset=utf-8');
353
+ res.set('Cache-Control', 'no-store');
354
+ res.sendFile(filePath);
355
+ });
356
+ router.get('/mods/:id/views/*', viewsHandler);
357
+
358
+ return router;
359
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * src/server/routes/notifications.mjs
3
+ *
4
+ * /api/notifications — list (?unread=true to filter)
5
+ * /api/notifications/:id/read (POST) — mark one as read
6
+ * /api/notifications/read-all (POST) — mark every as read
7
+ * /api/notifications/:id (DELETE) — dismiss
8
+ */
9
+ import { Router } from 'express';
10
+ import { notificationsStore } from '../notifications-store.mjs';
11
+ import { wrap } from './_shared.mjs';
12
+
13
+ /**
14
+ * @param {object} deps
15
+ * @param {Function} deps.broadcast
16
+ * @returns {import('express').Router}
17
+ */
18
+ export function createNotificationsRouter({ broadcast }) {
19
+ const router = Router();
20
+
21
+ // v3.3.0 — Per-user notification stream. Backed by an append-only
22
+ // JSONL log at ~/.config/bizar/notifications.jsonl. Read state is
23
+ // persisted separately at notifications.read.json.
24
+ router.get('/notifications', wrap(async (req, res) => {
25
+ const unread = req.query.unread === 'true' || req.query.unread === '1';
26
+ const limit = Math.min(500, Math.max(1, parseInt(req.query.limit || '200', 10) || 200));
27
+ const items = notificationsStore.list({ unread, limit });
28
+ res.json({ notifications: items, stats: notificationsStore.stats() });
29
+ }));
30
+
31
+ router.post('/notifications/:id/read', wrap(async (req, res) => {
32
+ const ok = notificationsStore.markRead(req.params.id);
33
+ if (ok) broadcast({ type: 'notifications:change' });
34
+ res.json({ ok });
35
+ }));
36
+
37
+ router.post('/notifications/read-all', wrap(async (_req, res) => {
38
+ const marked = notificationsStore.markAllRead();
39
+ broadcast({ type: 'notifications:change' });
40
+ res.json({ ok: true, marked });
41
+ }));
42
+
43
+ router.delete('/notifications/:id', wrap(async (req, res) => {
44
+ const ok = notificationsStore.remove(req.params.id);
45
+ if (!ok) {
46
+ res.status(404).json({ error: 'not_found' });
47
+ return;
48
+ }
49
+ broadcast({ type: 'notifications:change' });
50
+ res.status(204).end();
51
+ }));
52
+
53
+ return router;
54
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * src/server/routes/obsidian.mjs
3
+ *
4
+ * v2 — Backward-compatible rewrite. When `.bizar/memory.json` exists, this
5
+ * router delegates to memory-store.mjs. When memory is not configured, it falls
6
+ * back to the original obsidian-store.mjs behaviour.
7
+ *
8
+ * CRITICAL: Preserve legacy response shapes exactly. Do not change the JSON
9
+ * structure returned by any endpoint.
10
+ */
11
+
12
+ import { Router } from 'express';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
17
+
18
+ // Lazy imports so circular deps are avoided and we only load memory-store.mjs
19
+ // when it is actually needed.
20
+ let _memoryStore = null;
21
+ let _obsidianStore = null;
22
+ let _wrap = null;
23
+
24
+ async function getMemoryStore() {
25
+ if (!_memoryStore) {
26
+ _memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) => m);
27
+ }
28
+ return _memoryStore;
29
+ }
30
+
31
+ async function getObsidianStore() {
32
+ if (!_obsidianStore) {
33
+ _obsidianStore = await import(`${SERVER_ROOT}/obsidian-store.mjs`).then((m) => m);
34
+ }
35
+ return _obsidianStore;
36
+ }
37
+
38
+ async function getShared() {
39
+ if (!_wrap) {
40
+ const mod = await import('./_shared.mjs').then((m) => m);
41
+ _wrap = mod.wrap;
42
+ }
43
+ return { wrap: _wrap };
44
+ }
45
+
46
+ /**
47
+ * Returns true if the project has opted into the memory service via
48
+ * `.bizar/memory.json`.
49
+ */
50
+ async function hasMemoryConfig(projectRoot) {
51
+ const { safeReadJSON } = await import(`${SERVER_ROOT}/yaml.mjs`).then((m) => m);
52
+ // Use safeReadJSON from yaml.mjs is expensive — use simpler check
53
+ const { existsSync } = await import('node:fs');
54
+ const configPath = join(projectRoot, '.bizar', 'memory.json');
55
+ return existsSync(configPath);
56
+ }
57
+
58
+ export async function createObsidianRouter({ projectRoot }) {
59
+ const router = Router();
60
+ const { wrap } = await getShared();
61
+ const memoryEnabled = await hasMemoryConfig(projectRoot);
62
+
63
+ if (memoryEnabled) {
64
+ // ── Memory service is active — delegate to memory-store.mjs ──────────────
65
+ const ms = await getMemoryStore();
66
+
67
+ // GET /api/obsidian — vault stats
68
+ router.get('/obsidian', wrap(async (_req, res) => {
69
+ const { vaultRoot, mode } = ms.resolveVault(projectRoot);
70
+ const { existsSync, readdirSync, statSync, readFileSync } = await import('node:fs');
71
+ if (!existsSync(vaultRoot)) {
72
+ res.json({ exists: false, vaultDir: vaultRoot, noteCount: 0, totalSize: 0 });
73
+ return;
74
+ }
75
+ const notes = ms.listNotes(projectRoot);
76
+ const totalSize = notes.reduce((acc, n) => acc + n.size, 0);
77
+ const folders = new Set(notes.map((n) => n.relPath.split('/')[0]));
78
+ res.json({
79
+ exists: true,
80
+ vaultDir: vaultRoot,
81
+ noteCount: notes.length,
82
+ totalSize,
83
+ folderCount: folders.size,
84
+ folders: [...folders].sort(),
85
+ lastModified: notes[0]?.mtime || null,
86
+ });
87
+ }));
88
+
89
+ // POST /api/obsidian — init vault
90
+ router.post('/obsidian', wrap(async (_req, res) => {
91
+ const result = ms.initVault(projectRoot);
92
+ res.json({ vaultDir: result.vaultRoot, created: result.created });
93
+ }));
94
+
95
+ // GET /api/obsidian/notes — legacy shape: { notes: [{ path, relPath, mtime, size }] }
96
+ router.get('/obsidian/notes', wrap(async (_req, res) => {
97
+ const notes = ms.listNotes(projectRoot);
98
+ // Return legacy shape
99
+ const legacyNotes = notes.map((n) => ({
100
+ path: n.relPath,
101
+ relPath: n.relPath,
102
+ mtime: n.mtime,
103
+ size: n.size,
104
+ }));
105
+ res.json({ notes: legacyNotes });
106
+ }));
107
+
108
+ // POST /api/obsidian/notes — write note
109
+ // CRITICAL: returns 201 with FULL note shape { relPath, frontmatter, body, raw, mtime, size }
110
+ router.post('/obsidian/notes', wrap(async (req, res) => {
111
+ const { path: relPath, frontmatter, body } = req.body || {};
112
+ if (!relPath || typeof relPath !== 'string') {
113
+ res.status(400).json({ error: 'bad_request', message: 'path is required' });
114
+ return;
115
+ }
116
+ if (!relPath.endsWith('.md')) {
117
+ res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
118
+ return;
119
+ }
120
+ try {
121
+ const note = ms.writeNote(projectRoot, relPath, { frontmatter: frontmatter || {}, body: body || '' });
122
+ // Return FULL note shape (legacy behavior)
123
+ res.status(201).json(note);
124
+ } catch (err) {
125
+ res.status(400).json({ error: 'bad_request', message: err.message });
126
+ }
127
+ }));
128
+
129
+ // GET /api/obsidian/notes/:path — single note
130
+ router.get('/obsidian/notes/*', wrap(async (req, res) => {
131
+ const relPath = req.params[0];
132
+ const note = ms.readNote(projectRoot, relPath);
133
+ if (!note) {
134
+ res.status(404).json({ error: 'not_found' });
135
+ return;
136
+ }
137
+ res.json(note);
138
+ }));
139
+
140
+ // DELETE /api/obsidian/notes/:path
141
+ router.delete('/obsidian/notes/*', wrap(async (req, res) => {
142
+ const relPath = req.params[0];
143
+ const ok = ms.deleteNote(projectRoot, relPath);
144
+ if (!ok) {
145
+ res.status(404).json({ error: 'not_found' });
146
+ return;
147
+ }
148
+ res.status(204).end();
149
+ }));
150
+
151
+ // GET /api/obsidian/search?q=...
152
+ router.get('/obsidian/search', wrap(async (req, res) => {
153
+ const q = req.query.q || '';
154
+ const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
155
+ const results = ms.searchVault(projectRoot, q, { limit });
156
+ res.json({ query: q, results });
157
+ }));
158
+
159
+ // POST /api/obsidian/index — rebuild index
160
+ router.post('/obsidian/index', wrap(async (_req, res) => {
161
+ // No-op in memory mode (INDEX.md is not maintained)
162
+ res.json({ ok: true, indexPath: null });
163
+ }));
164
+
165
+ } else {
166
+ // ── Legacy obsidian-store.mjs behaviour ───────────────────────────────────
167
+ const os = await getObsidianStore();
168
+
169
+ router.get('/obsidian', wrap(async (_req, res) => {
170
+ res.json(os.vaultStats(projectRoot));
171
+ }));
172
+
173
+ router.post('/obsidian', wrap(async (_req, res) => {
174
+ const result = os.initObsidianVault(projectRoot);
175
+ res.json(result);
176
+ }));
177
+
178
+ router.get('/obsidian/notes', wrap(async (_req, res) => {
179
+ const notes = os.listVaultNotes(projectRoot);
180
+ res.json({ notes });
181
+ }));
182
+
183
+ router.post('/obsidian/notes', wrap(async (req, res) => {
184
+ const { path: relPath, frontmatter, body } = req.body || {};
185
+ if (!relPath || typeof relPath !== 'string') {
186
+ res.status(400).json({ error: 'bad_request', message: 'path is required' });
187
+ return;
188
+ }
189
+ if (!relPath.endsWith('.md')) {
190
+ res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
191
+ return;
192
+ }
193
+ try {
194
+ const note = os.writeVaultNote(projectRoot, relPath, { frontmatter, body });
195
+ res.status(201).json(note);
196
+ } catch (err) {
197
+ res.status(400).json({ error: 'bad_request', message: err.message });
198
+ }
199
+ }));
200
+
201
+ router.get('/obsidian/notes/*', wrap(async (req, res) => {
202
+ const relPath = req.params[0];
203
+ const note = os.readVaultNote(projectRoot, relPath);
204
+ if (!note) {
205
+ res.status(404).json({ error: 'not_found' });
206
+ return;
207
+ }
208
+ res.json(note);
209
+ }));
210
+
211
+ router.delete('/obsidian/notes/*', wrap(async (req, res) => {
212
+ const relPath = req.params[0];
213
+ const ok = os.deleteVaultNote(projectRoot, relPath);
214
+ if (!ok) {
215
+ res.status(404).json({ error: 'not_found' });
216
+ return;
217
+ }
218
+ res.status(204).end();
219
+ }));
220
+
221
+ router.get('/obsidian/search', wrap(async (req, res) => {
222
+ const q = req.query.q;
223
+ const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
224
+ const results = os.searchVault(projectRoot, q, { limit });
225
+ res.json({ query: q, results });
226
+ }));
227
+
228
+ router.post('/obsidian/index', wrap(async (_req, res) => {
229
+ const indexPath = os.rebuildIndex(projectRoot);
230
+ if (!indexPath) {
231
+ res.status(404).json({ error: 'not_initialized' });
232
+ return;
233
+ }
234
+ res.json({ ok: true, indexPath });
235
+ }));
236
+ }
237
+
238
+ return router;
239
+ }