@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,684 @@
1
+ /**
2
+ * src/server/serve-info.mjs
3
+ *
4
+ * v3.5.4 (bug #3) — Out-of-process reader for the plugin's `serve.json`
5
+ * (see plugins/bizar/src/serve-info.ts). The plugin owns the `opencode serve`
6
+ * child process; this module lets the dashboard server talk to the same
7
+ * child so it can abort sessions, list sessions, etc.
8
+ *
9
+ * Why a separate file:
10
+ * The plugin is TypeScript, the dashboard is plain `.mjs`. We cannot share
11
+ * code directly. Mirroring the read logic in a small `.mjs` is the
12
+ * lowest-cost bridge and keeps the plugin as the single source of truth
13
+ * for the on-disk contract.
14
+ *
15
+ * Opencode HTTP API contract (verified against opencode serve 1.17.x):
16
+ * - Auth: `Authorization: Basic base64("opencode:<password>")`. opencode
17
+ * serves the realm "Secure Area" with Basic auth.
18
+ * - `POST /api/session/{sessionId}/abort?directory={worktree}`
19
+ * — Aborts the session. The directory query param scopes the call to
20
+ * a specific worktree (mirrors how the plugin's HttpClient calls
21
+ * all per-session endpoints). 2xx on success; 404 if the session
22
+ * is unknown; 401 if auth fails.
23
+ * - `GET /api/session`
24
+ * — Lists every session in the opencode database, regardless of who
25
+ * started it. Response shape: `{data: Array<SessionView>}` where
26
+ * SessionView is {id, projectID, parentID?, title, agent, model,
27
+ * time:{created,updated,archived}, location:{directory,...}, ...}.
28
+ * - `GET /api/session/{sessionId}/message?directory={worktree}`
29
+ * — Lists every message in a session. Response shape:
30
+ * `{data: Array<{info, parts}>}` where info is
31
+ * {id, role, time:{created,...}} and parts is
32
+ * `Array<{type, text}>` (text-bearing parts only contribute to
33
+ * the chat display; tool/agent parts are ignored by the chat
34
+ * layer but still in the response). Used by the chat endpoint
35
+ * to poll for assistant responses, and by the bg-poller to scan
36
+ * the final assistant message for an `html-artifact` block.
37
+ *
38
+ * Both endpoints are best-effort. Failure to call them does not crash the
39
+ * dashboard — we surface a structured `{ok:false, error}` result and let
40
+ * the UI toast.
41
+ *
42
+ * Discovery:
43
+ * We walk the same multi-path list as BG_DIRS (see background-store.mjs):
44
+ * - ~/.cache/bizar/serve.json (default stateDir)
45
+ * - ~/.config/opencode/serve.json (fallback)
46
+ * - ~/.bizar/serve.json (legacy)
47
+ * First file that parses successfully wins.
48
+ *
49
+ * Freshness:
50
+ * We re-read the file on every call so a plugin restart picks up the
51
+ * new port/password automatically. No in-process caching; the file is
52
+ * tiny (< 1 KB).
53
+ */
54
+
55
+ import { readFileSync, existsSync } from 'node:fs';
56
+ import { join } from 'node:path';
57
+ import { homedir } from 'node:os';
58
+ import { createConnection, isIP } from 'node:net';
59
+
60
+ const HOME = homedir();
61
+
62
+ // Mirrors plugins/bizar/src/serve-info.ts → BG_DIRS pattern.
63
+ const SERVE_INFO_FILES = [
64
+ join(HOME, '.cache', 'bizar', 'serve.json'),
65
+ join(HOME, '.config', 'opencode', 'serve.json'),
66
+ join(HOME, '.bizar', 'serve.json'),
67
+ ];
68
+
69
+ /**
70
+ * @typedef {Object} ServeInfo
71
+ * @property {string} baseUrl e.g. "http://127.0.0.1:4097"
72
+ * @property {number} port
73
+ * @property {string} password OPENCODE_SERVER_PASSWORD (base64 secret)
74
+ * @property {string} worktree the plugin's cwd at start time
75
+ * @property {number} pid
76
+ * @property {number} startedAt epoch ms
77
+ */
78
+
79
+ /**
80
+ * Read the serve-info file from any of the candidate paths.
81
+ * Returns null when no usable file exists. Never throws.
82
+ *
83
+ * v3.11.0 — schema relaxed to derive missing fields. The plugin's
84
+ * `writeServeInfo` is supposed to write `{baseUrl, port, password,
85
+ * worktree, pid, startedAt}`, but older builds (and the version
86
+ * currently shipping on the user's install) wrote only
87
+ * `{password, pid, port}`. The strict pre-v3.11.0 schema then caused
88
+ * `readServeInfo()` to return `null`, which cascaded into
89
+ * `dispatchToBackground` marking every subtask as `dispatchPending: true`
90
+ * (see `task-delegator.mjs:563`). We now:
91
+ * - Require only `password` (string) and `port` (number)
92
+ * - Derive `baseUrl` from `port` (`http://127.0.0.1:<port>`)
93
+ * when the on-disk file omits it
94
+ * - Treat `worktree` and `startedAt` as optional, defaulting to `''`
95
+ * and `0` respectively when missing. Callers that need a real
96
+ * worktree (the bg-retry loop) must fill it in from their own
97
+ * state — never assume the value here.
98
+ * - Still validate that the derived `baseUrl` is loopback-safe.
99
+ *
100
+ * @returns {ServeInfo|null}
101
+ */
102
+ export function readServeInfo() {
103
+ for (const file of SERVE_INFO_FILES) {
104
+ if (!existsSync(file)) continue;
105
+ try {
106
+ const raw = readFileSync(file, 'utf8');
107
+ const parsed = JSON.parse(raw);
108
+ if (
109
+ typeof parsed?.password !== 'string' ||
110
+ typeof parsed?.port !== 'number'
111
+ ) {
112
+ continue;
113
+ }
114
+ // Derive baseUrl from port when the file omits it (older plugin
115
+ // builds write `{password, pid, port}` only).
116
+ let baseUrl = typeof parsed.baseUrl === 'string' && parsed.baseUrl.length > 0
117
+ ? parsed.baseUrl
118
+ : `http://127.0.0.1:${parsed.port}`;
119
+ if (!isSafeServeBaseUrl(baseUrl)) continue;
120
+ return {
121
+ baseUrl,
122
+ port: parsed.port,
123
+ password: parsed.password,
124
+ worktree: typeof parsed.worktree === 'string' ? parsed.worktree : '',
125
+ pid: typeof parsed.pid === 'number' ? parsed.pid : 0,
126
+ startedAt: typeof parsed.startedAt === 'number' ? parsed.startedAt : 0,
127
+ };
128
+ } catch {
129
+ // try next candidate
130
+ }
131
+ }
132
+ return null;
133
+ }
134
+
135
+ function isSafeServeBaseUrl(baseUrl) {
136
+ try {
137
+ const url = new URL(baseUrl);
138
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
139
+ return isLoopbackHostname(url.hostname);
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function isLoopbackHostname(hostname) {
146
+ const host = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
147
+ if (!host) return false;
148
+ if (host === 'localhost' || host === '::1') return true;
149
+ if (host.startsWith('127.')) return true;
150
+ if (host === '::ffff:127.0.0.1' || host === '::ffff:7f00:1') return true;
151
+ const ipVersion = isIP(host);
152
+ if (ipVersion === 4) return host.startsWith('127.');
153
+ if (ipVersion === 6) return host === '::1';
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * @typedef {Object} AbortResult
159
+ * @property {boolean} ok
160
+ * @property {string} [error]
161
+ * @property {number} [status]
162
+ * @property {string} [note]
163
+ */
164
+
165
+ /**
166
+ * POST /api/session/{sessionId}/abort?directory={worktree} on the opencode
167
+ * serve child described by `info`. Best-effort; never throws.
168
+ *
169
+ * Idempotent: if the session is already gone (404), we treat it as success
170
+ * (the desired terminal state was already achieved).
171
+ *
172
+ * @param {ServeInfo} info
173
+ * @param {string} sessionId
174
+ * @param {string} [worktreeOverride] defaults to info.worktree
175
+ * @param {number} [timeoutMs] defaults to 8000
176
+ * @returns {Promise<AbortResult>}
177
+ */
178
+ export async function abortSession(info, sessionId, worktreeOverride, timeoutMs = 8000) {
179
+ if (!info || !sessionId) {
180
+ return { ok: false, error: 'missing info or sessionId' };
181
+ }
182
+ const url = `${info.baseUrl}/api/session/${encodeURIComponent(sessionId)}/abort?directory=${encodeURIComponent(
183
+ worktreeOverride || info.worktree || '',
184
+ )}`;
185
+ const auth = `Basic ${Buffer.from(`opencode:${info.password}`).toString('base64')}`;
186
+ const ac = new AbortController();
187
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
188
+ try {
189
+ const res = await fetch(url, {
190
+ method: 'POST',
191
+ headers: {
192
+ Authorization: auth,
193
+ Accept: 'application/json',
194
+ },
195
+ signal: ac.signal,
196
+ });
197
+ if (res.ok) {
198
+ return { ok: true, status: res.status };
199
+ }
200
+ // 404: session already gone — treat as idempotent success.
201
+ if (res.status === 404) {
202
+ return { ok: true, status: 404, note: 'session already absent' };
203
+ }
204
+ // Drain for diagnostics
205
+ let detail = '';
206
+ try {
207
+ detail = (await res.text()).slice(0, 500);
208
+ } catch {
209
+ /* ignore */
210
+ }
211
+ return {
212
+ ok: false,
213
+ status: res.status,
214
+ error: `abort failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
215
+ };
216
+ } catch (err) {
217
+ const msg = err instanceof Error ? err.message : String(err);
218
+ const isAbort = err instanceof Error && err.name === 'AbortError';
219
+ return {
220
+ ok: false,
221
+ error: isAbort ? `abort timed out after ${timeoutMs}ms` : `abort network error: ${msg}`,
222
+ };
223
+ } finally {
224
+ clearTimeout(timer);
225
+ }
226
+ }
227
+
228
+ /**
229
+ * @typedef {Object} OpencodeSession
230
+ * @property {string} id
231
+ * @property {string} projectID
232
+ * @property {string|null} parentID
233
+ * @property {string} title
234
+ * @property {string} [agent]
235
+ * @property {{id:string,providerID:string,variant?:string}} [model]
236
+ * @property {{created:number,updated:number,archived:number|null}} time
237
+ * @property {{directory:string,workspaceID?:string|null}} location
238
+ */
239
+
240
+ /**
241
+ * GET /api/session on the opencode serve child. Returns the raw session
242
+ * list (each entry is a SessionView from opencode). Returns null when
243
+ * no serve is reachable or auth fails. Never throws.
244
+ *
245
+ * Used by Bug #5 to merge sessions started directly from opencode (CLI,
246
+ * MCP, etc.) into the dashboard's bg-instance list.
247
+ *
248
+ * @param {ServeInfo} info
249
+ * @param {number} [timeoutMs]
250
+ * @returns {Promise<OpencodeSession[]|null>}
251
+ */
252
+ export async function listOpencodeSessions(info, timeoutMs = 5000) {
253
+ if (!info) return null;
254
+ const url = `${info.baseUrl}/api/session`;
255
+ const auth = `Basic ${Buffer.from(`opencode:${info.password}`).toString('base64')}`;
256
+ const ac = new AbortController();
257
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
258
+ try {
259
+ const res = await fetch(url, {
260
+ method: 'GET',
261
+ headers: {
262
+ Authorization: auth,
263
+ Accept: 'application/json',
264
+ },
265
+ signal: ac.signal,
266
+ });
267
+ if (!res.ok) {
268
+ return null;
269
+ }
270
+ const body = await res.json().catch(() => null);
271
+ if (!body) return null;
272
+ // v2 wraps in {data: [...]}; defensive fallback to top-level array.
273
+ const arr = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : null;
274
+ if (!arr) return null;
275
+ // Normalize to a flat shape so callers don't have to know about v1/v2.
276
+ return arr.map((s) => ({
277
+ id: s?.id,
278
+ projectID: s?.projectID ?? s?.project_id ?? null,
279
+ parentID: s?.parentID ?? s?.parent_id ?? null,
280
+ title: s?.title ?? '',
281
+ agent: s?.agent ?? null,
282
+ model:
283
+ s?.model && typeof s.model === 'object'
284
+ ? {
285
+ id: s.model.id,
286
+ providerID: s.model.providerID ?? s.model.provider_id ?? '',
287
+ variant: s.model.variant ?? null,
288
+ }
289
+ : null,
290
+ time: {
291
+ created: s?.time?.created ?? 0,
292
+ updated: s?.time?.updated ?? 0,
293
+ archived: s?.time?.archived ?? null,
294
+ },
295
+ location: {
296
+ directory: s?.location?.directory ?? '',
297
+ workspaceID: s?.location?.workspaceID ?? s?.location?.workspace_id ?? null,
298
+ },
299
+ }));
300
+ } catch {
301
+ return null;
302
+ } finally {
303
+ clearTimeout(timer);
304
+ }
305
+ }
306
+
307
+ export const SERVE_INFO_FILE_PATHS = SERVE_INFO_FILES;
308
+
309
+ // ── v3.5.4 (bug: dispatch stuck) — spawn helpers ─────────────────────────
310
+ //
311
+ // The dashboard's task delegator used to shell out to
312
+ // `node plugins/bizar/dist/cli.js bg enqueue`. That CLI does not exist
313
+ // (the plugin is a Bun-native TS project with no compiled dist), so every
314
+ // spawn silently failed and subtasks got stuck at `doing 5% Dispatched`
315
+ // forever.
316
+ //
317
+ // The fix is to talk to the opencode serve child the plugin owns directly
318
+ // — the plugin already spawns `opencode serve` on init and publishes the
319
+ // port + password via serve-info. The endpoints below mirror what the
320
+ // plugin's own `bizar_spawn_background` MCP tool does in-process
321
+ // (plugins/bizar/src/http-client.ts → `createSession` + `sendPrompt`).
322
+ //
323
+ // All helpers are best-effort. A failure to reach the serve child
324
+ // returns a structured `{ ok: false, error, status? }` so callers can log
325
+ // the cause instead of swallowing it.
326
+
327
+ const DEFAULT_TIMEOUT_MS = 8_000;
328
+
329
+ /**
330
+ * Build the Authorization header for the opencode serve child. The wire
331
+ * format is `Basic base64("opencode:<password>")` (matches what the
332
+ * plugin's own HttpClient uses; see plugins/bizar/src/http-client.ts).
333
+ */
334
+ function buildAuthHeader(info) {
335
+ const creds = `opencode:${info.password}`;
336
+ return `Basic ${Buffer.from(creds).toString('base64')}`;
337
+ }
338
+
339
+ /**
340
+ * `POST /api/session?directory=...` — create a new background session.
341
+ *
342
+ * v2 wire format (per plugins/bizar/src/http-client.ts): request body is
343
+ * `{ title, agent, parentID?, model? }`; response is `{ data: { id, ... } }`
344
+ * or the session at top level. We unwrap defensively.
345
+ *
346
+ * @param {ServeInfo} info
347
+ * @param {{ title: string, agent: string, parentID?: string }} opts
348
+ * @param {string} [directory] defaults to info.worktree
349
+ * @param {number} [timeoutMs]
350
+ * @returns {Promise<{ok:true,sessionId:string}|{ok:false,error:string,status?:number}>}
351
+ */
352
+ export async function createOpencodeSession(info, opts, directory, timeoutMs = DEFAULT_TIMEOUT_MS) {
353
+ if (!info) return { ok: false, error: 'serve-info not available — plugin is not running' };
354
+ if (!opts || typeof opts.agent !== 'string' || opts.agent.length === 0) {
355
+ return { ok: false, error: 'agent is required' };
356
+ }
357
+ const title = (opts.title || `${opts.agent}: bg-session`).toString().slice(0, 200);
358
+ const dir = directory || info.worktree || '';
359
+ const url = `${info.baseUrl}/api/session?directory=${encodeURIComponent(dir)}`;
360
+ const ac = new AbortController();
361
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
362
+ try {
363
+ const body = { title, agent: opts.agent };
364
+ if (opts.parentID) body.parentID = opts.parentID;
365
+ const res = await fetch(url, {
366
+ method: 'POST',
367
+ headers: {
368
+ Authorization: buildAuthHeader(info),
369
+ 'Content-Type': 'application/json',
370
+ Accept: 'application/json',
371
+ },
372
+ body: JSON.stringify(body),
373
+ signal: ac.signal,
374
+ });
375
+ if (!res.ok) {
376
+ let detail = '';
377
+ try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
378
+ return {
379
+ ok: false,
380
+ status: res.status,
381
+ error: `POST /api/session failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
382
+ };
383
+ }
384
+ const parsed = await res.json().catch(() => null);
385
+ const sessionId =
386
+ (parsed && typeof parsed === 'object' && (parsed.data?.id || parsed.id)) ||
387
+ null;
388
+ if (!sessionId) {
389
+ return { ok: false, status: res.status, error: 'POST /api/session: response missing id' };
390
+ }
391
+ return { ok: true, sessionId: String(sessionId) };
392
+ } catch (err) {
393
+ const msg = err instanceof Error ? err.message : String(err);
394
+ const isAbort = err instanceof Error && err.name === 'AbortError';
395
+ return {
396
+ ok: false,
397
+ error: isAbort ? `createSession timed out after ${timeoutMs}ms` : `createSession network error: ${msg}`,
398
+ };
399
+ } finally {
400
+ clearTimeout(timer);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * `POST /api/session/{id}/prompt?directory=...` — fire the prompt.
406
+ *
407
+ * v2 wire format (per plugins/bizar/src/http-client.ts): body is
408
+ * `{ id: <messageID>, prompt: { text }, agent, model? }`. The `id` is a
409
+ * client-generated message ID; the dashboard already has its own task ID
410
+ * scheme, so we just synthesize a unique one.
411
+ *
412
+ * @param {ServeInfo} info
413
+ * @param {{ sessionId: string, agent: string, text: string, messageID?: string }} opts
414
+ * @param {string} [directory]
415
+ * @param {number} [timeoutMs]
416
+ * @returns {Promise<{ok:true,messageId:string}|{ok:false,error:string,status?:number}>}
417
+ */
418
+ export async function sendOpencodePrompt(info, opts, directory, timeoutMs = DEFAULT_TIMEOUT_MS) {
419
+ if (!info) return { ok: false, error: 'serve-info not available — plugin is not running' };
420
+ if (!opts || !opts.sessionId || !opts.agent || !opts.text) {
421
+ return { ok: false, error: 'sessionId, agent, and text are required' };
422
+ }
423
+ const dir = directory || info.worktree || '';
424
+ const messageID = opts.messageID || `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
425
+ const url = `${info.baseUrl}/api/session/${encodeURIComponent(opts.sessionId)}/prompt?directory=${encodeURIComponent(dir)}`;
426
+ const ac = new AbortController();
427
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
428
+ try {
429
+ const body = {
430
+ id: messageID,
431
+ prompt: { text: String(opts.text).slice(0, 200_000) },
432
+ agent: opts.agent,
433
+ };
434
+ const res = await fetch(url, {
435
+ method: 'POST',
436
+ headers: {
437
+ Authorization: buildAuthHeader(info),
438
+ 'Content-Type': 'application/json',
439
+ Accept: 'application/json',
440
+ },
441
+ body: JSON.stringify(body),
442
+ signal: ac.signal,
443
+ });
444
+ if (!res.ok) {
445
+ let detail = '';
446
+ try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
447
+ return {
448
+ ok: false,
449
+ status: res.status,
450
+ error: `POST /api/session/${opts.sessionId}/prompt failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
451
+ };
452
+ }
453
+ return { ok: true, messageId: messageID };
454
+ } catch (err) {
455
+ const msg = err instanceof Error ? err.message : String(err);
456
+ const isAbort = err instanceof Error && err.name === 'AbortError';
457
+ return {
458
+ ok: false,
459
+ error: isAbort ? `sendPrompt timed out after ${timeoutMs}ms` : `sendPrompt network error: ${msg}`,
460
+ };
461
+ } finally {
462
+ clearTimeout(timer);
463
+ }
464
+ }
465
+
466
+ // ── v3.5.5 — chat + artifact integration ──────────────────────────────
467
+ //
468
+ // The chat endpoint (POST /api/chat) and the bg-poller both need to
469
+ // read the opencode session's message list so they can:
470
+ // 1. Capture the assistant's reply to a user prompt (chat polling).
471
+ // 2. Scan the final assistant message for an `html-artifact` block
472
+ // that the agent emitted to declare a tangible artifact.
473
+ //
474
+ // `listOpencodeMessages` is a thin fetch wrapper over
475
+ // `GET /api/session/{id}/message?directory=...`. The response shape
476
+ // (per the plugin's own http-client.ts → listMessages()) is either a
477
+ // raw `Array<{ info, parts }>` or `{ data: Array<{ info, parts }> }`.
478
+ // We accept both.
479
+ //
480
+ // `extractContentFromOpencodeMessage` flattens the `parts[]` array
481
+ // into a single string for chat persistence and artifact detection.
482
+
483
+ /**
484
+ * GET /api/session/{id}/message — list the messages of a session.
485
+ *
486
+ * v2 wire format (per plugins/bizar/src/http-client.ts): response is
487
+ * `{ data: Array<{ info, parts }> }` or a bare array. We accept both.
488
+ *
489
+ * @param {ServeInfo} info
490
+ * @param {string} sessionId
491
+ * @param {string} [directory]
492
+ * @param {number} [timeoutMs]
493
+ * @returns {Promise<{ok:true,messages:Array}|{ok:false,error:string,status?:number}>}
494
+ */
495
+ export async function listOpencodeMessages(info, sessionId, directory, timeoutMs = 8_000) {
496
+ if (!info) return { ok: false, error: 'serve-info not available — plugin is not running' };
497
+ if (!sessionId) return { ok: false, error: 'sessionId is required' };
498
+ const dir = directory || info.worktree || '';
499
+ const url = `${info.baseUrl}/api/session/${encodeURIComponent(sessionId)}/message?directory=${encodeURIComponent(dir)}`;
500
+ const ac = new AbortController();
501
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
502
+ try {
503
+ const res = await fetch(url, {
504
+ method: 'GET',
505
+ headers: {
506
+ Authorization: buildAuthHeader(info),
507
+ Accept: 'application/json',
508
+ },
509
+ signal: ac.signal,
510
+ });
511
+ if (!res.ok) {
512
+ let detail = '';
513
+ try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
514
+ return {
515
+ ok: false,
516
+ status: res.status,
517
+ error: `GET /api/session/.../message failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
518
+ };
519
+ }
520
+ const body = await res.json().catch(() => null);
521
+ if (!body) return { ok: true, messages: [] };
522
+ const messages = Array.isArray(body) ? body : Array.isArray(body?.data) ? body.data : [];
523
+ return { ok: true, messages };
524
+ } catch (err) {
525
+ const msg = err instanceof Error ? err.message : String(err);
526
+ const isAbort = err instanceof Error && err.name === 'AbortError';
527
+ return {
528
+ ok: false,
529
+ error: isAbort ? `listMessages timed out after ${timeoutMs}ms` : `listMessages network error: ${msg}`,
530
+ };
531
+ } finally {
532
+ clearTimeout(timer);
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Flatten an opencode message's `parts[]` into a single string.
538
+ *
539
+ * Opencode v2 messages look like:
540
+ * { info: { id, role, time }, parts: [{type, text}, {type, text}, ...] }
541
+ *
542
+ * We concatenate every `text` part in order, joined by `\n\n`. Non-text
543
+ * parts (tool calls, etc.) are skipped — they don't contribute to a
544
+ * human-readable chat or artifact scan.
545
+ *
546
+ * @param {object} msg one entry from `listOpencodeMessages().messages`
547
+ * @returns {string}
548
+ */
549
+ export function extractContentFromOpencodeMessage(msg) {
550
+ if (!msg) return '';
551
+ // Some opencode v1 builds put text directly on the message itself.
552
+ if (typeof msg.text === 'string') return finalizeText(msg.text);
553
+ if (typeof msg.content === 'string') return finalizeText(msg.content);
554
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
555
+ const textParts = parts
556
+ .filter((p) => p && (p.type === 'text' || typeof p.text === 'string'))
557
+ .map((p) => (typeof p.text === 'string' ? p.text : ''))
558
+ .filter(Boolean);
559
+ return finalizeText(textParts.join('\n\n'));
560
+ }
561
+
562
+ /**
563
+ * Strip `<thinking>...</thinking>` blocks from model output.
564
+ *
565
+ * Some opencode builds (and the M3 model specifically) emit inline
566
+ * `<thinking>` reasoning tags directly in the assistant text content.
567
+ * React-markdown renders those as visible escaped HTML, so the chat UI
568
+ * ends up showing the raw tag. The reasoning itself is also captured
569
+ * separately as `reasoning_details` parts — we are not losing data by
570
+ * hiding the inline tag from the rendered text.
571
+ *
572
+ * Handles:
573
+ * - `<thinking>...</thinking>` blocks (case-insensitive, multiline)
574
+ * - `<thinking ...attrs>` and `<thinking/>` self-closing variants
575
+ * - Stray closing tags like `</thinking>` with no matching opener
576
+ * - Multiple blocks in the same string
577
+ * - Empty input → empty output
578
+ *
579
+ * After stripping, collapses runs of 3+ newlines to 2 so we don't
580
+ * leave huge blank gaps in the rendered output.
581
+ */
582
+ export function stripThinkingTags(text) {
583
+ if (typeof text !== 'string') return '';
584
+ const cleaned = text
585
+ .replace(/<thinking\b[^>]*>[\s\S]*?<\/thinking>/gi, '')
586
+ .replace(/<thinking\b[^>]*\/?>/gi, '')
587
+ .replace(/<\/thinking>/gi, '');
588
+ return cleaned.replace(/\n{3,}/g, '\n\n').trim();
589
+ }
590
+
591
+ function finalizeText(text) {
592
+ return stripThinkingTags(text);
593
+ }
594
+
595
+ /**
596
+ * Normalize an opencode message into the dashboard's chat shape:
597
+ * { id, role, content, ts, agent? }
598
+ *
599
+ * Used by GET /api/tasks/:id/chat to surface a bg instance's opencode
600
+ * session inside the chat UI.
601
+ *
602
+ * @param {object} msg
603
+ * @returns {{id:string,role:string,content:string,ts:number}}
604
+ */
605
+ export function normalizeOpencodeMessage(msg) {
606
+ if (!msg) return { id: '', role: 'assistant', content: '', ts: Date.now() };
607
+ const id = msg?.info?.id || msg?.id || '';
608
+ const role = msg?.info?.role || msg?.role || 'assistant';
609
+ const ts = msg?.info?.time?.created
610
+ || (typeof msg?.info?.time === 'object' ? msg.info.time.created : null)
611
+ || (typeof msg?.time?.created === 'number' ? msg.time.created : null)
612
+ || Date.now();
613
+ return {
614
+ id: String(id),
615
+ role: String(role),
616
+ content: extractContentFromOpencodeMessage(msg),
617
+ ts: typeof ts === 'number' ? ts : Date.now(),
618
+ };
619
+ }
620
+
621
+ /**
622
+ * `GET /health` — used by the dispatcher startup check to verify the
623
+ * serve child is actually reachable before we try to enqueue work.
624
+ *
625
+ * v3.11.0 — Replaced the HTTP `GET /health` probe with a TCP-connect
626
+ * port-open check via `net.createConnection`. Rationale:
627
+ * - The pre-v3.11.0 implementation sent `GET /health` with the
628
+ * Basic-auth header from `serve.json`. If the opencode serve
629
+ * instance rejected the auth (e.g. a stale password file, a
630
+ * plugin version mismatch, or a 401 from a different auth realm),
631
+ * the probe returned `false` even when the opencode process was
632
+ * perfectly healthy and answering other requests. That cascaded
633
+ * into every background dispatch short-circuiting on the
634
+ * `if (serveInfo && serveReachable)` guard at
635
+ * `task-delegator.mjs:563` and being marked `dispatchPending: true`.
636
+ * - TCP-connect is the standard "is this port alive" check. It does
637
+ * not depend on HTTP auth, the opencode version's endpoint shape,
638
+ * or the path being correct. If the opencode process is bound to
639
+ * the port, we can talk to it (auth on the actual endpoints will
640
+ * still be validated when we issue those calls).
641
+ * - 1.5s default timeout — long enough to survive a slow CI host,
642
+ * short enough that the dispatch path doesn't stall the user
643
+ * when the plugin is genuinely down.
644
+ *
645
+ * @param {ServeInfo} info
646
+ * @param {number} [timeoutMs]
647
+ * @returns {Promise<boolean>}
648
+ */
649
+ export function pingOpencodeServe(info, timeoutMs = 1_500) {
650
+ return new Promise((resolve) => {
651
+ if (!info || typeof info.port !== 'number') {
652
+ resolve(false);
653
+ return;
654
+ }
655
+ // We connect to 127.0.0.1 explicitly — the TCP probe must not
656
+ // accidentally hit a remote host if the baseUrl were ever wrong.
657
+ const host = '127.0.0.1';
658
+ const port = info.port;
659
+ let settled = false;
660
+ const finish = (ok) => {
661
+ if (settled) return;
662
+ settled = true;
663
+ try { socket.destroy(); } catch { /* ignore */ }
664
+ resolve(ok);
665
+ };
666
+ const socket = createConnection({ host, port });
667
+ const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs));
668
+ socket.once('connect', () => {
669
+ clearTimeout(timer);
670
+ finish(true);
671
+ });
672
+ socket.once('error', () => {
673
+ clearTimeout(timer);
674
+ finish(false);
675
+ });
676
+ // Surface any unexpected socket-level error so it doesn't crash
677
+ // the process. `error` is already handled above, but `close`
678
+ // can fire after `connect`/`error` without indicating failure.
679
+ socket.once('close', () => {
680
+ clearTimeout(timer);
681
+ finish(settled ? false : false);
682
+ });
683
+ });
684
+ }