@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.
- package/README.md +83 -16
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +329 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +767 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +471 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +658 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +2063 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-cli.test.mjs +542 -0
- package/bizar-dash/tests/memory-config.test.mjs +422 -0
- package/bizar-dash/tests/memory-git.test.mjs +246 -0
- package/bizar-dash/tests/memory-lightrag.test.mjs +153 -0
- package/bizar-dash/tests/memory-protocol-drift.test.mjs +45 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/banner.mjs +1 -1
- package/cli/bin.mjs +44 -47
- package/cli/bootstrap.mjs +1 -1
- package/cli/copy.mjs +25 -16
- package/cli/dev-link.mjs +17 -1
- package/cli/doctor.mjs +4 -4
- package/cli/doctor.test.mjs +2 -2
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +31 -39
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +1436 -0
- package/cli/service.mjs +5 -6
- package/cli/utils.mjs +6 -3
- package/config/AGENTS.md +7 -7
- package/config/agents/_shared/AGENT_BASELINE.md +59 -61
- package/config/opencode.json +13 -38
- package/config/skills/memory-protocol/SKILL.md +105 -0
- package/config/skills/obsidian/SKILL.md +270 -65
- package/install.sh +59 -19
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1491 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1168 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/open-kb.ts +191 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +584 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/server.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.0.0 — Express + WebSocket server for the Bizar dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Wires the v3 API router, the file watcher, the WebSocket layer, and
|
|
7
|
+
* the static frontend (Vite-built React SPA from `dist/`) into a single
|
|
8
|
+
* HTTP + WS pair.
|
|
9
|
+
*/
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import { WebSocketServer } from 'ws';
|
|
13
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { existsSync, readFileSync, statSync, openSync, readSync, closeSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { dirname as pathDirname } from 'node:path';
|
|
18
|
+
import { createApiRouter } from './api.mjs';
|
|
19
|
+
import { createState } from './state.mjs';
|
|
20
|
+
import { createWatcher } from './watcher.mjs';
|
|
21
|
+
import { modsLoader } from './mods-loader.mjs';
|
|
22
|
+
import { projectsStore } from './projects-store.mjs';
|
|
23
|
+
import { agentsStore } from './agents-store.mjs';
|
|
24
|
+
import { tasksStore } from './tasks-store.mjs';
|
|
25
|
+
import { schedulesStore } from './schedules-store.mjs';
|
|
26
|
+
import { providersStore, mcpsStore } from './providers-store.mjs';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { startBgPoller, stopBgPoller } from './bg-poller.mjs';
|
|
29
|
+
import { startBgRetryLoop, stopBgRetryLoop } from './bg-retry.mjs';
|
|
30
|
+
import { startDialogPoller } from './dialog-poller.mjs';
|
|
31
|
+
import {
|
|
32
|
+
checkWebSocketAuth,
|
|
33
|
+
isAllowedDashboardOrigin,
|
|
34
|
+
isAllowedDashboardOriginForRequest,
|
|
35
|
+
} from './auth.mjs';
|
|
36
|
+
import { readSettings } from './routes/_shared.mjs';
|
|
37
|
+
import { buildAllowedRootsFromSettings, resolveSafePath } from './lib/path-safe.mjs';
|
|
38
|
+
import { V2EventBus } from './v2-event-bus.mjs';
|
|
39
|
+
import { loadOrCreateAuth, V2_DEFAULT_PORT } from './v2-auth-file.mjs';
|
|
40
|
+
import { createV2Router } from './routes-v2/index.mjs';
|
|
41
|
+
|
|
42
|
+
let processHandlersInstalled = false;
|
|
43
|
+
let v2Bus = null;
|
|
44
|
+
let v2Auth = null;
|
|
45
|
+
|
|
46
|
+
function installProcessHandlers() {
|
|
47
|
+
if (processHandlersInstalled) return;
|
|
48
|
+
processHandlersInstalled = true;
|
|
49
|
+
// Catch-all to prevent server crash on unhandled rejections
|
|
50
|
+
process.on('unhandledRejection', (err) => {
|
|
51
|
+
console.error('[unhandledRejection]', err);
|
|
52
|
+
});
|
|
53
|
+
process.on('uncaughtException', (err) => {
|
|
54
|
+
console.error('[uncaughtException]', err);
|
|
55
|
+
// For uncaughtException, the process state is uncertain. Log and continue
|
|
56
|
+
// unless it's a fatal error. Don't exit.
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
// server.mjs lives at src/server/ — dist/ is at the package root
|
|
62
|
+
const DIST_DIR = join(__dirname, '..', '..', 'dist');
|
|
63
|
+
const WS_BACKPRESSURE_LIMIT_BYTES = 1024 * 1024;
|
|
64
|
+
const MOBILE_UA_RE = /Android.+Mobile|iPhone|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|IEMobile/i;
|
|
65
|
+
|
|
66
|
+
let currentBroadcast = () => {};
|
|
67
|
+
|
|
68
|
+
export function broadcast(msg) {
|
|
69
|
+
return currentBroadcast(msg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Publish an event on the v2 event bus (if initialized). Exposed for
|
|
74
|
+
* other parts of the dashboard server (e.g. bg-poller, dialog-poller)
|
|
75
|
+
* to push events into the v2 SSE stream without taking a direct
|
|
76
|
+
* dependency on the bus module.
|
|
77
|
+
*/
|
|
78
|
+
export function publishV2Event(event) {
|
|
79
|
+
if (!v2Bus) return -1;
|
|
80
|
+
return v2Bus.publish(event);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Return v2 auth info for the plugin to consume (e.g. via a startup
|
|
85
|
+
* handshake or env var). Password is the same one persisted to the
|
|
86
|
+
* auth file.
|
|
87
|
+
*/
|
|
88
|
+
export function getV2Auth() {
|
|
89
|
+
if (!v2Auth) return null;
|
|
90
|
+
return {
|
|
91
|
+
baseUrl: v2Auth.baseUrl,
|
|
92
|
+
port: v2Auth.port,
|
|
93
|
+
password: v2Auth.password,
|
|
94
|
+
file: v2Auth.file,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function shouldRedirectToMobile(req) {
|
|
99
|
+
if (req.method !== 'GET') return false;
|
|
100
|
+
if (req.path !== '/') return false;
|
|
101
|
+
if (req.query?.desktop === '1') return false;
|
|
102
|
+
const accept = req.headers.accept || '';
|
|
103
|
+
if (typeof accept === 'string' && !accept.includes('text/html')) return false;
|
|
104
|
+
const ua = req.headers['user-agent'] || '';
|
|
105
|
+
return typeof ua === 'string' && MOBILE_UA_RE.test(ua);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mobileRedirectTarget(req) {
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
for (const [key, value] of Object.entries(req.query || {})) {
|
|
111
|
+
if (key === 'desktop') continue;
|
|
112
|
+
if (Array.isArray(value)) {
|
|
113
|
+
for (const item of value) params.append(key, String(item));
|
|
114
|
+
} else if (value != null) {
|
|
115
|
+
params.set(key, String(value));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const qs = params.toString();
|
|
119
|
+
return qs ? `/m?${qs}` : '/m';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {object} opts
|
|
124
|
+
* @param {number} opts.port
|
|
125
|
+
* @param {string} opts.projectRoot
|
|
126
|
+
* @param {string} opts.opencodeConfigDir
|
|
127
|
+
* @param {string} opts.bizarRoot
|
|
128
|
+
*/
|
|
129
|
+
export async function createServer({
|
|
130
|
+
port,
|
|
131
|
+
projectRoot,
|
|
132
|
+
opencodeConfigDir,
|
|
133
|
+
bizarRoot,
|
|
134
|
+
}) {
|
|
135
|
+
installProcessHandlers();
|
|
136
|
+
const app = express();
|
|
137
|
+
app.disable('x-powered-by');
|
|
138
|
+
// v3.5.4 (CORS) — Reflect the request Origin back as
|
|
139
|
+
// Access-Control-Allow-Origin so the Vite dev server (5174), a tunneled
|
|
140
|
+
// remote, or a localhost:4321 same-origin tab all work. We also allow
|
|
141
|
+
// credentials so the dashboard can keep using cookie-style pair tokens.
|
|
142
|
+
// This is a local tool — same-origin is the norm; reflection is the
|
|
143
|
+
// simplest correct policy.
|
|
144
|
+
app.use(cors({
|
|
145
|
+
origin(origin, callback) {
|
|
146
|
+
if (!origin) {
|
|
147
|
+
callback(null, true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const allowed = isAllowedDashboardOrigin(origin);
|
|
151
|
+
callback(allowed ? null : new Error('origin_not_allowed'), allowed);
|
|
152
|
+
},
|
|
153
|
+
credentials: true,
|
|
154
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
155
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
156
|
+
}));
|
|
157
|
+
app.use(express.json({ limit: '2mb' }));
|
|
158
|
+
app.use((_req, res, next) => {
|
|
159
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
160
|
+
next();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
app.use(
|
|
164
|
+
(
|
|
165
|
+
err,
|
|
166
|
+
_req,
|
|
167
|
+
res,
|
|
168
|
+
_next, // eslint-disable-line no-unused-vars
|
|
169
|
+
) => {
|
|
170
|
+
const status = err?.status || err?.statusCode || 400;
|
|
171
|
+
res.status(status).json({
|
|
172
|
+
error: err?.type || 'bad_request',
|
|
173
|
+
message: err?.message || String(err),
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const state = createState({ projectRoot, opencodeConfigDir, bizarRoot });
|
|
179
|
+
|
|
180
|
+
// v3.0.4 — Auto-detect the user's cwd as a project on startup. This is
|
|
181
|
+
// idempotent and safe to call on every boot. The first time a user runs
|
|
182
|
+
// the dashboard, BizarHarness/ (or whatever their cwd is) shows up as
|
|
183
|
+
// a project without any manual setup.
|
|
184
|
+
try {
|
|
185
|
+
const detected = projectsStore.autoDetect({ cwd: projectRoot });
|
|
186
|
+
if (detected) {
|
|
187
|
+
// eslint-disable-next-line no-console
|
|
188
|
+
console.log(`[bizar-dash] auto-detected project: ${detected.id} (${detected.path})`);
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.error('[bizar-dash] autoDetect failed:', err.message);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// v3.6.0 — If the operator has configured a `dashboard.projectsDirectory`,
|
|
196
|
+
// also scan it for project roots on startup. Fire-and-forget: a slow
|
|
197
|
+
// scan or a missing directory must not block the server from booting.
|
|
198
|
+
//
|
|
199
|
+
// v3.11.0 — Rebuild the allow-list from settings (home +
|
|
200
|
+
// projectsDirectory + dashboard.allowedRoots), log it once for
|
|
201
|
+
// debuggability, and re-validate the configured `projectsDirectory`
|
|
202
|
+
// against the allow-list before scanning. A tampered settings file
|
|
203
|
+
// cannot widen the boundary.
|
|
204
|
+
try {
|
|
205
|
+
const settings = readSettings();
|
|
206
|
+
const allowedRoots = buildAllowedRootsFromSettings({
|
|
207
|
+
settings: settings.data,
|
|
208
|
+
home: homedir(),
|
|
209
|
+
});
|
|
210
|
+
// eslint-disable-next-line no-console
|
|
211
|
+
console.log(
|
|
212
|
+
`[bizar-dash] projects-directory scan: rebuilt allow-list (${allowedRoots.length} root(s))`,
|
|
213
|
+
);
|
|
214
|
+
const configured = settings.data?.dashboard?.projectsDirectory;
|
|
215
|
+
if (typeof configured === 'string' && configured.trim()) {
|
|
216
|
+
const safeRoot = resolveSafePath(configured, allowedRoots);
|
|
217
|
+
if (!safeRoot) {
|
|
218
|
+
// eslint-disable-next-line no-console
|
|
219
|
+
console.warn(
|
|
220
|
+
`[bizar-dash] projects-directory scan: skipping — ` +
|
|
221
|
+
`"${configured}" is outside the allow-list`,
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
projectsStore.scanDirectory(safeRoot).then(
|
|
225
|
+
(result) => {
|
|
226
|
+
if (result.error) {
|
|
227
|
+
// eslint-disable-next-line no-console
|
|
228
|
+
console.warn(`[bizar-dash] projects-directory scan: ${result.error}`);
|
|
229
|
+
} else if (result.added && result.added.length > 0) {
|
|
230
|
+
// eslint-disable-next-line no-console
|
|
231
|
+
console.log(
|
|
232
|
+
`[bizar-dash] projects-directory scan: added ${result.added.length} ` +
|
|
233
|
+
`project(s) (skipped ${result.skipped}, scanned ${result.scanned})`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
(err) => {
|
|
238
|
+
// eslint-disable-next-line no-console
|
|
239
|
+
console.warn(`[bizar-dash] projects-directory scan failed: ${err?.message || err}`);
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
// eslint-disable-next-line no-console
|
|
246
|
+
console.warn('[bizar-dash] startup scan setup failed:', err?.message || err);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const watchPaths = [
|
|
250
|
+
state.paths.opencodeJson,
|
|
251
|
+
state.paths.agentsDir,
|
|
252
|
+
state.paths.commandsDir,
|
|
253
|
+
state.paths.bizarDir,
|
|
254
|
+
state.paths.plansDir,
|
|
255
|
+
state.paths.globalPlansDir,
|
|
256
|
+
join(opencodeConfigDir, 'projects.json'),
|
|
257
|
+
].filter((p) => existsSafe(p));
|
|
258
|
+
|
|
259
|
+
const watcher = createWatcher({
|
|
260
|
+
paths: watchPaths,
|
|
261
|
+
onChange: (event, p) => {
|
|
262
|
+
wss.clients.forEach((client) => {
|
|
263
|
+
safeSend(client, JSON.stringify({ type: 'change', event, path: p, ts: Date.now() }));
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const server = createHttpServer(app);
|
|
269
|
+
server.requestTimeout = 30_000;
|
|
270
|
+
server.headersTimeout = 35_000;
|
|
271
|
+
server.keepAliveTimeout = 5_000;
|
|
272
|
+
app.set('port', port);
|
|
273
|
+
// v3.6.0 — Bind the WS server's noServer mode so we can authenticate
|
|
274
|
+
// the upgrade ourselves instead of letting ws accept any TCP
|
|
275
|
+
// connection to /ws. The actual upgrade handling happens further
|
|
276
|
+
// down via server.on('upgrade', ...).
|
|
277
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
|
|
278
|
+
|
|
279
|
+
function safeSend(client, payload) {
|
|
280
|
+
if (client.readyState !== 1) return false;
|
|
281
|
+
if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES) {
|
|
282
|
+
try {
|
|
283
|
+
client.terminate();
|
|
284
|
+
} catch {
|
|
285
|
+
/* ignore */
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
client.send(payload);
|
|
291
|
+
return true;
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function localBroadcast(msg) {
|
|
298
|
+
const payload = JSON.stringify(msg);
|
|
299
|
+
wss.clients.forEach((client) => {
|
|
300
|
+
safeSend(client, payload);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
currentBroadcast = localBroadcast;
|
|
304
|
+
|
|
305
|
+
const apiRouter = await createApiRouter({
|
|
306
|
+
state,
|
|
307
|
+
watcher,
|
|
308
|
+
projectRoot,
|
|
309
|
+
opencodeConfigDir,
|
|
310
|
+
bizarRoot,
|
|
311
|
+
broadcast: localBroadcast,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// All /api/* routes go through apiRouter (after mod routes are checked).
|
|
315
|
+
// IMPORTANT: mount v2 router FIRST so `/api/v2/*` matches before
|
|
316
|
+
// apiRouter's internal 404 catch-all (api.mjs line ~109) can swallow it.
|
|
317
|
+
// v2 plugin protocol
|
|
318
|
+
// ──────────────────────────────────────────────────────────────────
|
|
319
|
+
// HTTP+SSE bridge sourced from .bizar/research/OPENAPI_SPEC.yaml.
|
|
320
|
+
// Mounted at /api/v2/* to avoid colliding with existing /api/* routes.
|
|
321
|
+
// Auth: HTTP Basic (see v2-auth-file.mjs). Password persisted at
|
|
322
|
+
// ~/.cache/bizarharness/dash-auth.json (mode 0600).
|
|
323
|
+
try {
|
|
324
|
+
v2Auth = loadOrCreateAuth({ port: V2_DEFAULT_PORT });
|
|
325
|
+
v2Bus = new V2EventBus({ logger: console });
|
|
326
|
+
// eslint-disable-next-line no-console
|
|
327
|
+
console.log(
|
|
328
|
+
`[v2] auth file: ${v2Auth.file} (port ${v2Auth.port}, password len ${v2Auth.password.length})`,
|
|
329
|
+
);
|
|
330
|
+
app.use(
|
|
331
|
+
'/api/v2',
|
|
332
|
+
createV2Router({
|
|
333
|
+
eventBus: v2Bus,
|
|
334
|
+
getPassword: () => v2Auth.password,
|
|
335
|
+
version: '0.7.0-alpha.1',
|
|
336
|
+
startedAt: Date.now(),
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// eslint-disable-next-line no-console
|
|
341
|
+
console.warn('[v2] failed to initialize:', err?.message || err);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// v3.6.2 — Auth now wraps mod routes too. Previously mods mounted at
|
|
345
|
+
// `/api/mods/<id>` BEFORE `requireAuth`, which meant every mod route
|
|
346
|
+
// silently bypassed bearer-token checks.
|
|
347
|
+
const { requireAuth } = await import('./auth.mjs');
|
|
348
|
+
app.use('/api', requireAuth({ skipPaths: ['/auth/status', '/pair/verify', '/v2/health', '/v2/doc'] }));
|
|
349
|
+
|
|
350
|
+
// ── Mod route mounting ──────────────────────────────────────────────
|
|
351
|
+
{
|
|
352
|
+
const modCtx = { broadcast: localBroadcast, state, projectRoot, opencodeConfigDir };
|
|
353
|
+
const modRouters = await modsLoader.loadModRouters(modCtx);
|
|
354
|
+
for (const { id, router: modRouter, mountPath } of modRouters) {
|
|
355
|
+
app.use(mountPath, modRouter);
|
|
356
|
+
// eslint-disable-next-line no-console
|
|
357
|
+
console.log(`[mod] mounted ${id} routes at ${mountPath}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// All /api/* routes go through apiRouter (after mod routes are checked)
|
|
362
|
+
app.use('/api', apiRouter);
|
|
363
|
+
|
|
364
|
+
// v3.6.0 — Authenticate WebSocket upgrades. The wss is in
|
|
365
|
+
// `noServer: true` mode above, so this handler is the gate. We
|
|
366
|
+
// reject with 401 if the token is missing/invalid; otherwise we
|
|
367
|
+
// hand off to ws.handleUpgrade so the existing wss.on('connection')
|
|
368
|
+
// listener below receives the new socket.
|
|
369
|
+
server.on('upgrade', (req, socket, head) => {
|
|
370
|
+
const url = req.url || '';
|
|
371
|
+
if (!url.startsWith('/ws')) {
|
|
372
|
+
socket.destroy();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!isAllowedDashboardOriginForRequest(req)) {
|
|
376
|
+
try {
|
|
377
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
378
|
+
socket.destroy();
|
|
379
|
+
} catch {
|
|
380
|
+
/* ignore */
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!checkWebSocketAuth(req)) {
|
|
385
|
+
try {
|
|
386
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
387
|
+
socket.destroy();
|
|
388
|
+
} catch {
|
|
389
|
+
/* ignore */
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
394
|
+
wss.emit('connection', ws, req);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const heartbeatInterval = setInterval(() => {
|
|
399
|
+
wss.clients.forEach((client) => {
|
|
400
|
+
if (client.isAlive === false) {
|
|
401
|
+
try {
|
|
402
|
+
client.terminate();
|
|
403
|
+
} catch {
|
|
404
|
+
/* ignore */
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
client.isAlive = false;
|
|
409
|
+
try {
|
|
410
|
+
client.ping();
|
|
411
|
+
} catch {
|
|
412
|
+
try {
|
|
413
|
+
client.terminate();
|
|
414
|
+
} catch {
|
|
415
|
+
/* ignore */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}, 30_000);
|
|
420
|
+
if (typeof heartbeatInterval.unref === 'function') heartbeatInterval.unref();
|
|
421
|
+
|
|
422
|
+
// ── Static frontend (React SPA in dist/) ─────────────────────────
|
|
423
|
+
const distBuilt =
|
|
424
|
+
existsSync(DIST_DIR) && existsSync(join(DIST_DIR, 'index.html'));
|
|
425
|
+
|
|
426
|
+
if (distBuilt) {
|
|
427
|
+
const assetsDir = join(DIST_DIR, 'assets');
|
|
428
|
+
if (existsSync(assetsDir)) {
|
|
429
|
+
app.use(
|
|
430
|
+
'/assets',
|
|
431
|
+
express.static(assetsDir, { maxAge: '1y', immutable: true, index: false }),
|
|
432
|
+
);
|
|
433
|
+
// v3.23.0 — Serve the mobile CSS at a stable /mobile.css URL so the
|
|
434
|
+
// desktop index.html can link to it without needing to know the hash.
|
|
435
|
+
// MobileApp renders the mobile shell; without its CSS the user sees
|
|
436
|
+
// unformatted HTML. The actual file is named mobile-<hash>.css; we
|
|
437
|
+
// resolve it at startup so the URL is stable.
|
|
438
|
+
try {
|
|
439
|
+
const mobileCss = readdirSync(assetsDir).find((f) => /^mobile-.*\.css$/.test(f));
|
|
440
|
+
if (mobileCss) {
|
|
441
|
+
app.get('/mobile.css', (_req, res) => {
|
|
442
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
443
|
+
res.sendFile(join(assetsDir, mobileCss));
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
} catch { /* ignore — mobile.css won't be served, fine */ }
|
|
447
|
+
app.get('/', (req, res, next) => {
|
|
448
|
+
if (!shouldRedirectToMobile(req)) {
|
|
449
|
+
next();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
res.redirect(302, mobileRedirectTarget(req));
|
|
453
|
+
});
|
|
454
|
+
// v3.5.0 — Mobile dashboard at /m
|
|
455
|
+
app.get('/m', (_req, res) => {
|
|
456
|
+
res.sendFile(join(DIST_DIR, 'mobile.html'));
|
|
457
|
+
});
|
|
458
|
+
// SPA fallback for /m/* — but ONLY for HTML navigation requests
|
|
459
|
+
// The negative lookahead (?!assets/) excludes asset paths so they fall
|
|
460
|
+
// through to the static /assets/* handler above
|
|
461
|
+
app.get(/^\/m\/(?!assets\/)/, (_req, res) => {
|
|
462
|
+
res.sendFile(join(DIST_DIR, 'mobile.html'));
|
|
463
|
+
});
|
|
464
|
+
app.use(
|
|
465
|
+
express.static(DIST_DIR, {
|
|
466
|
+
extensions: ['html'],
|
|
467
|
+
setHeaders: (res, filePath) => {
|
|
468
|
+
if (filePath.endsWith('index.html') || filePath.endsWith('mobile.html')) {
|
|
469
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
app.get('*', (req, res, next) => {
|
|
475
|
+
if (req.path.startsWith('/api') || req.path === '/ws') return next();
|
|
476
|
+
res.sendFile(join(DIST_DIR, 'index.html'));
|
|
477
|
+
});
|
|
478
|
+
} else {
|
|
479
|
+
const distIndex = join(DIST_DIR, 'index.html');
|
|
480
|
+
if (!existsSync(distIndex)) {
|
|
481
|
+
// eslint-disable-next-line no-console
|
|
482
|
+
console.error(`[bizar-dash] dist/index.html not found at ${DIST_DIR}`);
|
|
483
|
+
// eslint-disable-next-line no-console
|
|
484
|
+
console.error(`[bizar-dash] The published package should include a prebuilt dist/.`);
|
|
485
|
+
// eslint-disable-next-line no-console
|
|
486
|
+
console.error(`[bizar-dash] Try: npm install -g @polderlabs/bizar-dash --force`);
|
|
487
|
+
}
|
|
488
|
+
app.get('/', (_req, res) => res.status(503).type('html').send(renderNotBuiltPage()));
|
|
489
|
+
app.get('*', (_req, res) => {
|
|
490
|
+
if (_req.path.startsWith('/api') || _req.path === '/ws') {
|
|
491
|
+
res.status(404).json({ error: 'not_found', message: `no route for ${_req.method} ${_req.originalUrl}` });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
res.status(503).type('html').send(renderNotBuiltPage());
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
wss.on('connection', (ws, req) => {
|
|
499
|
+
ws.isAlive = true;
|
|
500
|
+
ws.on('pong', () => {
|
|
501
|
+
ws.isAlive = true;
|
|
502
|
+
});
|
|
503
|
+
const path = req.url
|
|
504
|
+
? (() => {
|
|
505
|
+
try {
|
|
506
|
+
return new URL(req.url, 'http://localhost').pathname;
|
|
507
|
+
} catch {
|
|
508
|
+
return req.url || '';
|
|
509
|
+
}
|
|
510
|
+
})()
|
|
511
|
+
: '';
|
|
512
|
+
|
|
513
|
+
// /ws/logs — stream log file changes
|
|
514
|
+
if (path === '/ws/logs') {
|
|
515
|
+
const HOME = homedir();
|
|
516
|
+
const serviceLog = join(HOME, '.config', 'bizar', 'service.log');
|
|
517
|
+
const dashboardLog = join(HOME, '.config', 'bizar', 'dashboard.log');
|
|
518
|
+
const logFile = existsSync(serviceLog) ? serviceLog : existsSync(dashboardLog) ? dashboardLog : null;
|
|
519
|
+
|
|
520
|
+
let fileSize = logFile && existsSync(logFile) ? statSync(logFile).size : 0;
|
|
521
|
+
|
|
522
|
+
// Send initial tail
|
|
523
|
+
if (logFile) {
|
|
524
|
+
try {
|
|
525
|
+
const text = readFileSync(logFile, 'utf8');
|
|
526
|
+
const lines = text.split(/\r?\n/).filter(Boolean).slice(-100);
|
|
527
|
+
safeSend(ws, JSON.stringify({ type: 'log init', lines, file: logFile }));
|
|
528
|
+
} catch {
|
|
529
|
+
safeSend(ws, JSON.stringify({ type: 'log init', lines: [], file: logFile }));
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
safeSend(ws, JSON.stringify({ type: 'log init', lines: [], file: null }));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
let destroying = false;
|
|
536
|
+
function sendLogChunk() {
|
|
537
|
+
if (destroying || ws.readyState !== 1) return;
|
|
538
|
+
try {
|
|
539
|
+
const f = logFile;
|
|
540
|
+
if (!f || !existsSync(f)) return;
|
|
541
|
+
const newSize = statSync(f).size;
|
|
542
|
+
// Reset if the file was rotated/truncated (size shrank).
|
|
543
|
+
if (newSize < fileSize) {
|
|
544
|
+
fileSize = 0;
|
|
545
|
+
}
|
|
546
|
+
if (newSize > fileSize) {
|
|
547
|
+
// Read only the new bytes
|
|
548
|
+
const fd = openSync(f, 'r');
|
|
549
|
+
const buf = Buffer.alloc(newSize - fileSize);
|
|
550
|
+
readSync(fd, buf, 0, buf.length, fileSize);
|
|
551
|
+
closeSync(fd);
|
|
552
|
+
const newText = buf.toString('utf8');
|
|
553
|
+
const newLines = newText.split(/\r?\n/).filter(Boolean);
|
|
554
|
+
for (const line of newLines) {
|
|
555
|
+
safeSend(ws, JSON.stringify({ type: 'log line', line, ts: Date.now() }));
|
|
556
|
+
}
|
|
557
|
+
fileSize = newSize;
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
/* ignore */
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const interval = setInterval(sendLogChunk, 1000);
|
|
565
|
+
|
|
566
|
+
ws.on('message', (raw) => {
|
|
567
|
+
try {
|
|
568
|
+
const msg = JSON.parse(raw.toString());
|
|
569
|
+
if (msg?.type === 'ping') {
|
|
570
|
+
safeSend(ws, JSON.stringify({ type: 'pong', ts: Date.now() }));
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
573
|
+
/* ignore */
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
ws.on('close', () => {
|
|
578
|
+
destroying = true;
|
|
579
|
+
clearInterval(interval);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
ws.on('error', () => {
|
|
583
|
+
destroying = true;
|
|
584
|
+
clearInterval(interval);
|
|
585
|
+
});
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Default /ws — snapshot + ping/pong
|
|
590
|
+
try {
|
|
591
|
+
safeSend(
|
|
592
|
+
ws,
|
|
593
|
+
JSON.stringify({
|
|
594
|
+
type: 'snapshot',
|
|
595
|
+
ts: Date.now(),
|
|
596
|
+
data: buildSnapshotSafe(state, opencodeConfigDir),
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
} catch {
|
|
600
|
+
/* ignore */
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
ws.on('message', (raw) => {
|
|
604
|
+
let msg;
|
|
605
|
+
try {
|
|
606
|
+
msg = JSON.parse(raw.toString());
|
|
607
|
+
} catch {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (msg?.type === 'ping') {
|
|
611
|
+
try {
|
|
612
|
+
safeSend(ws, JSON.stringify({ type: 'pong', ts: Date.now() }));
|
|
613
|
+
} catch {
|
|
614
|
+
/* ignore */
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
watcher.start();
|
|
621
|
+
|
|
622
|
+
// v3.5.5 — Bridge bg state file changes into task status updates +
|
|
623
|
+
// artifact auto-detection. Started after the watcher so the WS
|
|
624
|
+
// broadcast channel is fully wired by the time the first tick
|
|
625
|
+
// runs. Idempotent — calling startBgPoller twice is a no-op.
|
|
626
|
+
try {
|
|
627
|
+
startBgPoller();
|
|
628
|
+
} catch (err) {
|
|
629
|
+
// eslint-disable-next-line no-console
|
|
630
|
+
console.error('[bizar-dash] failed to start bg-poller:', err.message);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// v3.11.0 — Periodic recovery for bg instances stuck in
|
|
634
|
+
// `dispatchPending: true` with `toolCallCount === 0`. Before this
|
|
635
|
+
// existed, the only unstick path was the user manually hitting
|
|
636
|
+
// `POST /api/tasks/:id/start`. The retry loop walks every bg
|
|
637
|
+
// state file every 30s, attempts to re-dispatch via the opencode
|
|
638
|
+
// serve child, and caps each instance at MAX_DISPATCH_RETRIES
|
|
639
|
+
// (10) before marking it `failed`.
|
|
640
|
+
try {
|
|
641
|
+
startBgRetryLoop();
|
|
642
|
+
} catch (err) {
|
|
643
|
+
// eslint-disable-next-line no-console
|
|
644
|
+
console.error('[bizar-dash] failed to start bg-retry loop:', err.message);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// v0.5.1 — Poll for dialog descriptors written by the plugin and
|
|
648
|
+
// broadcast them to connected dashboard clients via WS.
|
|
649
|
+
// Idempotent — calling startDialogPoller twice is a no-op.
|
|
650
|
+
try {
|
|
651
|
+
startDialogPoller();
|
|
652
|
+
} catch (err) {
|
|
653
|
+
// eslint-disable-next-line no-console
|
|
654
|
+
console.error('[bizar-dash] failed to start dialog-poller:', err.message);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function close() {
|
|
658
|
+
try {
|
|
659
|
+
stopBgPoller();
|
|
660
|
+
} catch {
|
|
661
|
+
/* ignore */
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
stopBgRetryLoop();
|
|
665
|
+
} catch {
|
|
666
|
+
/* ignore */
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
watcher.stop();
|
|
670
|
+
} catch {
|
|
671
|
+
/* ignore */
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
wss.clients.forEach((c) => c.terminate());
|
|
675
|
+
wss.close();
|
|
676
|
+
} catch {
|
|
677
|
+
/* ignore */
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
clearInterval(heartbeatInterval);
|
|
681
|
+
} catch {
|
|
682
|
+
/* ignore */
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
server.close();
|
|
686
|
+
} catch {
|
|
687
|
+
/* ignore */
|
|
688
|
+
}
|
|
689
|
+
currentBroadcast = () => {};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return { app, server, wss, state, watcher, port, close };
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function buildSnapshotSafe(state, opencodeConfigDir) {
|
|
696
|
+
try {
|
|
697
|
+
return buildSnapshot(state, opencodeConfigDir);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
return { error: 'snapshot_failed', message: err.message };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function buildSnapshot(state, opencodeConfigDir) {
|
|
704
|
+
const cfgFile = join(opencodeConfigDir, 'opencode.json');
|
|
705
|
+
let cfg = null;
|
|
706
|
+
if (existsSync(cfgFile)) {
|
|
707
|
+
try {
|
|
708
|
+
cfg = JSON.parse(readFileSync(cfgFile, 'utf8'));
|
|
709
|
+
} catch {
|
|
710
|
+
cfg = null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const activeProject = projectsStore.active();
|
|
714
|
+
return {
|
|
715
|
+
overview: state.getOverview(),
|
|
716
|
+
agents: agentsStore.list(),
|
|
717
|
+
artifacts: state.getArtifacts(),
|
|
718
|
+
projects: projectsStore.list().projects,
|
|
719
|
+
activeProject,
|
|
720
|
+
config: {
|
|
721
|
+
path: cfgFile,
|
|
722
|
+
data: cfg,
|
|
723
|
+
raw: cfg ? JSON.stringify(cfg, null, 2) : '',
|
|
724
|
+
exists: existsSync(cfgFile),
|
|
725
|
+
},
|
|
726
|
+
settings: readSettings(),
|
|
727
|
+
tasks: activeProject ? tasksStore.loadTasks(activeProject.id) : [],
|
|
728
|
+
mods: modsLoader.list(),
|
|
729
|
+
schedules: activeProject ? schedulesStore.list(activeProject.id) : [],
|
|
730
|
+
providers: providersStore.list(),
|
|
731
|
+
mcps: mcpsStore.list(),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function existsSafe(p) {
|
|
736
|
+
try {
|
|
737
|
+
return existsSync(p) || existsSync(dirnameSafe(p));
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function dirnameSafe(p) {
|
|
744
|
+
const idx = p.lastIndexOf('/');
|
|
745
|
+
return idx === -1 ? '.' : p.slice(0, idx);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function renderNotBuiltPage() {
|
|
749
|
+
return `<!DOCTYPE html>
|
|
750
|
+
<html lang="en">
|
|
751
|
+
<head>
|
|
752
|
+
<meta charset="UTF-8" />
|
|
753
|
+
<title>Dashboard assets not found</title>
|
|
754
|
+
<style>
|
|
755
|
+
:root { color-scheme: dark; }
|
|
756
|
+
body {
|
|
757
|
+
margin: 0; min-height: 100vh; display: flex; align-items: center;
|
|
758
|
+
justify-content: center; background: #0b0e14; color: #c9d1d9;
|
|
759
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
760
|
+
padding: 24px;
|
|
761
|
+
}
|
|
762
|
+
.card {
|
|
763
|
+
max-width: 560px; background: #12161f; border: 1px solid #f87171;
|
|
764
|
+
border-radius: 12px; padding: 32px;
|
|
765
|
+
}
|
|
766
|
+
h1 { margin: 0 0 12px; color: #f87171; font-size: 22px; }
|
|
767
|
+
p { margin: 0 0 12px; line-height: 1.6; }
|
|
768
|
+
code {
|
|
769
|
+
font-family: 'JetBrains Mono', monospace; background: #1a1f2b;
|
|
770
|
+
border: 1px solid #232a39; padding: 2px 6px; border-radius: 4px;
|
|
771
|
+
font-size: 13px;
|
|
772
|
+
}
|
|
773
|
+
pre {
|
|
774
|
+
background: #1a1f2b; border: 1px solid #232a39; padding: 12px 16px;
|
|
775
|
+
border-radius: 8px; font-family: 'JetBrains Mono', monospace;
|
|
776
|
+
font-size: 13px; overflow-x: auto;
|
|
777
|
+
}
|
|
778
|
+
</style>
|
|
779
|
+
</head>
|
|
780
|
+
<body>
|
|
781
|
+
<div class="card">
|
|
782
|
+
<h1>Dashboard assets not found</h1>
|
|
783
|
+
<p>The <code>@polderlabs/bizar-dash</code> package ships prebuilt assets
|
|
784
|
+
in <code>dist/</code>. If you are seeing this, your install is broken.</p>
|
|
785
|
+
<p>Try:</p>
|
|
786
|
+
<pre><code>npm install -g @polderlabs/bizar-dash --force</code></pre>
|
|
787
|
+
<p>If the problem persists, file an issue at<br/>
|
|
788
|
+
<code>github.com/DrB0rk/BizarHarness</code></p>
|
|
789
|
+
<p>The REST API and WebSocket are still live at <code>/api/*</code>
|
|
790
|
+
and <code>/ws</code>.</p>
|
|
791
|
+
</div>
|
|
792
|
+
</body>
|
|
793
|
+
</html>`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export { DIST_DIR, renderNotBuiltPage };
|