@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,55 @@
1
+ // src/components/chat/SuggestionCards.tsx — 4 hardcoded suggestion cards for first-run.
2
+
3
+ import { Code, BookOpen, Lightbulb, Image as ImageIcon } from 'lucide-react';
4
+
5
+ interface Props {
6
+ projectName: string;
7
+ onPickSuggestion: (text: string) => void;
8
+ }
9
+
10
+ export function SuggestionCards({ projectName, onPickSuggestion }: Props) {
11
+ const suggestions = [
12
+ {
13
+ icon: <Code size={16} />,
14
+ title: projectName ? `Explain ${projectName}` : 'Explain this project',
15
+ preview: 'Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',
16
+ prompt: projectName
17
+ ? `Explain the ${projectName} project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.`
18
+ : 'Explain the current project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',
19
+ },
20
+ {
21
+ icon: <BookOpen size={16} />,
22
+ title: 'Summarize recent sessions',
23
+ preview: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',
24
+ prompt: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',
25
+ },
26
+ {
27
+ icon: <Lightbulb size={16} />,
28
+ title: 'Suggest a refactor',
29
+ preview: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',
30
+ prompt: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',
31
+ },
32
+ {
33
+ icon: <ImageIcon size={16} />,
34
+ title: 'Find visual regressions',
35
+ preview: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',
36
+ prompt: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',
37
+ },
38
+ ];
39
+
40
+ return (
41
+ <div className="chat-suggestions">
42
+ {suggestions.map((s, i) => (
43
+ <button
44
+ key={i}
45
+ type="button"
46
+ className="chat-suggestion"
47
+ onClick={() => onPickSuggestion(s.prompt)}
48
+ >
49
+ <div className="chat-suggestion-title">{s.icon}<span>{s.title}</span></div>
50
+ <div className="chat-suggestion-preview">{s.preview}</div>
51
+ </button>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,36 @@
1
+ // src/components/chat/WelcomeScreen.tsx — gradient greeting + no-project state.
2
+
3
+ import { Folder } from 'lucide-react';
4
+ import { SuggestionCards } from './SuggestionCards';
5
+
6
+ interface Props {
7
+ variant: 'empty' | 'no-project';
8
+ projectName: string;
9
+ onPickSuggestion: (text: string) => void;
10
+ }
11
+
12
+ export function WelcomeScreen({ variant, projectName, onPickSuggestion }: Props) {
13
+ if (variant === 'no-project') {
14
+ return (
15
+ <div className="chat-welcome">
16
+ <h1 className="chat-welcome-h1">
17
+ <span className="chat-greeting-hello">No project selected.</span>
18
+ </h1>
19
+ <p className="chat-welcome-subtitle">Pick a project in Overview to scope chat sessions.</p>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ const displayName = projectName || 'there';
25
+
26
+ return (
27
+ <div className="chat-welcome">
28
+ <h1 className="chat-welcome-h1">
29
+ <span className="chat-greeting-hello">Hello, </span>
30
+ <span className="chat-greeting-name">{displayName}.</span>
31
+ </h1>
32
+ <p className="chat-welcome-subtitle">How can I help you today?</p>
33
+ <SuggestionCards projectName={projectName} onPickSuggestion={onPickSuggestion} />
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,20 @@
1
+ // src/components/chat/index.ts — barrel export for all chat components and hooks.
2
+
3
+ export { ChatTopBar } from './ChatTopBar';
4
+ export { ChatThread } from './ChatThread';
5
+ export { MessageBubble } from './MessageBubble';
6
+ export { WelcomeScreen } from './WelcomeScreen';
7
+ export { SuggestionCards } from './SuggestionCards';
8
+ export { Composer } from './Composer';
9
+ export { FloatingComposer } from './FloatingComposer';
10
+ export { AgentChip } from './AgentChip';
11
+ export { SessionList } from './SessionList';
12
+ export { InfoPanel } from './InfoPanel';
13
+ export { LoadingSkeleton } from './LoadingSkeleton';
14
+ export { StreamingIndicator } from './StreamingIndicator';
15
+ export { ConfirmModal } from './ConfirmModal';
16
+ export { EmptyState } from './EmptyState';
17
+ export { useChat } from './useChat';
18
+ export { useSlashCommands } from './useSlashCommands';
19
+ export { useAutoGrowTextarea } from './useAutoGrowTextarea';
20
+ export type { SlashCommand } from './useSlashCommands';
@@ -0,0 +1,18 @@
1
+ // src/components/chat/useAutoGrowTextarea.ts — auto-resize textarea up to maxHeight.
2
+
3
+ import { useLayoutEffect } from 'react';
4
+
5
+ export function useAutoGrowTextarea(
6
+ ref: React.RefObject<HTMLTextAreaElement | null>,
7
+ value: string,
8
+ maxHeight = 240,
9
+ ): void {
10
+ useLayoutEffect(() => {
11
+ const el = ref.current;
12
+ if (!el) return;
13
+ el.style.height = 'auto';
14
+ const next = Math.min(el.scrollHeight, maxHeight);
15
+ el.style.height = `${next}px`;
16
+ el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
17
+ }, [ref, value, maxHeight]);
18
+ }
@@ -0,0 +1,219 @@
1
+ // src/components/chat/useChat.ts — shared chat state: messages, sessions, send, polling.
2
+
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import type { ChatMessage, ChatResponse, ChatSession, Settings, Snapshot } from '../../lib/types';
5
+ import { api } from '../../lib/api';
6
+
7
+ export function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?: string | null) {
8
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
9
+ const [sessions, setSessions] = useState<ChatSession[]>([]);
10
+ const [opencodeSessions, setOpencodeSessions] = useState<ChatSession[]>([]);
11
+ const [sessionId, setSessionId] = useState('');
12
+ const [loading, setLoading] = useState(true);
13
+ const [sending, setSending] = useState(false);
14
+ const [pinned, setPinned] = useState<Set<number>>(new Set());
15
+
16
+ const listRef = useRef<HTMLDivElement>(null);
17
+ const toastRef = useRef<{ error: (msg: string) => void; success: (msg: string) => void; info: (msg: string) => void } | null>(null);
18
+
19
+ // Auto-scroll on new messages
20
+ useEffect(() => {
21
+ if (listRef.current) {
22
+ listRef.current.scrollTop = listRef.current.scrollHeight;
23
+ }
24
+ }, [messages]);
25
+
26
+ const loadChat = useCallback(async (sid?: string) => {
27
+ try {
28
+ const url = sid ? `/chat?session=${encodeURIComponent(sid)}` : '/chat?limit=200';
29
+ const data = await api.get<ChatResponse>(url);
30
+ setMessages(data.messages || []);
31
+ setSessions(data.sessions || []);
32
+ } catch (err) {
33
+ toastRef.current?.error(`Chat load failed: ${(err as Error).message}`);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, []);
38
+
39
+ const loadTaskChat = useCallback(async (taskId: string) => {
40
+ setLoading(true);
41
+ try {
42
+ const data = await api.get<{ sessionId?: string; messages: ChatMessage[] }>(`/tasks/${encodeURIComponent(taskId)}/chat`);
43
+ setMessages(data.messages || []);
44
+ setSessionId(data.sessionId || taskId);
45
+ } catch (err) {
46
+ toastRef.current?.error(`Task chat load failed: ${(err as Error).message}`);
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }, []);
51
+
52
+ const refreshSessions = useCallback(async () => {
53
+ try {
54
+ const data = await api.get<{ sessions: ChatSession[] }>('/chat/sessions');
55
+ setSessions(data.sessions || []);
56
+ } catch { /* best-effort */ }
57
+ }, []);
58
+
59
+ const refreshOpencodeSessions = useCallback(async () => {
60
+ try {
61
+ const data = await api.get<{ sessions: ChatSession[] }>('/opencode-sessions');
62
+ // Enrich with source marker and opencodeUrl; port is discovered from serve.json
63
+ const enriched: ChatSession[] = (data.sessions || []).map((s) => ({
64
+ ...s,
65
+ source: 'opencode' as const,
66
+ opencodeUrl: `/opencode/session/${s.id}`,
67
+ title: s.title || s.id,
68
+ }));
69
+ setOpencodeSessions(enriched);
70
+ } catch { /* best-effort */ }
71
+ }, []);
72
+
73
+ const onCreateSession = useCallback(async () => {
74
+ if (!snapshot.activeProject) {
75
+ toastRef.current?.error('Pick a project in Overview to scope chat sessions.');
76
+ return;
77
+ }
78
+ try {
79
+ const created = await api.post<ChatSession>('/chat/sessions', {});
80
+ setSessionId(created.id);
81
+ setMessages([]);
82
+ setPinned(new Set());
83
+ await loadChat(created.id);
84
+ refreshSessions().catch(() => {});
85
+ } catch (err) {
86
+ toastRef.current?.error(`Create failed: ${(err as Error).message}`);
87
+ }
88
+ }, [snapshot.activeProject, loadChat, refreshSessions]);
89
+
90
+ const onSend = useCallback(async (message: string, agent: string, model: string, attachments?: string[]) => {
91
+ const optimistic: ChatMessage = {
92
+ role: 'user',
93
+ content: message,
94
+ agent,
95
+ ts: new Date().toISOString(),
96
+ };
97
+ setMessages((cur) => [...cur, optimistic]);
98
+ setSending(true);
99
+ try {
100
+ const response = await api.post<{ messages?: ChatMessage[] }>('/chat', { message, agent, model, attachments: attachments || [] });
101
+ if (response?.messages) {
102
+ for (const msg of response.messages) {
103
+ if (msg.role === 'assistant') {
104
+ setMessages((cur) => {
105
+ const exists = cur.some((m) => m.ts === msg.ts);
106
+ return exists ? cur : [...cur, msg];
107
+ });
108
+ }
109
+ }
110
+ } else {
111
+ toastRef.current?.info('Still processing… the response will appear shortly.');
112
+ }
113
+ } catch (err) {
114
+ setMessages((cur) => cur.filter((m) => m !== optimistic));
115
+ toastRef.current?.error(`Send failed: ${(err as Error).message}`);
116
+ } finally {
117
+ setSending(false);
118
+ }
119
+ }, []);
120
+
121
+ const onRegenerate = useCallback(async (messageId: string) => {
122
+ if (!sessionId) {
123
+ toastRef.current?.error('No active session to regenerate.');
124
+ return;
125
+ }
126
+ try {
127
+ await api.post('/chat/regenerate', { sessionId, messageId });
128
+ await loadChat(sessionId);
129
+ } catch (err) {
130
+ toastRef.current?.error(`Regenerate failed: ${(err as Error).message}`);
131
+ }
132
+ }, [sessionId, loadChat]);
133
+
134
+ const deleteMessage = useCallback((idx: number) => {
135
+ setMessages((cur) => cur.filter((_, i) => i !== idx));
136
+ }, []);
137
+
138
+ const togglePin = useCallback((idx: number) => {
139
+ setPinned((cur) => {
140
+ const next = new Set(cur);
141
+ if (next.has(idx)) next.delete(idx);
142
+ else next.add(idx);
143
+ return next;
144
+ });
145
+ }, []);
146
+
147
+ const copyMessage = useCallback((m: ChatMessage) => {
148
+ const text = m.content || m.message || '';
149
+ navigator.clipboard?.writeText(text).then(
150
+ () => toastRef.current?.success('Copied.'),
151
+ () => toastRef.current?.error('Copy failed.'),
152
+ );
153
+ }, []);
154
+
155
+ // Initial load
156
+ useEffect(() => {
157
+ if (initialTaskId) {
158
+ loadTaskChat(initialTaskId);
159
+ } else {
160
+ loadChat();
161
+ }
162
+ refreshOpencodeSessions();
163
+ // eslint-disable-next-line react-hooks/exhaustive-deps
164
+ }, []);
165
+
166
+ // Listen for bizar:setChatTask events
167
+ useEffect(() => {
168
+ const handler = (e: Event) => {
169
+ const taskId = (e as CustomEvent<{ taskId: string }>).detail?.taskId;
170
+ if (taskId) loadTaskChat(taskId);
171
+ };
172
+ window.addEventListener('bizar:setChatTask', handler);
173
+ return () => window.removeEventListener('bizar:setChatTask', handler);
174
+ }, [loadTaskChat]);
175
+
176
+ // WebSocket for live updates — dynamically imported to avoid SSR issues
177
+ useEffect(() => {
178
+ let closed = false;
179
+ import('../../lib/ws').then(({ Ws }) => {
180
+ if (closed) return;
181
+ const ws = new Ws();
182
+ ws.on((msg) => {
183
+ if (msg.type !== 'chat:message') return;
184
+ const next = msg.message;
185
+ setMessages((cur) => {
186
+ const exists = cur.some((m) => m.ts === next.ts);
187
+ return exists ? cur : [...cur, next];
188
+ });
189
+ });
190
+ });
191
+ return () => { closed = true; };
192
+ }, []);
193
+
194
+ return {
195
+ // State
196
+ messages,
197
+ sessions,
198
+ opencodeSessions,
199
+ sessionId,
200
+ setSessionId,
201
+ loading,
202
+ sending,
203
+ pinned,
204
+ listRef,
205
+ // Setters for toast integration
206
+ setToast: (toast: typeof toastRef.current) => { toastRef.current = toast; },
207
+ // Actions
208
+ loadChat,
209
+ loadTaskChat,
210
+ refreshSessions,
211
+ refreshOpencodeSessions,
212
+ onCreateSession,
213
+ onSend,
214
+ onRegenerate,
215
+ deleteMessage,
216
+ togglePin,
217
+ copyMessage,
218
+ };
219
+ }
@@ -0,0 +1,47 @@
1
+ // src/components/chat/useSlashCommands.ts — filter slash-command suggestions from mods.
2
+
3
+ import { useMemo, useState } from 'react';
4
+ import type { Mod, Snapshot } from '../../lib/types';
5
+
6
+ export type SlashCommand = { cmd: string; desc: string; mod?: string };
7
+
8
+ const BUILTIN_COMMANDS: SlashCommand[] = [
9
+ { cmd: '/visual-plan [on|off|status]', desc: 'Toggle or view visual plan mode' },
10
+ { cmd: '/plan new <slug> [template]', desc: 'Create a new plan' },
11
+ { cmd: '/plan list', desc: 'List all plans' },
12
+ { cmd: '/plan open <slug>', desc: 'Open a plan in the viewer' },
13
+ { cmd: '/plan status <slug> <status>', desc: 'Set plan status' },
14
+ { cmd: '/plan get <slug>', desc: 'Fetch plan canvas' },
15
+ { cmd: '/plan add <slug> --title T --type kind', desc: 'Add element to a plan' },
16
+ { cmd: '/plan update <slug> <id>', desc: 'Update a plan element' },
17
+ { cmd: '/plan delete <slug> <id>', desc: 'Delete a plan element' },
18
+ { cmd: '/plan comment <slug> [id] "text"', desc: 'Add a comment' },
19
+ { cmd: '/plan comments <slug> [id]', desc: 'Read plan comments' },
20
+ { cmd: '/bizar', desc: 'Launch Bizar dashboard' },
21
+ { cmd: '/bizar <args>', desc: 'Route via Bizar menu' },
22
+ { cmd: '/audit', desc: 'Run security audit' },
23
+ { cmd: '/explain <q>', desc: 'Read-only code Q&A' },
24
+ { cmd: '/init', desc: 'Initialize .bizar/ in this project' },
25
+ { cmd: '/learn', desc: 'Extract patterns from session' },
26
+ { cmd: '/pr-review', desc: 'PR review' },
27
+ { cmd: '/help | /commands', desc: 'Show all Bizar commands' },
28
+ ];
29
+
30
+ export function useSlashCommands(snapshot: Snapshot) {
31
+ const [query, setQuery] = useState('');
32
+
33
+ const allCommands = useMemo<SlashCommand[]>(() => [
34
+ ...BUILTIN_COMMANDS,
35
+ ...(snapshot.mods || []).flatMap((m: Mod) =>
36
+ m.entry?.command ? [{ cmd: `/${m.id}`, desc: m.description || m.name, mod: m.id }] : [],
37
+ ),
38
+ ], [snapshot.mods]);
39
+
40
+ const suggestions = useMemo(() => {
41
+ if (!query.startsWith('/') || query.includes(' ')) return [];
42
+ const q = query.toLowerCase();
43
+ return allCommands.filter((c) => c.cmd.toLowerCase().startsWith(q)).slice(0, 6);
44
+ }, [query, allCommands]);
45
+
46
+ return { allCommands, suggestions, setQuery };
47
+ }
@@ -0,0 +1,37 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Bizar Dashboard</title>
7
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪩</text></svg>" />
8
+ <link rel="preconnect" href="https://rsms.me/" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
12
+ <!-- v3.23.0 — Mobile app CSS at a stable URL so MobileApp renders with
13
+ styles even when the auto-redirect to /m is bypassed. -->
14
+ <link rel="stylesheet" href="/mobile.css" />
15
+ <script>
16
+ // v3.5.0 — Auto-redirect to /m on mobile devices.
17
+ // v3.23.0 — Also redirect on width alone so emulators + desktop browsers resized
18
+ // to a narrow window still land on the mobile app. The `pointer: coarse`
19
+ // check is kept as one signal but not required.
20
+ (function () {
21
+ var narrowViewport =
22
+ window.matchMedia('(max-width: 768px)').matches ||
23
+ window.matchMedia('(max-width: 768px) and (pointer: coarse)').matches;
24
+ var touchDevice = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
25
+ var isMobile = narrowViewport || touchDevice;
26
+ var wantsDesktop = new URLSearchParams(window.location.search).has('desktop');
27
+ if (isMobile && !wantsDesktop && !window.location.pathname.startsWith('/m')) {
28
+ window.location.replace('/m' + window.location.pathname + window.location.search);
29
+ }
30
+ })();
31
+ </script>
32
+ </head>
33
+ <body>
34
+ <div id="root"></div>
35
+ <script type="module" src="/main.tsx"></script>
36
+ </body>
37
+ </html>
@@ -0,0 +1,193 @@
1
+ // src/lib/api.ts — REST client. Single instance, type-safe wrappers.
2
+ //
3
+ // v3.6.1 — Authentication is now loopback-aware on the server side.
4
+ // The browser makes API calls over 127.0.0.1 (or via Tailscale serve
5
+ // proxying to 127.0.0.1), and the server auto-trusts loopback
6
+ // connections. So a fresh browser tab "just works" — no token paste
7
+ // required for the normal case.
8
+ //
9
+ // The token still exists for non-loopback clients (external API
10
+ // consumers, scripts) and as a defense-in-depth option via the
11
+ // BIZAR_DASHBOARD_REQUIRE_AUTH=1 env var. The localStorage flow
12
+ // remains: probe /api/auth/status on load, send the token header
13
+ // if one is set, fall back to none if not (the server will trust
14
+ // us anyway because we're loopback).
15
+ //
16
+ // For SSE routes, the token is also added as a `?token=...` query
17
+ // parameter (browsers can't set custom headers on EventSource).
18
+ // Again, loopback makes this optional but kept for safety.
19
+
20
+ const TOKEN_KEY = 'bizar-auth-token';
21
+ const LOOPBACK_KEY = 'bizar-loopback-trusted';
22
+
23
+ export class ApiError extends Error {
24
+ status: number;
25
+ data: unknown;
26
+ constructor(message: string, status: number, data?: unknown) {
27
+ super(message);
28
+ this.name = 'ApiError';
29
+ this.status = status;
30
+ this.data = data;
31
+ }
32
+ }
33
+
34
+ class ApiClient {
35
+ base = '/api';
36
+
37
+ /** Get the cached bearer token from localStorage, or '' if none. */
38
+ getToken(): string {
39
+ try {
40
+ return localStorage.getItem(TOKEN_KEY) || '';
41
+ } catch {
42
+ return '';
43
+ }
44
+ }
45
+
46
+ /** Persist the token. Called from the Settings tab after Reveal/Regenerate. */
47
+ setToken(token: string): void {
48
+ try {
49
+ if (token) localStorage.setItem(TOKEN_KEY, token);
50
+ else localStorage.removeItem(TOKEN_KEY);
51
+ } catch {
52
+ /* localStorage unavailable — token won't persist across reloads */
53
+ }
54
+ }
55
+
56
+ /** Build the URL with the token appended as ?token=… if present. */
57
+ urlWithToken(path: string): string {
58
+ const url = this.base + path;
59
+ const tok = this.getToken();
60
+ if (!tok) return url;
61
+ const sep = url.includes('?') ? '&' : '?';
62
+ return `${url}${sep}token=${encodeURIComponent(tok)}`;
63
+ }
64
+
65
+ /**
66
+ * v3.6.1 — Probe the server's /api/auth/status endpoint and cache
67
+ * whether the current browser is on a loopback-trusted connection.
68
+ * Called once on app mount so the rest of the client can decide
69
+ * whether to surface the "paste your token" UI in Settings or
70
+ * just let the dashboard work.
71
+ *
72
+ * Returns a snapshot of the status; result is also cached for
73
+ * subsequent calls to isLoopbackTrusted().
74
+ */
75
+ async probeAuthStatus(): Promise<{ required: boolean; loopback: boolean; peer: string }> {
76
+ try {
77
+ const r = await this.get<{ required: boolean; loopback: boolean; peer: string }>('/auth/status');
78
+ try { localStorage.setItem(LOOPBACK_KEY, r.loopback ? '1' : '0'); } catch { /* noop */ }
79
+ return r;
80
+ } catch {
81
+ return { required: false, loopback: true, peer: '' };
82
+ }
83
+ }
84
+
85
+ /** Cached result of the last /api/auth/status probe. */
86
+ isLoopbackTrusted(): boolean {
87
+ try { return localStorage.getItem(LOOPBACK_KEY) !== '0'; } catch { return true; }
88
+ }
89
+
90
+ /**
91
+ * Perform an authenticated GET against the dashboard server.
92
+ *
93
+ * IMPORTANT: do NOT include the `/api` prefix in `path` — the
94
+ * wrapper adds it for you. Calling `api.get('/api/projects')` will
95
+ * 404 because the server sees `/api/api/projects`. Use the
96
+ * unprefixed form: `api.get('/projects')`.
97
+ *
98
+ * @example
99
+ * const r = await api.get<ProjectList>('/projects');
100
+ */
101
+ async get<T>(path: string): Promise<T> {
102
+ return this.req<T>('GET', path);
103
+ }
104
+
105
+ /**
106
+ * Perform an authenticated POST against the dashboard server.
107
+ *
108
+ * IMPORTANT: do NOT include the `/api` prefix in `path` — the
109
+ * wrapper adds it. Use the unprefixed form: `api.post('/fs/mkdir', body)`.
110
+ */
111
+ async post<T>(path: string, body?: unknown): Promise<T> {
112
+ return this.req<T>('POST', path, body);
113
+ }
114
+
115
+ /**
116
+ * Perform an authenticated PUT against the dashboard server.
117
+ *
118
+ * IMPORTANT: do NOT include the `/api` prefix in `path` — the
119
+ * wrapper adds it. Use the unprefixed form: `api.put('/settings', body)`.
120
+ */
121
+ async put<T>(path: string, body?: unknown): Promise<T> {
122
+ return this.req<T>('PUT', path, body);
123
+ }
124
+
125
+ /**
126
+ * Perform an authenticated PATCH against the dashboard server.
127
+ *
128
+ * IMPORTANT: do NOT include the `/api` prefix in `path` — the
129
+ * wrapper adds it. Use the unprefixed form: `api.patch('/settings', body)`.
130
+ */
131
+ async patch<T>(path: string, body?: unknown): Promise<T> {
132
+ return this.req<T>('PATCH', path, body);
133
+ }
134
+
135
+ /**
136
+ * Perform an authenticated DELETE against the dashboard server.
137
+ *
138
+ * IMPORTANT: do NOT include the `/api` prefix in `path` — the
139
+ * wrapper adds it. Use the unprefixed form: `api.del('/projects/123')`.
140
+ */
141
+ async del<T = unknown>(path: string): Promise<T> {
142
+ return this.req<T>('DELETE', path);
143
+ }
144
+
145
+ private async req<T>(method: string, path: string, body?: unknown): Promise<T> {
146
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
147
+ const tok = this.getToken();
148
+ if (tok) headers.Authorization = `Bearer ${tok}`;
149
+ const opts: RequestInit = { method, headers };
150
+ if (body !== undefined && body !== null) {
151
+ opts.body = typeof body === 'string' ? body : JSON.stringify(body);
152
+ }
153
+ const r = await fetch(this.base + path, opts);
154
+ const ct = r.headers.get('content-type') || '';
155
+ if (ct.includes('application/json')) {
156
+ const data = await r.json();
157
+ if (!r.ok) {
158
+ const msg =
159
+ (data && typeof data === 'object' && 'message' in data
160
+ ? (data as { message?: string }).message
161
+ : undefined) || `${method} ${path}: ${r.status}`;
162
+ throw new ApiError(msg, r.status, data);
163
+ }
164
+ return data as T;
165
+ }
166
+ const text = await r.text();
167
+ if (!r.ok) {
168
+ throw new ApiError(
169
+ `${method} ${path}: ${r.status} — ${text.slice(0, 200)}`,
170
+ r.status,
171
+ text,
172
+ );
173
+ }
174
+ try {
175
+ return JSON.parse(text) as T;
176
+ } catch {
177
+ return text as T;
178
+ }
179
+ }
180
+ }
181
+
182
+ export const api = new ApiClient();
183
+ export { ApiClient, TOKEN_KEY };
184
+
185
+ /**
186
+ * Placeholder for the "Enhance prompt" feature.
187
+ * TODO: call system LLM API to improve the prompt
188
+ * Will use settings.systemLlm config
189
+ * Endpoint: POST /api/enhance-prompt with { text }
190
+ */
191
+ export async function enhancePrompt(text: string): Promise<string> {
192
+ return text; // passthrough for now
193
+ }