@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,485 @@
1
+ /**
2
+ * src/server/routes/chat.mjs
3
+ *
4
+ * /api/chat — get chat history (per session)
5
+ * /api/chat (POST) — send a message; dispatches to opencode plugin
6
+ * /api/chat/sessions — list sessions for the active project
7
+ * /api/chat/sessions (POST) — create a new session
8
+ * /api/chat/regenerate (POST) — re-dispatch the last user message
9
+ *
10
+ * The POST /api/chat handler is the most complex endpoint in the
11
+ * codebase: it persists the user message, mints an opencode session
12
+ * if needed, posts the prompt, polls up to 90s for an assistant
13
+ * reply, and persists + broadcasts the result. On plugin offline,
14
+ * it falls back to a "queued" 202 response.
15
+ */
16
+ import { Router } from 'express';
17
+ import {
18
+ existsSync,
19
+ mkdirSync,
20
+ readFileSync,
21
+ writeFileSync,
22
+ appendFileSync,
23
+ readdirSync,
24
+ statSync,
25
+ } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { projectsStore } from '../projects-store.mjs';
28
+ import {
29
+ readServeInfo,
30
+ createOpencodeSession,
31
+ sendOpencodePrompt,
32
+ listOpencodeMessages,
33
+ extractContentFromOpencodeMessage,
34
+ } from '../serve-info.mjs';
35
+ import { wrap } from './_shared.mjs';
36
+
37
+ const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,120}$/;
38
+
39
+ /**
40
+ * @param {object} deps
41
+ * @param {object} deps.state
42
+ * @param {Function} deps.broadcast
43
+ * @returns {import('express').Router}
44
+ */
45
+ export function createChatRouter({ state, broadcast }) {
46
+ const router = Router();
47
+
48
+ router.get('/chat', wrap(async (req, res) => {
49
+ const sessionId = req.query.session ? String(req.query.session) : null;
50
+ const requestedLimit = req.query.limit ? Number(req.query.limit) : 200;
51
+ const limit = Math.min(500, Math.max(1, Number.isFinite(requestedLimit) ? requestedLimit : 200));
52
+ res.json(state.getChat({ sessionId, limit }));
53
+ }));
54
+
55
+ // v3.5.5 — POST /api/chat now actually invokes the agent.
56
+ //
57
+ // Flow:
58
+ // 1. Persist the user message to the per-project .jsonl log (same
59
+ // as before — keeps the chat history intact even when the plugin
60
+ // is offline).
61
+ // 2. Resolve a chat session id (use the body-provided one, or mint
62
+ // a new `sess_*`).
63
+ // 3. Look up (or create) the opencode session id that backs this
64
+ // chat session — stored in a sidecar file at
65
+ // `<sessions>/<chatSessionId>.opencode.json`.
66
+ // 4. POST the prompt to the opencode session via the plugin's
67
+ // opencode serve child.
68
+ // 5. Poll for the assistant response (up to 90s) and persist it.
69
+ // 6. Broadcast both messages on the WS `chat:message` channel.
70
+ //
71
+ // Failure modes:
72
+ // - No active project: we still broadcast the message but skip
73
+ // persistence and return 202 (legacy behavior).
74
+ // - No serve-info: we fall back to the legacy "queued" path —
75
+ // the user message is persisted and broadcast but no agent
76
+ // invocation happens. The client can poll GET /api/chat for an
77
+ // eventual reply if the plugin comes up.
78
+ // - createSession / sendPrompt failure: 502 with the underlying
79
+ // error, the user message is still persisted.
80
+ // - Polling timeout: 202 with `timeout: true` so the client knows
81
+ // the agent is still working and can poll again.
82
+ router.post('/chat', wrap(async (req, res) => {
83
+ const body = req.body || {};
84
+ const message = typeof body.message === 'string' ? body.message.trim() : '';
85
+ if (!message) {
86
+ res.status(400).json({ error: 'bad_request', message: 'message is required' });
87
+ return;
88
+ }
89
+ const active = projectsStore.active();
90
+
91
+ // 1. Persist the user message to the per-project .jsonl log.
92
+ // (No-op when no project is active — the legacy fallback path
93
+ // only broadcast.)
94
+ let chatSessionId = null;
95
+ let file = null;
96
+ let record = null;
97
+ if (active) {
98
+ const dir = projectsStore.ensureProjectDir(active.id);
99
+ const sessionsDir = join(dir, 'sessions');
100
+ mkdirSync(sessionsDir, { recursive: true });
101
+ const requestedSessionId = typeof body.session === 'string' ? body.session.trim() : '';
102
+ chatSessionId = SESSION_ID_RE.test(requestedSessionId)
103
+ ? requestedSessionId
104
+ : `sess_${Date.now().toString(36)}`;
105
+ file = join(sessionsDir, `${chatSessionId}.jsonl`);
106
+ record = {
107
+ id: `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
108
+ ts: new Date().toISOString(),
109
+ role: 'user',
110
+ agent: body.agent || null,
111
+ model: body.model || null,
112
+ content: message,
113
+ attachments: body.attachments || [],
114
+ };
115
+ try {
116
+ appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
117
+ } catch {
118
+ // best effort
119
+ }
120
+ } else {
121
+ // No project — synthesize an id so the response shape is consistent.
122
+ const requestedSessionId = typeof body.session === 'string' ? body.session.trim() : '';
123
+ chatSessionId = SESSION_ID_RE.test(requestedSessionId)
124
+ ? requestedSessionId
125
+ : `sess_${Date.now().toString(36)}`;
126
+ record = {
127
+ id: `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
128
+ ts: new Date().toISOString(),
129
+ role: 'user',
130
+ agent: body.agent || null,
131
+ model: body.model || null,
132
+ content: message,
133
+ attachments: body.attachments || [],
134
+ };
135
+ }
136
+
137
+ state.appendActivity({
138
+ kind: 'chat.message',
139
+ agent: body.agent || null,
140
+ message: message.slice(0, 500),
141
+ });
142
+ broadcast({ type: 'chat:message', sessionId: chatSessionId, message: record });
143
+
144
+ // 2. No active project → legacy 202. Don't try to dispatch.
145
+ if (!active) {
146
+ return res.status(202).json({
147
+ accepted: true,
148
+ agent: body.agent || null,
149
+ queued: true,
150
+ reason: 'no_active_project',
151
+ });
152
+ }
153
+
154
+ // 3. No plugin running → fall back to queued. The user message is
155
+ // already persisted + broadcast; the next time the plugin comes
156
+ // up the user can re-send or POST /api/chat/regenerate.
157
+ const serveInfo = readServeInfo();
158
+ if (!serveInfo) {
159
+ return res.status(202).json({
160
+ accepted: true,
161
+ agent: body.agent || null,
162
+ queued: true,
163
+ session: chatSessionId,
164
+ reason: 'plugin_offline',
165
+ });
166
+ }
167
+
168
+ const sessionsDir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
169
+ const sidecarPath = join(sessionsDir, `${chatSessionId}.opencode.json`);
170
+
171
+ // 4. Resolve or create the opencode session that backs this chat.
172
+ let opencodeSessionId = null;
173
+ try {
174
+ if (existsSync(sidecarPath)) {
175
+ const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'));
176
+ opencodeSessionId = sidecar?.opencodeSessionId || null;
177
+ }
178
+ } catch {
179
+ opencodeSessionId = null;
180
+ }
181
+
182
+ if (!opencodeSessionId) {
183
+ const agentName = body.agent || active.defaultAgent || 'odin';
184
+ const create = await createOpencodeSession(
185
+ serveInfo,
186
+ { title: `Chat: ${agentName}`, agent: agentName },
187
+ active.path || serveInfo.worktree,
188
+ );
189
+ if (!create.ok || !create.sessionId) {
190
+ return res.status(502).json({
191
+ error: 'create_session_failed',
192
+ message: create.error || 'failed to create opencode session',
193
+ session: chatSessionId,
194
+ });
195
+ }
196
+ opencodeSessionId = create.sessionId;
197
+ try {
198
+ const sidecar = {
199
+ opencodeSessionId,
200
+ agent: agentName,
201
+ createdAt: Date.now(),
202
+ chatSessionId,
203
+ };
204
+ writeFileSync(sidecarPath, JSON.stringify(sidecar, null, 2) + '\n', 'utf8');
205
+ } catch {
206
+ // best effort
207
+ }
208
+ }
209
+
210
+ // 5. POST the prompt. We use the opencode session id as the
211
+ // messageID — the polling loop below uses it to detect the
212
+ // "before vs after" boundary.
213
+ const agentName = body.agent || active.defaultAgent || 'odin';
214
+ const send = await sendOpencodePrompt(
215
+ serveInfo,
216
+ {
217
+ sessionId: opencodeSessionId,
218
+ agent: agentName,
219
+ text: message,
220
+ messageID: record.id,
221
+ },
222
+ active.path || serveInfo.worktree,
223
+ );
224
+ if (!send.ok) {
225
+ return res.status(502).json({
226
+ error: 'send_prompt_failed',
227
+ message: send.error || 'failed to send prompt to opencode',
228
+ session: chatSessionId,
229
+ opencodeSessionId,
230
+ });
231
+ }
232
+
233
+ // 6. Poll for the assistant response. We look for the LAST
234
+ // assistant message; if its creation time is greater than the
235
+ // user's messageID timestamp, we consider it our reply. This
236
+ // is the same heuristic the plugin's own event stream uses.
237
+ const promptSentAt = Date.now();
238
+ const deadline = promptSentAt + 90_000;
239
+ let assistantRecord = null;
240
+ let lastSeenAssistantId = null;
241
+ while (Date.now() < deadline) {
242
+ await new Promise((r) => setTimeout(r, 2_000));
243
+ const list = await listOpencodeMessages(
244
+ serveInfo,
245
+ opencodeSessionId,
246
+ active.path || serveInfo.worktree,
247
+ );
248
+ if (!list.ok || !Array.isArray(list.messages) || list.messages.length === 0) {
249
+ continue;
250
+ }
251
+ // Walk newest → oldest; pick the first assistant message that
252
+ // was created at or after promptSentAt.
253
+ const assistants = list.messages
254
+ .filter((m) => (m?.info?.role || m?.role) === 'assistant')
255
+ .sort((a, b) => {
256
+ const ta = a?.info?.time?.created || 0;
257
+ const tb = b?.info?.time?.created || 0;
258
+ return tb - ta;
259
+ });
260
+ for (const m of assistants) {
261
+ const created = m?.info?.time?.created || 0;
262
+ if (created >= promptSentAt - 1_000) {
263
+ const id = m?.info?.id || '';
264
+ if (id && id === lastSeenAssistantId) continue;
265
+ lastSeenAssistantId = id;
266
+ assistantRecord = {
267
+ id: id || `asst_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
268
+ ts: new Date(created || Date.now()).toISOString(),
269
+ role: 'assistant',
270
+ agent: agentName,
271
+ content: extractContentFromOpencodeMessage(m),
272
+ opencodeSessionId,
273
+ inReplyTo: record.id,
274
+ };
275
+ break;
276
+ }
277
+ }
278
+ if (assistantRecord) break;
279
+ }
280
+
281
+ if (assistantRecord) {
282
+ // 7. Persist + broadcast the assistant message.
283
+ try {
284
+ appendFileSync(file, JSON.stringify(assistantRecord) + '\n', 'utf8');
285
+ } catch {
286
+ // best effort
287
+ }
288
+ broadcast({ type: 'chat:message', sessionId: chatSessionId, message: assistantRecord });
289
+ state.appendActivity({
290
+ kind: 'chat.response',
291
+ agent: agentName,
292
+ message: (assistantRecord.content || '').slice(0, 500),
293
+ });
294
+ return res.json({
295
+ accepted: true,
296
+ session: chatSessionId,
297
+ opencodeSessionId,
298
+ userMessage: record,
299
+ assistantMessage: assistantRecord,
300
+ });
301
+ }
302
+
303
+ // 8. Timeout — the agent is still working. The next poll of GET
304
+ // /api/chat?session=… will pick up whatever has arrived by
305
+ // then. Return 202 so the client knows nothing failed.
306
+ return res.status(202).json({
307
+ accepted: true,
308
+ session: chatSessionId,
309
+ opencodeSessionId,
310
+ userMessage: record,
311
+ queued: true,
312
+ timeout: true,
313
+ });
314
+ }));
315
+
316
+ router.get('/chat/sessions', wrap(async (_req, res) => {
317
+ const active = projectsStore.active();
318
+ if (!active) {
319
+ res.json({ sessions: [] });
320
+ return;
321
+ }
322
+ const dir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
323
+ if (!existsSync(dir)) {
324
+ res.json({ sessions: [] });
325
+ return;
326
+ }
327
+ const sessions = readdirSync(dir)
328
+ .filter((f) => f.endsWith('.jsonl'))
329
+ .map((f) => {
330
+ const st = statSync(join(dir, f));
331
+ return { id: f.replace(/\.jsonl$/, ''), file: f, mtime: st.mtimeMs, size: st.size };
332
+ });
333
+ sessions.sort((a, b) => b.mtime - a.mtime);
334
+ res.json({ sessions });
335
+ }));
336
+
337
+ // v3.0.4 — Create a new chat session. Generates an id, ensures the
338
+ // sessions dir + empty .jsonl file exist, and returns the session
339
+ // metadata. Idempotent: if the session already exists, returns it.
340
+ router.post('/chat/sessions', wrap(async (req, res) => {
341
+ const active = projectsStore.active();
342
+ if (!active) {
343
+ res.status(400).json({ error: 'no_active_project', message: 'No active project. Pick one in Overview first.' });
344
+ return;
345
+ }
346
+ const requestedId = typeof req.body?.id === 'string' ? req.body.id.trim() : '';
347
+ const sessionId = requestedId && /^[\w-]+$/.test(requestedId)
348
+ ? requestedId
349
+ : `sess_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
350
+ const dir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
351
+ mkdirSync(dir, { recursive: true });
352
+ const file = join(dir, `${sessionId}.jsonl`);
353
+ if (!existsSync(file)) {
354
+ writeFileSync(file, '', 'utf8');
355
+ }
356
+ state.appendActivity({ kind: 'chat.session.create', id: sessionId });
357
+ broadcast({ type: 'chat:session:create', sessionId });
358
+ res.status(201).json({
359
+ id: sessionId,
360
+ file: `${sessionId}.jsonl`,
361
+ mtime: Date.now(),
362
+ size: 0,
363
+ });
364
+ }));
365
+
366
+ // ── /api/chat/regenerate ─────────────────────────────────────────────
367
+ // v3.0.0: re-dispatches the last user message before messageId via POST /chat.
368
+ // Full opencode re-dispatch lands in v3.1 when the plugin exposes a stable HTTP API.
369
+ router.post('/chat/regenerate', wrap(async (req, res) => {
370
+ const { sessionId, messageId } = req.body || {};
371
+ if (!messageId) {
372
+ res.status(400).json({ error: 'bad_request', message: 'messageId is required' });
373
+ return;
374
+ }
375
+ const active = projectsStore.active();
376
+ if (!active) {
377
+ res.status(400).json({ error: 'no_active_project', message: 'no active project' });
378
+ return;
379
+ }
380
+ const dir = projectsStore.ensureProjectDir(active.id);
381
+ const sessionsDir = join(dir, 'sessions');
382
+ if (!existsSync(sessionsDir)) {
383
+ res.status(404).json({ error: 'not_found', message: 'no sessions found' });
384
+ return;
385
+ }
386
+ const allFiles = readdirSync(sessionsDir).filter((f) => f.endsWith('.jsonl'));
387
+ const targetFiles = sessionId ? allFiles.filter((f) => f === `${sessionId}.jsonl`) : allFiles;
388
+ if (!targetFiles.length) {
389
+ res.status(404).json({ error: 'not_found', message: 'session not found' });
390
+ return;
391
+ }
392
+ // Read the session file and find the last user message before messageId
393
+ const full = join(sessionsDir, targetFiles[0]);
394
+ let lastUserMessage = null;
395
+ let foundTarget = false;
396
+ try {
397
+ const lines = readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean);
398
+ for (let i = 0; i < lines.length; i++) {
399
+ try {
400
+ const msg = JSON.parse(lines[i]);
401
+ if (msg.id === messageId || (messageId && String(msg.ts) === String(messageId))) {
402
+ foundTarget = true;
403
+ for (let j = i - 1; j >= 0; j--) {
404
+ try {
405
+ const prev = JSON.parse(lines[j]);
406
+ if (prev.role === 'user') {
407
+ lastUserMessage = prev;
408
+ break;
409
+ }
410
+ } catch {
411
+ /* skip */
412
+ }
413
+ }
414
+ break;
415
+ }
416
+ } catch {
417
+ /* skip */
418
+ }
419
+ }
420
+ } catch (err) {
421
+ res.status(500).json({ error: 'read_failed', message: err.message });
422
+ return;
423
+ }
424
+ // Fallback: find last user message
425
+ if (!lastUserMessage) {
426
+ try {
427
+ const lines = readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean).reverse();
428
+ for (const line of lines) {
429
+ try {
430
+ const msg = JSON.parse(line);
431
+ if (msg.role === 'user') {
432
+ lastUserMessage = msg;
433
+ break;
434
+ }
435
+ } catch {
436
+ /* ignore */
437
+ }
438
+ }
439
+ } catch {
440
+ /* ignore */
441
+ }
442
+ }
443
+ if (!lastUserMessage) {
444
+ res.status(404).json({ error: 'not_found', message: 'no user message found to regenerate' });
445
+ return;
446
+ }
447
+ // Re-post via POST /chat (queued for agent processing)
448
+ const record = {
449
+ ts: new Date().toISOString(),
450
+ role: 'user',
451
+ agent: lastUserMessage.agent || null,
452
+ model: lastUserMessage.model || null,
453
+ content: lastUserMessage.content || lastUserMessage.message || '',
454
+ attachments: lastUserMessage.attachments || [],
455
+ };
456
+ try {
457
+ const lines = existsSync(full) ? readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean) : [];
458
+ lines.push(JSON.stringify(record));
459
+ writeFileSync(full, lines.join('\n') + '\n', 'utf8');
460
+ } catch {
461
+ /* best effort */
462
+ }
463
+ state.appendActivity({
464
+ kind: 'chat.regenerate',
465
+ agent: lastUserMessage.agent || null,
466
+ message: (lastUserMessage.content || '').slice(0, 500),
467
+ });
468
+ broadcast({ type: 'chat:regenerate', message: record });
469
+ res.status(202).json({ accepted: true, regeneratedMessage: record });
470
+ }));
471
+
472
+ // S3 — /chat/audit: thin endpoint that AuditDialog.tsx calls.
473
+ // The heavy lifting is the existing `/audit` slash command routed to
474
+ // forseti via opencode.json.template. This keeps the dialog functional
475
+ // without re-implementing audit logic.
476
+ router.post('/chat/audit', wrap(async (req, res) => {
477
+ res.json({
478
+ ok: true,
479
+ note: 'audit dispatched — see command.audit in config/opencode.json.template',
480
+ audit: { status: 'queued' },
481
+ });
482
+ }));
483
+
484
+ return router;
485
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * src/server/routes/config.mjs
3
+ *
4
+ * /api/config — read ~/.config/opencode/opencode.json
5
+ * /api/config (PUT) — write opencode.json
6
+ * /api/config/reload (POST) — re-read
7
+ * /api/config/providers — list providers
8
+ * /api/config/providers (POST) — add
9
+ * /api/config/providers/:id (PUT) — update
10
+ * /api/config/providers/:id (DELETE) — remove
11
+ * /api/config/mcps — list MCP servers
12
+ * /api/config/mcps (POST) — add
13
+ * /api/config/mcps/:id (PUT) — update
14
+ * /api/config/mcps/:id (DELETE) — remove
15
+ *
16
+ * Two stores are involved: the on-disk opencode.json (read+write)
17
+ * and the providersStore/mcpsStore in-memory registries. /api/config
18
+ * is the former; /api/config/providers and /api/config/mcps are
19
+ * the latter. /api/providers (without the config/ prefix) is the
20
+ * dashboard's aggregated provider list and lives in providers.mjs.
21
+ */
22
+ import { Router } from 'express';
23
+ import { existsSync, mkdirSync } from 'node:fs';
24
+ import { dirname } from 'node:path';
25
+ import { providersStore, mcpsStore } from '../providers-store.mjs';
26
+ import { OPENCODE_JSON, atomicWriteJson, safeReadJSON, wrap } from './_shared.mjs';
27
+
28
+ /**
29
+ * @param {object} deps
30
+ * @param {object} deps.state
31
+ * @param {object} deps.watcher
32
+ * @returns {import('express').Router}
33
+ */
34
+ export function createConfigRouter({ state, watcher }) {
35
+ const router = Router();
36
+
37
+ router.get('/config', wrap(async (_req, res) => {
38
+ const data = safeReadJSON(OPENCODE_JSON, null);
39
+ res.json({
40
+ path: OPENCODE_JSON,
41
+ data,
42
+ raw: data === null ? '' : JSON.stringify(data, null, 2),
43
+ exists: existsSync(OPENCODE_JSON),
44
+ });
45
+ }));
46
+
47
+ router.put('/config', wrap(async (req, res) => {
48
+ const body = req.body;
49
+ let parsed;
50
+ if (typeof body === 'string') {
51
+ try {
52
+ parsed = JSON.parse(body);
53
+ } catch (err) {
54
+ res.status(400).json({ error: 'invalid_json', message: err.message });
55
+ return;
56
+ }
57
+ } else if (body && typeof body === 'object') {
58
+ parsed = body;
59
+ } else {
60
+ res.status(400).json({ error: 'bad_request', message: 'body must be JSON' });
61
+ return;
62
+ }
63
+ mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
64
+ atomicWriteJson(OPENCODE_JSON, parsed);
65
+ state.appendActivity({ kind: 'config.update' });
66
+ watcher.poke('change', OPENCODE_JSON);
67
+ res.json({ path: OPENCODE_JSON, data: parsed, exists: true, raw: JSON.stringify(parsed, null, 2) });
68
+ }));
69
+
70
+ router.post('/config/reload', wrap(async (_req, res) => {
71
+ const data = safeReadJSON(OPENCODE_JSON, null);
72
+ res.json({
73
+ path: OPENCODE_JSON,
74
+ data,
75
+ raw: data === null ? '' : JSON.stringify(data, null, 2),
76
+ exists: existsSync(OPENCODE_JSON),
77
+ });
78
+ }));
79
+
80
+ // ── /api/config/providers ─────────────────────────────────────────────
81
+ router.get('/config/providers', wrap(async (_req, res) => {
82
+ res.json({ providers: providersStore.list() });
83
+ }));
84
+
85
+ router.post('/config/providers', wrap(async (req, res) => {
86
+ const provider = providersStore.add(req.body || {});
87
+ res.status(201).json(provider);
88
+ }));
89
+
90
+ router.put('/config/providers/:id', wrap(async (req, res) => {
91
+ const provider = providersStore.update(req.params.id, req.body || {});
92
+ res.json(provider);
93
+ }));
94
+
95
+ router.delete('/config/providers/:id', wrap(async (req, res) => {
96
+ const ok = providersStore.remove(req.params.id);
97
+ if (!ok) {
98
+ res.status(404).json({ error: 'not_found' });
99
+ return;
100
+ }
101
+ res.status(204).end();
102
+ }));
103
+
104
+ // ── /api/config/mcps ───────────────────────────────────────────────────
105
+ router.get('/config/mcps', wrap(async (_req, res) => {
106
+ res.json({ mcps: mcpsStore.list() });
107
+ }));
108
+
109
+ router.post('/config/mcps', wrap(async (req, res) => {
110
+ const mcp = mcpsStore.add(req.body || {});
111
+ res.status(201).json(mcp);
112
+ }));
113
+
114
+ router.put('/config/mcps/:id', wrap(async (req, res) => {
115
+ const mcp = mcpsStore.update(req.params.id, req.body || {});
116
+ res.json(mcp);
117
+ }));
118
+
119
+ router.delete('/config/mcps/:id', wrap(async (req, res) => {
120
+ const ok = mcpsStore.remove(req.params.id);
121
+ if (!ok) {
122
+ res.status(404).json({ error: 'not_found' });
123
+ return;
124
+ }
125
+ res.status(204).end();
126
+ }));
127
+
128
+ return router;
129
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * src/server/routes/diagnostics.mjs
3
+ *
4
+ * /api/diagnostics — full snapshot
5
+ * /api/diagnostics/health — health check
6
+ * /api/diagnostics/logs — tail of the active log file
7
+ */
8
+ import { Router } from 'express';
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { diagnosticsStore } from '../diagnostics-store.mjs';
12
+ import { BIZAR_HOME, wrap } from './_shared.mjs';
13
+
14
+ /**
15
+ * @returns {import('express').Router}
16
+ */
17
+ export function createDiagnosticsRouter() {
18
+ const router = Router();
19
+
20
+ router.get('/diagnostics', wrap(async (_req, res) => {
21
+ res.json(diagnosticsStore.snapshot());
22
+ }));
23
+
24
+ router.get('/diagnostics/health', wrap(async (_req, res) => {
25
+ res.json(diagnosticsStore.health());
26
+ }));
27
+
28
+ router.get('/diagnostics/logs', wrap(async (req, res) => {
29
+ const requestedTail = parseInt(String(req.query.tail || '100'), 10);
30
+ const tail = Math.min(5000, Math.max(1, Number.isFinite(requestedTail) ? requestedTail : 100));
31
+ const serviceLog = join(BIZAR_HOME, 'service.log');
32
+ const dashboardLog = join(BIZAR_HOME, 'dashboard.log');
33
+ // Prefer service.log, fall back to dashboard.log
34
+ const logFile = existsSync(serviceLog) ? serviceLog : existsSync(dashboardLog) ? dashboardLog : null;
35
+ if (!logFile) {
36
+ res.json({ lines: [], file: null, total: 0 });
37
+ return;
38
+ }
39
+ try {
40
+ const text = readFileSync(logFile, 'utf8');
41
+ const allLines = text.split(/\r?\n/).filter(Boolean);
42
+ const lines = allLines.slice(-tail);
43
+ res.json({ lines, file: logFile, total: allLines.length });
44
+ } catch (err) {
45
+ res.status(500).json({ error: 'read_failed', message: err.message });
46
+ }
47
+ }));
48
+
49
+ return router;
50
+ }