@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,522 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* src/cli.mjs
|
|
4
|
+
*
|
|
5
|
+
* v3.0.0 — `bizar-dash` command.
|
|
6
|
+
*
|
|
7
|
+
* v3.20.7 — Added `cleanup` subcommand and duplicate-dashboard detection.
|
|
8
|
+
* Running `bizar dash start` while another dashboard (on any port in
|
|
9
|
+
* the default range) is already serving will refuse and point at
|
|
10
|
+
* `cleanup` unless `--force` is given. Solves the "dashboard on
|
|
11
|
+
* port 4321 won't go away" zombie problem.
|
|
12
|
+
*
|
|
13
|
+
* Subcommands:
|
|
14
|
+
* start — launch dashboard (default)
|
|
15
|
+
* stop — kill the running dashboard
|
|
16
|
+
* status — print port + URL of any running dashboard
|
|
17
|
+
* tui — run the TUI dashboard
|
|
18
|
+
* cleanup — find and kill zombie / orphan dashboards (keeps canonical)
|
|
19
|
+
* --no-web — TUI only (no browser)
|
|
20
|
+
* --web-only — web only
|
|
21
|
+
* --bg, --detach — start in background and return
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
import { dirname } from 'node:path';
|
|
29
|
+
import {
|
|
30
|
+
findAllDashboards,
|
|
31
|
+
killDashboard,
|
|
32
|
+
clearCanonicalFiles,
|
|
33
|
+
waitForExit,
|
|
34
|
+
summarize,
|
|
35
|
+
} from './cli/dashboard-ports.mjs';
|
|
36
|
+
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
38
|
+
const __dirname = dirname(__filename);
|
|
39
|
+
// Use %APPDATA%\bizar on Windows, ~/.config/bizar on Unix.
|
|
40
|
+
const BIZAR_HOME = process.platform === 'win32'
|
|
41
|
+
? join(process.env.APPDATA || homedir(), 'bizar')
|
|
42
|
+
: join(homedir(), '.config', 'bizar');
|
|
43
|
+
const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
44
|
+
const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
45
|
+
const DEFAULT_PORT = 4321;
|
|
46
|
+
|
|
47
|
+
function readVersion() {
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
50
|
+
return pkg?.version || '0.0.0';
|
|
51
|
+
} catch {
|
|
52
|
+
return '0.0.0';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function showHelp() {
|
|
57
|
+
console.log(`
|
|
58
|
+
bizar — Web + TUI dashboard for the Bizar agent platform
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
bizar Launch the dashboard (default = web)
|
|
62
|
+
bizar start Start the dashboard in this process
|
|
63
|
+
bizar stop Kill the running dashboard
|
|
64
|
+
bizar status Show port + URL of any running dashboard
|
|
65
|
+
bizar cleanup Find and kill zombie / orphan dashboards
|
|
66
|
+
bizar tui [--no-web] Run the TUI dashboard
|
|
67
|
+
bizar --bg, --detach Start in background, return to shell
|
|
68
|
+
bizar --web-only Web dashboard only (no TUI)
|
|
69
|
+
bizar --no-web TUI only (no browser)
|
|
70
|
+
bizar --help Show this help
|
|
71
|
+
|
|
72
|
+
Notes:
|
|
73
|
+
The dashboard reads your opencode config at
|
|
74
|
+
~/.config/opencode/. The per-project state lives in
|
|
75
|
+
~/.config/opencode/projects/<id>/. Mods are installed to
|
|
76
|
+
~/.config/bizar/mods/.
|
|
77
|
+
|
|
78
|
+
Install:
|
|
79
|
+
npm install -g @polderlabs/bizar-dash
|
|
80
|
+
npm install -g @polderlabs/bizar # required peer
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function findFreePort(preferred) {
|
|
85
|
+
const net = await import('node:net');
|
|
86
|
+
for (let p = preferred; p < preferred + 100; p++) {
|
|
87
|
+
if (await isPortFree(net, p)) return p;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`no free port in range ${preferred}..${preferred + 99}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPortFree(net, port) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const server = net.createServer();
|
|
95
|
+
let settled = false;
|
|
96
|
+
const finish = (ok) => {
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
resolve(ok);
|
|
100
|
+
};
|
|
101
|
+
server.once('error', () => finish(false));
|
|
102
|
+
server.once('listening', () => server.close(() => finish(true)));
|
|
103
|
+
const t = setTimeout(() => finish(false), 1000);
|
|
104
|
+
server.listen(port, '127.0.0.1', () => clearTimeout(t));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function startDashboard({ port, projectRoot, opencodeConfigDir, bizarRoot, bg = false } = {}) {
|
|
109
|
+
const { createServer } = await import('./server/server.mjs');
|
|
110
|
+
const { launchBrowser } = await import('./server/browser.mjs');
|
|
111
|
+
|
|
112
|
+
// v3.20.7 — Detect duplicate dashboards BEFORE binding the port.
|
|
113
|
+
// If another dashboard is already serving (on any port in the
|
|
114
|
+
// default range), refuse to start unless --force. This prevents
|
|
115
|
+
// the "two dashboards, one is a zombie" deadlock.
|
|
116
|
+
const force = process.argv.includes('--force') || process.argv.includes('-f');
|
|
117
|
+
if (!force) {
|
|
118
|
+
const existing = await findAllDashboards();
|
|
119
|
+
const others = existing.filter((i) => i.state === 'healthy' && i.pid !== process.pid);
|
|
120
|
+
if (others.length > 0) {
|
|
121
|
+
console.error(' ✗ Another Bizar dashboard is already running.');
|
|
122
|
+
console.error('');
|
|
123
|
+
for (const inst of others) {
|
|
124
|
+
const port = inst.port ? `:${inst.port}` : '';
|
|
125
|
+
console.error(` pid ${inst.pid}${port} [${inst.state}] ${inst.cmdline.slice(0, 80)}`);
|
|
126
|
+
}
|
|
127
|
+
console.error('');
|
|
128
|
+
console.error(' To replace it, run `bizar dash cleanup` first, or pass');
|
|
129
|
+
console.error(' --force to skip this check.');
|
|
130
|
+
console.error(' To stop the canonical one, use `bizar dash stop`.');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const usePort = port || (await findFreePort(DEFAULT_PORT));
|
|
136
|
+
const { server, close } = await createServer({
|
|
137
|
+
port: usePort,
|
|
138
|
+
projectRoot: projectRoot || process.cwd(),
|
|
139
|
+
opencodeConfigDir: opencodeConfigDir || (
|
|
140
|
+
process.platform === 'win32'
|
|
141
|
+
? join(process.env.APPDATA || homedir(), 'opencode')
|
|
142
|
+
: join(homedir(), '.config', 'opencode')
|
|
143
|
+
),
|
|
144
|
+
bizarRoot: bizarRoot || join(__dirname, '..', '..'),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// v3.6.0 — Default bind is localhost (more secure). Operators
|
|
148
|
+
// exposing the dashboard over Tailscale or LAN can override with
|
|
149
|
+
// BIZAR_DASHBOARD_BIND=0.0.0.0 (the auth.mjs token will then gate
|
|
150
|
+
// the surface).
|
|
151
|
+
const bindHost = process.env.BIZAR_DASHBOARD_BIND || '127.0.0.1';
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
server.once('error', reject);
|
|
154
|
+
server.listen(usePort, bindHost, () => {
|
|
155
|
+
server.off('error', reject);
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
161
|
+
writeFileSync(PORT_FILE, String(usePort), 'utf8');
|
|
162
|
+
writeFileSync(PID_FILE, String(process.pid), 'utf8');
|
|
163
|
+
|
|
164
|
+
// v3.6.0 — Surface the bind in the URL hint so the operator can see
|
|
165
|
+
// whether they're bound to localhost only or to all interfaces.
|
|
166
|
+
const url = bindHost === '127.0.0.1' || bindHost === 'localhost'
|
|
167
|
+
? `http://localhost:${usePort}/`
|
|
168
|
+
: `http://${bindHost}:${usePort}/`;
|
|
169
|
+
console.log(`Bizar dashboard: ${url} (bind: ${bindHost})`);
|
|
170
|
+
if (!bg) {
|
|
171
|
+
// Foreground mode: try to launch the browser; then block on a
|
|
172
|
+
// signal so the process keeps running until the operator hits
|
|
173
|
+
// Ctrl-C (or `bizar stop`).
|
|
174
|
+
launchBrowser(url).catch(() => {});
|
|
175
|
+
console.log('Press Ctrl-C to stop the dashboard.');
|
|
176
|
+
let shuttingDown = false;
|
|
177
|
+
const cleanup = () => {
|
|
178
|
+
if (shuttingDown) return;
|
|
179
|
+
shuttingDown = true;
|
|
180
|
+
try { close(); } catch { /* ignore */ }
|
|
181
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
182
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
183
|
+
};
|
|
184
|
+
const handleSignal = () => {
|
|
185
|
+
cleanup();
|
|
186
|
+
process.exit(0);
|
|
187
|
+
};
|
|
188
|
+
process.once('SIGINT', handleSignal);
|
|
189
|
+
process.once('SIGTERM', handleSignal);
|
|
190
|
+
await new Promise(() => {});
|
|
191
|
+
return { url, port: usePort, close };
|
|
192
|
+
}
|
|
193
|
+
// Background mode: do NOT launch a browser (the caller already detached),
|
|
194
|
+
// do NOT block on a signal. The caller (detachAfterBoot) decides how to
|
|
195
|
+
// keep this process alive (or not) once we return.
|
|
196
|
+
return { url, port: usePort, close };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Called after startDashboard returns in background mode. The server is
|
|
201
|
+
* listening and the PID/PORT files are written. We intentionally keep
|
|
202
|
+
* the process alive (a Node process with nothing keeping it busy exits
|
|
203
|
+
* once the event loop is empty), but detach stdio and ignore signals so
|
|
204
|
+
* the launching shell can exit cleanly while the dashboard keeps
|
|
205
|
+
* serving.
|
|
206
|
+
*/
|
|
207
|
+
function detachAfterBoot() {
|
|
208
|
+
console.log(`Detached. Use 'bizar dash stop' to terminate, 'bizar dash status' to check.`);
|
|
209
|
+
// Don't exit — the server keeps the event loop busy. But stop
|
|
210
|
+
// responding to stdin so the parent shell can close.
|
|
211
|
+
if (process.stdin && process.stdin.pause) process.stdin.pause();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function startInBackground(args) {
|
|
215
|
+
const binPath = join(__dirname, 'cli.mjs');
|
|
216
|
+
const child = spawn(process.execPath, [binPath, ...args], {
|
|
217
|
+
detached: true,
|
|
218
|
+
stdio: 'ignore',
|
|
219
|
+
cwd: process.cwd(),
|
|
220
|
+
env: process.env,
|
|
221
|
+
});
|
|
222
|
+
child.on('error', (err) => {
|
|
223
|
+
console.error(`Failed to start background dashboard: ${err.message}`);
|
|
224
|
+
});
|
|
225
|
+
child.unref();
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
227
|
+
if (existsSync(PORT_FILE)) {
|
|
228
|
+
const port = readFileSync(PORT_FILE, 'utf8').trim();
|
|
229
|
+
console.log(`Bizar dashboard started in background on http://localhost:${port}/`);
|
|
230
|
+
console.log(`Use 'bizar status' to check, 'bizar stop' to stop.`);
|
|
231
|
+
} else {
|
|
232
|
+
console.log('Bizar dashboard starting in background (port file not yet written)...');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* v3.20.7 — Find and kill every non-canonical dashboard. Keeps the
|
|
238
|
+
* canonical one (PID file) intact. Useful when a previous start left
|
|
239
|
+
* a zombie behind, or when an old dashboard on a different port is
|
|
240
|
+
* still serving.
|
|
241
|
+
*/
|
|
242
|
+
async function cleanupDashboards({ force = false, signal = 'SIGTERM' } = {}) {
|
|
243
|
+
const instances = await findAllDashboards();
|
|
244
|
+
if (instances.length === 0) {
|
|
245
|
+
console.log('No dashboards detected.');
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
console.log(summarize(instances));
|
|
249
|
+
console.log('');
|
|
250
|
+
|
|
251
|
+
// Strategy (v3.20.14):
|
|
252
|
+
// - Always kill zombies (PID alive + cmdline looks like dashboard,
|
|
253
|
+
// but port doesn't respond to /api/health).
|
|
254
|
+
// - Always kill orphans (port responds but no PID claimed it).
|
|
255
|
+
// - Kill healthy non-canonical dashboards UNLESS the canonical PID
|
|
256
|
+
// is alive and matches the canonical PID file. In that case the
|
|
257
|
+
// user wants the canonical one running and the others are duplicates.
|
|
258
|
+
// - Never kill the canonical PID.
|
|
259
|
+
//
|
|
260
|
+
// Previous bug: cleanup refused to kill `healthy` non-canonical
|
|
261
|
+
// dashboards even when the canonical PID file was dead or recycled.
|
|
262
|
+
// This left orphaned dashboards running that `bizar dash stop`
|
|
263
|
+
// couldn't reach (it only knows the canonical PID file).
|
|
264
|
+
const canonical = instances.find((i) => i.isCanonical && i.pid);
|
|
265
|
+
const canonicalIsAlive = canonical && existsSync(`/proc/${canonical.pid}`);
|
|
266
|
+
const targets = instances.filter((i) => {
|
|
267
|
+
if (i.pid && i.pid === canonical?.pid) return false; // never kill canonical
|
|
268
|
+
if (i.state === 'zombie' || i.state === 'orphan') return true;
|
|
269
|
+
if (i.state === 'dead') return true; // dead PIDs just need their canonical files cleared
|
|
270
|
+
if (i.state === 'healthy' && !i.isCanonical) {
|
|
271
|
+
// If the canonical PID is alive, this is a duplicate — only kill
|
|
272
|
+
// with --force (user confirmed they want to replace it).
|
|
273
|
+
// If the canonical PID is dead/recycled, the PID file is stale;
|
|
274
|
+
// there's no real "canonical" dashboard to preserve, so kill
|
|
275
|
+
// this one to unblock the user.
|
|
276
|
+
return force || !canonicalIsAlive;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (targets.length === 0) {
|
|
282
|
+
if (canonical && !canonicalIsAlive) {
|
|
283
|
+
// The canonical PID is gone — clear the stale files even though
|
|
284
|
+
// there's nothing else to clean up.
|
|
285
|
+
console.log(`Canonical PID ${canonical.pid} is dead or recycled — clearing stale PID/port files.`);
|
|
286
|
+
clearCanonicalFiles();
|
|
287
|
+
} else {
|
|
288
|
+
console.log('Nothing to clean up. Use --force to also kill healthy non-canonical duplicates.');
|
|
289
|
+
}
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let killed = 0;
|
|
294
|
+
for (const inst of targets) {
|
|
295
|
+
if (inst.pid) {
|
|
296
|
+
console.log(` Killing pid ${inst.pid} (${inst.state}, port ${inst.port || '?'}) — ${signal}`);
|
|
297
|
+
const ok = killDashboard(inst.pid, signal);
|
|
298
|
+
if (ok) killed += 1;
|
|
299
|
+
} else if (inst.state === 'orphan') {
|
|
300
|
+
// No PID — port is held by a process we don't know. Tell the user.
|
|
301
|
+
console.log(` Orphan on port ${inst.port}: no PID file claims it. Use 'lsof -i :${inst.port}' or 'fuser ${inst.port}/tcp' to find the owner.`);
|
|
302
|
+
} else if (inst.state === 'dead') {
|
|
303
|
+
console.log(` Dead PID ${inst.pid} — clearing stale port/pid files.`);
|
|
304
|
+
clearCanonicalFiles(inst.pid);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// If we sent SIGTERM, wait briefly for graceful exit then SIGKILL stragglers.
|
|
309
|
+
if (signal === 'SIGTERM' && killed > 0) {
|
|
310
|
+
const pids = targets.filter((t) => t.pid).map((t) => t.pid);
|
|
311
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
312
|
+
for (const pid of pids) {
|
|
313
|
+
try {
|
|
314
|
+
process.kill(pid, 0); // still alive?
|
|
315
|
+
// Yes — escalate to SIGKILL
|
|
316
|
+
console.log(` Escalating to SIGKILL for pid ${pid}`);
|
|
317
|
+
killDashboard(pid, 'SIGKILL');
|
|
318
|
+
} catch {
|
|
319
|
+
// already exited — good
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Clear canonical files if the canonical PID is dead.
|
|
325
|
+
if (canonical && !existsSync(`/proc/${canonical.pid}`)) {
|
|
326
|
+
console.log(`Canonical PID ${canonical.pid} is dead — clearing stale port file.`);
|
|
327
|
+
clearCanonicalFiles();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log(`Cleaned up ${killed} dashboard${killed === 1 ? '' : 's'}.`);
|
|
332
|
+
return killed;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function stopDashboard() {
|
|
336
|
+
// v3.20.14: scan for any running dashboard, not just the one in
|
|
337
|
+
// the PID file. If the PID file points to a dead/recycled PID but
|
|
338
|
+
// ps-scan finds a real dashboard (started by a different user or
|
|
339
|
+
// after the PID file was deleted), kill it. Otherwise the user is
|
|
340
|
+
// stuck — `start` refuses because "another dashboard is running"
|
|
341
|
+
// but `stop` reports "no dashboard is running" because it only
|
|
342
|
+
// knows about the PID file.
|
|
343
|
+
const instances = await findAllDashboards();
|
|
344
|
+
const alive = instances.filter((i) => (i.state === 'healthy' || i.state === 'zombie') && i.pid);
|
|
345
|
+
if (alive.length === 0) {
|
|
346
|
+
// No live dashboard. Clear stale PID/port files if they exist.
|
|
347
|
+
if (existsSync(PID_FILE)) {
|
|
348
|
+
console.log(`Stale PID file at ${PID_FILE} — clearing.`);
|
|
349
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
350
|
+
}
|
|
351
|
+
if (existsSync(PORT_FILE)) {
|
|
352
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
353
|
+
}
|
|
354
|
+
console.log('No Bizar dashboard is running.');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
for (const inst of alive) {
|
|
358
|
+
try {
|
|
359
|
+
process.kill(inst.pid, 'SIGTERM');
|
|
360
|
+
console.log(`Stopped Bizar dashboard (pid ${inst.pid}${inst.port ? `, port ${inst.port}` : ''}, ${inst.state}).`);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.log(`Could not stop dashboard (pid ${inst.pid}): ${err.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Wait for graceful exit; escalate to SIGKILL for any stragglers.
|
|
366
|
+
for (const inst of alive) {
|
|
367
|
+
const exited = await waitForExit(inst.pid, 3000);
|
|
368
|
+
if (!exited) {
|
|
369
|
+
try {
|
|
370
|
+
process.kill(inst.pid, 'SIGKILL');
|
|
371
|
+
console.log(` pid ${inst.pid} did not exit gracefully; sent SIGKILL.`);
|
|
372
|
+
} catch { /* ignore */ }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
376
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function showStatus() {
|
|
380
|
+
// v3.20.14: scan ps + canonical PID file rather than only trusting
|
|
381
|
+
// the PID file. A dashboard started by another user/process without
|
|
382
|
+
// writing to our PID file would have been invisible here.
|
|
383
|
+
const instances = await findAllDashboards();
|
|
384
|
+
const alive = instances.filter((i) => i.state === 'healthy' && i.pid);
|
|
385
|
+
if (alive.length === 0) {
|
|
386
|
+
console.log('No Bizar dashboard is running. Use: bizar dash start');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const inst of alive) {
|
|
390
|
+
const port = inst.port ? `:${inst.port}` : '';
|
|
391
|
+
const canonical = inst.isCanonical ? ' (canonical)' : '';
|
|
392
|
+
console.log(`Bizar dashboard is running at http://localhost:${inst.port}/ — pid ${inst.pid}${port}${canonical}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function runTui({ launchWeb } = {}) {
|
|
397
|
+
const { createServer } = await import('./server/server.mjs');
|
|
398
|
+
const { launchBrowser } = await import('./server/browser.mjs');
|
|
399
|
+
const { launchTui } = await import('./server/tui.mjs');
|
|
400
|
+
|
|
401
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
402
|
+
const { server, close: closeServer } = await createServer({
|
|
403
|
+
port,
|
|
404
|
+
projectRoot: process.cwd(),
|
|
405
|
+
opencodeConfigDir: (
|
|
406
|
+
process.platform === 'win32'
|
|
407
|
+
? join(process.env.APPDATA || homedir(), 'opencode')
|
|
408
|
+
: join(homedir(), '.config', 'opencode')
|
|
409
|
+
),
|
|
410
|
+
bizarRoot: join(__dirname, '..', '..'),
|
|
411
|
+
});
|
|
412
|
+
// v3.6.0 — Honor BIZAR_DASHBOARD_BIND same as startDashboard.
|
|
413
|
+
const bindHost = process.env.BIZAR_DASHBOARD_BIND || '127.0.0.1';
|
|
414
|
+
await new Promise((resolve, reject) => {
|
|
415
|
+
server.once('error', reject);
|
|
416
|
+
server.listen(port, bindHost, () => {
|
|
417
|
+
server.off('error', reject);
|
|
418
|
+
resolve();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
422
|
+
writeFileSync(PORT_FILE, String(port), 'utf8');
|
|
423
|
+
writeFileSync(PID_FILE, String(process.pid), 'utf8');
|
|
424
|
+
|
|
425
|
+
if (launchWeb) {
|
|
426
|
+
const url = `http://localhost:${port}/`;
|
|
427
|
+
launchBrowser(url).catch(() => {});
|
|
428
|
+
console.log(`Web dashboard: ${url}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
await launchTui({ port });
|
|
433
|
+
} finally {
|
|
434
|
+
try { closeServer(); } catch { /* ignore */ }
|
|
435
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
436
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Exports (in-process use by `bizar` via @polderlabs/bizar-dash/dash-cli) ──
|
|
441
|
+
export { startDashboard as start, stopDashboard as stop, showStatus as status, runTui as tui, cleanupDashboards as cleanup };
|
|
442
|
+
export { startDashboard, stopDashboard, showStatus, runTui, startInBackground, cleanupDashboards };
|
|
443
|
+
|
|
444
|
+
function isMainEntry() {
|
|
445
|
+
try {
|
|
446
|
+
const entry = process.argv[1];
|
|
447
|
+
if (!entry) return false;
|
|
448
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── CLI dispatch (only when this file is the entry point) ────────────────────
|
|
455
|
+
// When `bizar` imports this file in-process, import.meta.url does not match
|
|
456
|
+
// process.argv[1], so none of the code below runs.
|
|
457
|
+
async function main() {
|
|
458
|
+
const args = process.argv.slice(2);
|
|
459
|
+
|
|
460
|
+
// Install CLI-level signal handlers ONLY when running as a CLI entry point.
|
|
461
|
+
// When imported in-process, the parent process owns its signals.
|
|
462
|
+
process.on('SIGINT', () => {
|
|
463
|
+
stopDashboard().finally(() => process.exit(0));
|
|
464
|
+
});
|
|
465
|
+
process.on('SIGTERM', () => {
|
|
466
|
+
stopDashboard().finally(() => process.exit(0));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Parse --port and --bind from CLI args (v3.11.1 — was missing)
|
|
470
|
+
const portArgIdx = args.indexOf('--port');
|
|
471
|
+
const portArg = portArgIdx >= 0 ? parseInt(args[portArgIdx + 1], 10) : undefined;
|
|
472
|
+
const bindArgIdx = args.indexOf('--bind');
|
|
473
|
+
const bindArg = bindArgIdx >= 0 ? args[bindArgIdx + 1] : undefined;
|
|
474
|
+
const portOpts = {};
|
|
475
|
+
if (Number.isFinite(portArg)) portOpts.port = portArg;
|
|
476
|
+
if (bindArg) process.env.BIZAR_DASHBOARD_BIND = bindArg;
|
|
477
|
+
|
|
478
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
479
|
+
showHelp();
|
|
480
|
+
} else if (args.includes('--version') || args.includes('-v')) {
|
|
481
|
+
console.log(readVersion());
|
|
482
|
+
} else if (args[0] === 'stop') {
|
|
483
|
+
await stopDashboard();
|
|
484
|
+
} else if (args[0] === 'status') {
|
|
485
|
+
showStatus();
|
|
486
|
+
} else if (args[0] === 'cleanup') {
|
|
487
|
+
// v3.20.7 — `bizar dash cleanup` finds and kills zombie/orphan
|
|
488
|
+
// dashboards. --force also kills healthy non-canonical duplicates.
|
|
489
|
+
// --kill escalates to SIGKILL.
|
|
490
|
+
const force = args.includes('--force') || args.includes('-f');
|
|
491
|
+
const kill = args.includes('--kill');
|
|
492
|
+
await cleanupDashboards({ force, signal: kill ? 'SIGKILL' : 'SIGTERM' });
|
|
493
|
+
} else if (args[0] === 'tui') {
|
|
494
|
+
const rest = args.slice(1);
|
|
495
|
+
const skipWeb = rest.includes('--no-web');
|
|
496
|
+
await runTui({ launchWeb: !skipWeb });
|
|
497
|
+
} else if (args[0] === 'start' && (args.includes('--bg') || args.includes('--detach'))) {
|
|
498
|
+
// `bizar start --bg` (or --detach) — start in background and return.
|
|
499
|
+
// MUST be matched before the plain `start` branch or the --bg flag
|
|
500
|
+
// is ignored and we run the dashboard in the foreground.
|
|
501
|
+
const portIdx = args.indexOf('--port');
|
|
502
|
+
const bgPort = portIdx >= 0 ? parseInt(args[portIdx + 1], 10) : undefined;
|
|
503
|
+
const bgOpts = { bg: true, ...(Number.isFinite(bgPort) ? { port: bgPort } : {}) };
|
|
504
|
+
await startDashboard(bgOpts).then(() => detachAfterBoot());
|
|
505
|
+
} else if (args.includes('--bg') || args.includes('--detach')) {
|
|
506
|
+
// Bare `bizar --bg` / `bizar --detach` — same intent.
|
|
507
|
+
await startDashboard({ bg: true }).then(() => detachAfterBoot());
|
|
508
|
+
} else if (args.includes('--web-only')) {
|
|
509
|
+
await startDashboard(portOpts);
|
|
510
|
+
} else if (args[0] === 'start' || args.length === 0) {
|
|
511
|
+
await startDashboard(portOpts);
|
|
512
|
+
} else {
|
|
513
|
+
showHelp();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (isMainEntry()) {
|
|
518
|
+
main().catch((err) => {
|
|
519
|
+
console.error(err && err.message ? err.message : err);
|
|
520
|
+
process.exit(1);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/activity-log.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.2.0 — Append-only activity log for the dashboard's Activity tab
|
|
5
|
+
* and canvas-node detail panels.
|
|
6
|
+
*
|
|
7
|
+
* Storage:
|
|
8
|
+
* ~/.config/opencode/activity.jsonl (one JSON object per line)
|
|
9
|
+
*
|
|
10
|
+
* Why JSONL:
|
|
11
|
+
* - Cheap append (write one line, no full-file rewrite).
|
|
12
|
+
* - Easy to tail / filter / reverse-scan without parsing the whole
|
|
13
|
+
* file.
|
|
14
|
+
* - Survives partial writes — corrupt trailing lines are skipped on
|
|
15
|
+
* read.
|
|
16
|
+
*
|
|
17
|
+
* Use cases:
|
|
18
|
+
* - The Activity tab reads the latest N entries for the global
|
|
19
|
+
* stream and filters by `nodeId` for per-node drilldowns.
|
|
20
|
+
* - Comment/task injection on canvas nodes writes here with
|
|
21
|
+
* `kind: 'node.comment'` and a `nodeId` so the detail panel can
|
|
22
|
+
* show a thread.
|
|
23
|
+
* - Task delegation broadcasts append with `kind: 'task.delegated'`
|
|
24
|
+
* and a `nodeId` of `task:<id>`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
existsSync,
|
|
29
|
+
readFileSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
renameSync,
|
|
32
|
+
appendFileSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
statSync,
|
|
35
|
+
} from 'node:fs';
|
|
36
|
+
import { join, dirname } from 'node:path';
|
|
37
|
+
import { homedir } from 'node:os';
|
|
38
|
+
|
|
39
|
+
const HOME = homedir();
|
|
40
|
+
const LOG_FILE = join(HOME, '.config', 'opencode', 'activity.jsonl');
|
|
41
|
+
|
|
42
|
+
// Hard cap on retained entries (the file is rotated/truncated by the
|
|
43
|
+
// caller; we just stop reading past this many in `recent()`).
|
|
44
|
+
const MAX_RETAINED = 5000;
|
|
45
|
+
const MAX_LINES_PER_READ = 5000;
|
|
46
|
+
|
|
47
|
+
// Atomic write: write to a sibling temp file, then rename into place.
|
|
48
|
+
// `rename` is atomic on POSIX (same filesystem), so a crash between
|
|
49
|
+
// read and write never leaves a partial / empty log file.
|
|
50
|
+
function atomicWriteText(filePath, text) {
|
|
51
|
+
const tmp = `${filePath}.tmp.${process.pid}`;
|
|
52
|
+
writeFileSync(tmp, text, 'utf8');
|
|
53
|
+
renameSync(tmp, filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureFile() {
|
|
57
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
58
|
+
if (!existsSync(LOG_FILE)) {
|
|
59
|
+
writeFileSync(LOG_FILE, '', 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Rotate the log when it exceeds the size cap. Atomic: write the
|
|
64
|
+
// truncated contents to a temp file, then rename into place so a
|
|
65
|
+
// crash between read and write can't lose the whole log.
|
|
66
|
+
const ROTATE_MAX_BYTES = 5 * 1024 * 1024;
|
|
67
|
+
function rotateIfNeeded(logFile) {
|
|
68
|
+
if (!existsSync(logFile)) return;
|
|
69
|
+
const st = statSync(logFile);
|
|
70
|
+
if (st.size < ROTATE_MAX_BYTES) return;
|
|
71
|
+
const lines = readFileSync(logFile, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
72
|
+
const kept = lines.slice(-MAX_RETAINED);
|
|
73
|
+
atomicWriteText(logFile, kept.join('\n') + '\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function safeParse(line) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(line);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const activityLog = {
|
|
85
|
+
LOG_FILE,
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Append an event. `event` is a plain object; we add `ts` if absent.
|
|
89
|
+
*/
|
|
90
|
+
append(event) {
|
|
91
|
+
try {
|
|
92
|
+
ensureFile();
|
|
93
|
+
const record = { ...(event || {}), ts: new Date().toISOString() };
|
|
94
|
+
appendFileSync(LOG_FILE, JSON.stringify(record) + '\n', 'utf8');
|
|
95
|
+
// Best-effort rotate when the file gets large.
|
|
96
|
+
try {
|
|
97
|
+
rotateIfNeeded(LOG_FILE);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[activity-log] rotation failed:', err.message);
|
|
100
|
+
}
|
|
101
|
+
return record;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error('[activity-log] append failed:', err.message);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read up to `limit` most-recent entries, newest-first.
|
|
110
|
+
*/
|
|
111
|
+
recent(limit = 100) {
|
|
112
|
+
try {
|
|
113
|
+
ensureFile();
|
|
114
|
+
const text = readFileSync(LOG_FILE, 'utf8');
|
|
115
|
+
if (!text.trim()) return [];
|
|
116
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
117
|
+
const slice = lines.slice(-Math.min(MAX_LINES_PER_READ, Math.max(1, limit * 4)));
|
|
118
|
+
const out = [];
|
|
119
|
+
for (let i = slice.length - 1; i >= 0 && out.length < limit; i--) {
|
|
120
|
+
const parsed = safeParse(slice[i]);
|
|
121
|
+
if (parsed) out.push(parsed);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Filter the recent log by nodeId. Newest-first.
|
|
131
|
+
*/
|
|
132
|
+
forNode(nodeId, limit = 50) {
|
|
133
|
+
if (!nodeId) return [];
|
|
134
|
+
const all = this.recent(limit * 4);
|
|
135
|
+
return all.filter((e) => e.nodeId === nodeId).slice(0, limit);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Filter by kind. Newest-first.
|
|
140
|
+
*/
|
|
141
|
+
byKind(kind, limit = 100) {
|
|
142
|
+
const all = this.recent(limit * 4);
|
|
143
|
+
return all.filter((e) => e.kind === kind).slice(0, limit);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Stats: counts by kind, last ts, file size.
|
|
148
|
+
*/
|
|
149
|
+
stats() {
|
|
150
|
+
try {
|
|
151
|
+
ensureFile();
|
|
152
|
+
const st = statSync(LOG_FILE);
|
|
153
|
+
const text = readFileSync(LOG_FILE, 'utf8');
|
|
154
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
155
|
+
const counts = {};
|
|
156
|
+
let lastTs = null;
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
const p = safeParse(line);
|
|
159
|
+
if (!p) continue;
|
|
160
|
+
counts[p.kind || 'unknown'] = (counts[p.kind || 'unknown'] || 0) + 1;
|
|
161
|
+
if (!lastTs || p.ts > lastTs) lastTs = p.ts;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
file: LOG_FILE,
|
|
165
|
+
size: st.size,
|
|
166
|
+
lines: lines.length,
|
|
167
|
+
lastTs,
|
|
168
|
+
counts,
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return { file: LOG_FILE, size: 0, lines: 0, lastTs: null, counts: {} };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|