@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,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/tasks.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/tasks — list
|
|
5
|
+
* /api/tasks (POST) — create
|
|
6
|
+
* /api/tasks/submit (POST) — Odin-style multi-task dispatch
|
|
7
|
+
* /api/tasks/bulk (POST) — bulk action across ids
|
|
8
|
+
* /api/tasks/:id (PUT) — update
|
|
9
|
+
* /api/tasks/:id (DELETE) — delete
|
|
10
|
+
* /api/tasks/:id/start (POST) — trigger dispatch of a queued task
|
|
11
|
+
* /api/tasks/:id/status (PATCH) — move status (queued|doing|done|blocked|archived)
|
|
12
|
+
* /api/tasks/:id/comments (POST) — append comment
|
|
13
|
+
* /api/tasks/:id/chat (GET) — opencode messages for the task's bg instance
|
|
14
|
+
* /api/tasks/:id/artifacts (GET) — list artifacts for this task
|
|
15
|
+
* /api/tasks/:id/artifacts (POST) — attach artifact + link to task
|
|
16
|
+
* /api/tasks/:id/timer (POST) — toggle timer
|
|
17
|
+
* /api/tasks/:id/timer/start (POST) — start timer
|
|
18
|
+
* /api/tasks/:id/timer/stop (POST) — stop timer
|
|
19
|
+
* /api/tasks/:id/archive (POST) — archive
|
|
20
|
+
* /api/tasks/:id/unarchive (POST) — unarchive
|
|
21
|
+
* /api/tasks/:id/work (POST) — agent claims progress
|
|
22
|
+
* /api/tasks/:id/progress (POST) — progress + current step update
|
|
23
|
+
*
|
|
24
|
+
* IMPORTANT — ordering matters for Express. /tasks/bulk and
|
|
25
|
+
* /tasks/submit MUST be declared before /tasks/:id so that the
|
|
26
|
+
* literal "bulk" / "submit" segments don't get captured by the
|
|
27
|
+
* :id parameter. Same for /tasks/:id/start vs /tasks/:id (less of a
|
|
28
|
+
* conflict since :id is followed by something either way).
|
|
29
|
+
*/
|
|
30
|
+
import { Router } from 'express';
|
|
31
|
+
import { tasksStore } from '../tasks-store.mjs';
|
|
32
|
+
import { agentsStore } from '../agents-store.mjs';
|
|
33
|
+
import { notificationsStore } from '../notifications-store.mjs';
|
|
34
|
+
import { projectsStore } from '../projects-store.mjs';
|
|
35
|
+
import { artifactsStore } from '../artifacts-store.mjs';
|
|
36
|
+
import {
|
|
37
|
+
readServeInfo,
|
|
38
|
+
listOpencodeMessages,
|
|
39
|
+
normalizeOpencodeMessage,
|
|
40
|
+
} from '../serve-info.mjs';
|
|
41
|
+
import { readActiveProjectId, wrap } from './_shared.mjs';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} deps
|
|
45
|
+
* @param {object} deps.state
|
|
46
|
+
* @param {Function} deps.broadcast
|
|
47
|
+
* @param {string} deps.projectRoot
|
|
48
|
+
* @returns {import('express').Router}
|
|
49
|
+
*/
|
|
50
|
+
export function createTasksRouter({ state, broadcast, projectRoot }) {
|
|
51
|
+
const router = Router();
|
|
52
|
+
|
|
53
|
+
// /tasks/submit must come BEFORE /tasks/:id so the literal segment
|
|
54
|
+
// isn't captured by the :id param. The Odin task delegator splits
|
|
55
|
+
// the input into subtasks and dispatches them to bg infrastructure.
|
|
56
|
+
router.post('/tasks/submit', wrap(async (req, res) => {
|
|
57
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
58
|
+
const { taskDelegator } = await import('../task-delegator.mjs');
|
|
59
|
+
try {
|
|
60
|
+
const result = await taskDelegator.submit(req.body || {}, {
|
|
61
|
+
projectRoot,
|
|
62
|
+
projectId,
|
|
63
|
+
state,
|
|
64
|
+
broadcast,
|
|
65
|
+
});
|
|
66
|
+
// 207 Multi-Status when some dispatches failed — caller can decide
|
|
67
|
+
// whether to retry. Pure success keeps 201.
|
|
68
|
+
const partial = (result?.dispatch?.errors || []).length > 0;
|
|
69
|
+
res.status(partial ? 207 : 201).json(result);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const status = err?.message?.includes('required') ? 400 : 500;
|
|
72
|
+
res.status(status).json({
|
|
73
|
+
error: status === 400 ? 'bad_request' : 'submission_failed',
|
|
74
|
+
message: err?.message || String(err),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
router.get('/tasks', wrap(async (req, res) => {
|
|
80
|
+
const projectId = req.query.projectId || readActiveProjectId();
|
|
81
|
+
const includeArchived = req.query.archived === 'true' || req.query.archived === '1';
|
|
82
|
+
const onlyArchived = req.query.archived === 'only' || req.query.archived === 'archived';
|
|
83
|
+
res.json(tasksStore.loadTasks(projectId, { includeArchived, onlyArchived }));
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
router.post('/tasks', wrap(async (req, res) => {
|
|
87
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
88
|
+
const task = await tasksStore.create(projectId, req.body || {});
|
|
89
|
+
broadcast({ type: 'tasks:change', task });
|
|
90
|
+
res.status(201).json(task);
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
router.put('/tasks/:id', wrap(async (req, res) => {
|
|
94
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
95
|
+
const task = await tasksStore.update(projectId, req.params.id, req.body || {});
|
|
96
|
+
if (!task) {
|
|
97
|
+
res.status(404).json({ error: 'not_found' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
broadcast({ type: 'tasks:change', task });
|
|
101
|
+
res.json(task);
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// v3.5.4 — Trigger execution of a queued task. Used by external
|
|
105
|
+
// operators, tests, and the UI's "Retry dispatch" button. Reuses the
|
|
106
|
+
// same dispatch logic as `/tasks/submit` so dispatch errors surface
|
|
107
|
+
// in the same shape. Returns 400 when the task is not in `queued`
|
|
108
|
+
// status, 404 when missing.
|
|
109
|
+
router.post('/tasks/:id/start', wrap(async (req, res) => {
|
|
110
|
+
const projectId = req.body?.projectId || req.query?.projectId || readActiveProjectId();
|
|
111
|
+
const taskId = req.params.id;
|
|
112
|
+
const task = await tasksStore.getById(projectId, taskId);
|
|
113
|
+
if (!task) {
|
|
114
|
+
res.status(404).json({ error: 'not_found', message: `task ${taskId} not found` });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (task.status !== 'queued') {
|
|
118
|
+
res.status(400).json({
|
|
119
|
+
error: 'invalid_status',
|
|
120
|
+
message: `task is '${task.status}', must be 'queued' to start`,
|
|
121
|
+
task,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { taskDelegator } = await import('../task-delegator.mjs');
|
|
126
|
+
const result = await taskDelegator.dispatchSingleTask(taskId, {
|
|
127
|
+
projectRoot,
|
|
128
|
+
projectId,
|
|
129
|
+
state,
|
|
130
|
+
broadcast,
|
|
131
|
+
});
|
|
132
|
+
if (!result.ok) {
|
|
133
|
+
// Surface dispatch errors with 502 so callers know the task did
|
|
134
|
+
// not actually start. `result.task` carries the refreshed state.
|
|
135
|
+
const code = (result.errors || []).some((e) => e.kind === 'not_found') ? 404 : 502;
|
|
136
|
+
res.status(code).json({
|
|
137
|
+
error: 'dispatch_failed',
|
|
138
|
+
message: (result.errors || []).map((e) => e.message).join('; ') || 'dispatch failed',
|
|
139
|
+
task: result.task,
|
|
140
|
+
errors: result.errors || [],
|
|
141
|
+
warnings: result.warnings || [],
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
res.status(202).json({
|
|
146
|
+
ok: true,
|
|
147
|
+
task: result.task,
|
|
148
|
+
dispatched: result.dispatched,
|
|
149
|
+
warnings: result.warnings || [],
|
|
150
|
+
});
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
router.patch('/tasks/:id/status', wrap(async (req, res) => {
|
|
154
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
155
|
+
const { status } = req.body || {};
|
|
156
|
+
if (!['queued', 'doing', 'done', 'blocked', 'archived'].includes(status)) {
|
|
157
|
+
res.status(400).json({ error: 'bad_request', message: 'invalid status' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const task = await tasksStore.move(projectId, req.params.id, status);
|
|
161
|
+
if (!task) {
|
|
162
|
+
res.status(404).json({ error: 'not_found' });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
broadcast({ type: 'tasks:change', task });
|
|
166
|
+
if (status === 'done' && task.recurring) {
|
|
167
|
+
const next = await tasksStore.spawnNextRecurrence(projectId, task);
|
|
168
|
+
if (next) broadcast({ type: 'tasks:change', task: next });
|
|
169
|
+
}
|
|
170
|
+
// v3.3.0 — Fire a notification on completion. The Tasks view
|
|
171
|
+
// already paints the badge in real time via tasks:change; the
|
|
172
|
+
// notification is the cross-tab history bit.
|
|
173
|
+
if (status === 'done') {
|
|
174
|
+
try {
|
|
175
|
+
notificationsStore.add({
|
|
176
|
+
severity: 'success',
|
|
177
|
+
source: 'tasks',
|
|
178
|
+
title: 'Task completed',
|
|
179
|
+
message: task.title || task.id,
|
|
180
|
+
meta: { taskId: task.id },
|
|
181
|
+
}, { broadcast });
|
|
182
|
+
} catch { /* best-effort */ }
|
|
183
|
+
} else if (status === 'blocked') {
|
|
184
|
+
try {
|
|
185
|
+
notificationsStore.add({
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
source: 'tasks',
|
|
188
|
+
title: 'Task blocked',
|
|
189
|
+
message: task.title || task.id,
|
|
190
|
+
meta: { taskId: task.id },
|
|
191
|
+
}, { broadcast });
|
|
192
|
+
} catch { /* best-effort */ }
|
|
193
|
+
}
|
|
194
|
+
res.json(task);
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
router.post('/tasks/:id/comments', wrap(async (req, res) => {
|
|
198
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
199
|
+
const task = await tasksStore.addComment(projectId, req.params.id, req.body?.text || '');
|
|
200
|
+
if (!task) {
|
|
201
|
+
res.status(404).json({ error: 'not_found' });
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
broadcast({ type: 'tasks:change', task });
|
|
205
|
+
res.json(task);
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
// v3.5.5 — Chat-task linkage. Given a task id, return the
|
|
209
|
+
// opencode session's messages so the chat UI can open a thread
|
|
210
|
+
// for an in-progress or completed task. The task must have a
|
|
211
|
+
// `bgInstanceId` (or `sessionId`) in its metadata — the delegator
|
|
212
|
+
// writes that when the opencode session is created.
|
|
213
|
+
//
|
|
214
|
+
// Response shape:
|
|
215
|
+
// { taskId, bgInstanceId, sessionId, agent, messages: [{id,role,content,ts}, ...] }
|
|
216
|
+
//
|
|
217
|
+
// Status codes:
|
|
218
|
+
// 200 — messages returned (may be empty)
|
|
219
|
+
// 404 — task not found, or no bg instance / session id
|
|
220
|
+
// 503 — plugin offline
|
|
221
|
+
// 502 — opencode listMessages call failed
|
|
222
|
+
router.get('/tasks/:id/chat', wrap(async (req, res) => {
|
|
223
|
+
const projectId = req.query.projectId || readActiveProjectId();
|
|
224
|
+
const task = await tasksStore.getById(projectId, req.params.id);
|
|
225
|
+
if (!task) {
|
|
226
|
+
res.status(404).json({ error: 'not_found', message: `task ${req.params.id} not found` });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const meta = task.metadata || {};
|
|
230
|
+
let bgInstanceId = meta.bgInstanceId || meta.instanceId || null;
|
|
231
|
+
let sessionId = meta.sessionId || null;
|
|
232
|
+
let agent = meta.agent || task.assignee || null;
|
|
233
|
+
|
|
234
|
+
// If we have a bgInstanceId but not a sessionId, look it up.
|
|
235
|
+
if (bgInstanceId && !sessionId) {
|
|
236
|
+
try {
|
|
237
|
+
const { backgroundStore } = await import('../background-store.mjs');
|
|
238
|
+
const bg = backgroundStore.get(bgInstanceId);
|
|
239
|
+
if (bg) {
|
|
240
|
+
sessionId = bg.sessionId || null;
|
|
241
|
+
agent = agent || bg.agent || null;
|
|
242
|
+
}
|
|
243
|
+
} catch { /* best effort */ }
|
|
244
|
+
}
|
|
245
|
+
if (!bgInstanceId && !sessionId) {
|
|
246
|
+
return res.status(404).json({
|
|
247
|
+
error: 'no_bg_instance',
|
|
248
|
+
message: 'task has no bg instance or opencode session id',
|
|
249
|
+
taskId: task.id,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const serveInfo = readServeInfo();
|
|
254
|
+
if (!serveInfo) {
|
|
255
|
+
return res.status(503).json({
|
|
256
|
+
error: 'plugin_offline',
|
|
257
|
+
message: 'opencode plugin is not running',
|
|
258
|
+
taskId: task.id,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (!sessionId) {
|
|
262
|
+
return res.status(404).json({
|
|
263
|
+
error: 'no_session',
|
|
264
|
+
message: 'bg instance has no opencode session id',
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
bgInstanceId,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const active = projectsStore.active();
|
|
271
|
+
const directory = (active && active.path) || serveInfo.worktree || '';
|
|
272
|
+
const result = await listOpencodeMessages(serveInfo, sessionId, directory);
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
return res.status(502).json({
|
|
275
|
+
error: 'opencode_error',
|
|
276
|
+
message: result.error || 'listOpencodeMessages failed',
|
|
277
|
+
taskId: task.id,
|
|
278
|
+
sessionId,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const messages = Array.isArray(result.messages)
|
|
282
|
+
? result.messages.map(normalizeOpencodeMessage)
|
|
283
|
+
: [];
|
|
284
|
+
res.json({
|
|
285
|
+
taskId: task.id,
|
|
286
|
+
bgInstanceId,
|
|
287
|
+
sessionId,
|
|
288
|
+
agent,
|
|
289
|
+
messages,
|
|
290
|
+
});
|
|
291
|
+
}));
|
|
292
|
+
|
|
293
|
+
// v3.5.5 — Per-task artifact endpoints. These live in tasks.mjs
|
|
294
|
+
// (not artifacts.mjs) because the path parameter is the task id.
|
|
295
|
+
// The standalone /api/artifacts/:id routes are in artifacts.mjs.
|
|
296
|
+
router.get('/tasks/:id/artifacts', wrap(async (req, res) => {
|
|
297
|
+
const items = artifactsStore.list({ taskId: req.params.id });
|
|
298
|
+
res.json({ artifacts: items, taskId: req.params.id });
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
router.post('/tasks/:id/artifacts', wrap(async (req, res) => {
|
|
302
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
303
|
+
const active = projectsStore.active();
|
|
304
|
+
const { name, contentType, content } = req.body || {};
|
|
305
|
+
if (typeof content !== 'string' || !content.length) {
|
|
306
|
+
res.status(400).json({ error: 'content_required', message: 'content (string) is required' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const meta = artifactsStore.save({
|
|
310
|
+
taskId: req.params.id,
|
|
311
|
+
projectId: projectId || (active ? active.id : null),
|
|
312
|
+
name: name || `Artifact for ${req.params.id}`,
|
|
313
|
+
contentType: contentType || 'text/html',
|
|
314
|
+
content,
|
|
315
|
+
});
|
|
316
|
+
// Link the artifact back to the task. We do NOT overwrite
|
|
317
|
+
// existing artifactId/artifactIds metadata; the new id is
|
|
318
|
+
// appended to the array and the legacy `artifactId` field is
|
|
319
|
+
// bumped to the latest.
|
|
320
|
+
let updated = null;
|
|
321
|
+
if (projectId) {
|
|
322
|
+
const task = await tasksStore.getById(projectId, req.params.id);
|
|
323
|
+
const prevMeta = (task && task.metadata) || {};
|
|
324
|
+
const prevIds = Array.isArray(prevMeta.artifactIds) ? prevMeta.artifactIds : [];
|
|
325
|
+
const newIds = prevIds.includes(meta.id) ? prevIds : [...prevIds, meta.id];
|
|
326
|
+
updated = await tasksStore.update(projectId, req.params.id, {
|
|
327
|
+
metadata: {
|
|
328
|
+
artifactId: meta.id,
|
|
329
|
+
artifactIds: newIds,
|
|
330
|
+
artifactName: meta.name,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
if (updated) {
|
|
334
|
+
broadcast({ type: 'tasks:change', task: updated });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
broadcast({ type: 'artifact:new', artifact: meta });
|
|
338
|
+
res.status(201).json(meta);
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
router.post('/tasks/:id/timer', wrap(async (req, res) => {
|
|
342
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
343
|
+
const task = await tasksStore.toggleTimer(projectId, req.params.id);
|
|
344
|
+
if (!task) {
|
|
345
|
+
res.status(404).json({ error: 'not_found' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
broadcast({ type: 'tasks:change', task });
|
|
349
|
+
res.json(task);
|
|
350
|
+
}));
|
|
351
|
+
|
|
352
|
+
router.post('/tasks/:id/timer/start', wrap(async (req, res) => {
|
|
353
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
354
|
+
const task = await tasksStore.startTimer(projectId, req.params.id);
|
|
355
|
+
if (!task) { res.status(404).json({ error: 'not_found' }); return; }
|
|
356
|
+
broadcast({ type: 'tasks:change', task });
|
|
357
|
+
res.json(task);
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
router.post('/tasks/:id/timer/stop', wrap(async (req, res) => {
|
|
361
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
362
|
+
const task = await tasksStore.stopTimer(projectId, req.params.id);
|
|
363
|
+
if (!task) { res.status(404).json({ error: 'not_found' }); return; }
|
|
364
|
+
broadcast({ type: 'tasks:change', task });
|
|
365
|
+
res.json(task);
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
// v3.1.0 — Archive / unarchive / bulk.
|
|
369
|
+
router.post('/tasks/:id/archive', wrap(async (req, res) => {
|
|
370
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
371
|
+
const task = await tasksStore.archive(projectId, req.params.id);
|
|
372
|
+
if (!task) { res.status(404).json({ error: 'not_found' }); return; }
|
|
373
|
+
broadcast({ type: 'tasks:change', task });
|
|
374
|
+
res.json(task);
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
router.post('/tasks/:id/unarchive', wrap(async (req, res) => {
|
|
378
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
379
|
+
const task = await tasksStore.unarchive(projectId, req.params.id);
|
|
380
|
+
if (!task) { res.status(404).json({ error: 'not_found' }); return; }
|
|
381
|
+
broadcast({ type: 'tasks:change', task });
|
|
382
|
+
res.json(task);
|
|
383
|
+
}));
|
|
384
|
+
|
|
385
|
+
// /tasks/bulk MUST come before /tasks/:id so the literal segment
|
|
386
|
+
// doesn't get captured as an id.
|
|
387
|
+
router.post('/tasks/bulk', wrap(async (req, res) => {
|
|
388
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
389
|
+
const { ids, action, params } = req.body || {};
|
|
390
|
+
if (!Array.isArray(ids)) {
|
|
391
|
+
res.status(400).json({ error: 'bad_request', message: 'ids[] required' });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const out = await tasksStore.bulk(projectId, ids, action, params || {});
|
|
395
|
+
for (const r of out.affected) {
|
|
396
|
+
if (!r.ok) continue;
|
|
397
|
+
if (action === 'delete') {
|
|
398
|
+
broadcast({ type: 'tasks:delete', id: r.id });
|
|
399
|
+
} else {
|
|
400
|
+
const all = await tasksStore.loadTasks(projectId, { includeArchived: true });
|
|
401
|
+
const t = all.find((x) => x.id === r.id);
|
|
402
|
+
if (t) broadcast({ type: 'tasks:change', task: t });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
res.json(out);
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
// v3.1.0 — Mark a task as worked-on by an agent. Bumps both sides.
|
|
409
|
+
router.post('/tasks/:id/work', wrap(async (req, res) => {
|
|
410
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
411
|
+
const { agent, status, complete } = req.body || {};
|
|
412
|
+
if (!agent) {
|
|
413
|
+
res.status(400).json({ error: 'bad_request', message: 'agent is required' });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const task = await tasksStore.setWorkedBy(projectId, req.params.id, agent, { status, complete: !!complete });
|
|
417
|
+
if (!task) { res.status(404).json({ error: 'not_found' }); return; }
|
|
418
|
+
let agentSnapshot = null;
|
|
419
|
+
if (status === 'doing') {
|
|
420
|
+
agentSnapshot = agentsStore.updateStatus(agent, 'working', task.id);
|
|
421
|
+
} else if (status === 'done' || complete) {
|
|
422
|
+
agentSnapshot = agentsStore.recordTaskResult(agent, task.id, complete !== false);
|
|
423
|
+
} else {
|
|
424
|
+
agentSnapshot = agentsStore.updateStatus(agent, 'idle', null);
|
|
425
|
+
}
|
|
426
|
+
if (agentSnapshot) broadcast({ type: 'agent:status', agent: agentSnapshot });
|
|
427
|
+
broadcast({ type: 'tasks:change', task });
|
|
428
|
+
if (task.recurring && status === 'done') {
|
|
429
|
+
const next = await tasksStore.spawnNextRecurrence(projectId, task);
|
|
430
|
+
if (next) broadcast({ type: 'tasks:change', task: next });
|
|
431
|
+
}
|
|
432
|
+
res.json(task);
|
|
433
|
+
}));
|
|
434
|
+
|
|
435
|
+
router.delete('/tasks/:id', wrap(async (req, res) => {
|
|
436
|
+
const projectId = req.query.projectId || readActiveProjectId();
|
|
437
|
+
const ok = await tasksStore.delete(projectId, req.params.id);
|
|
438
|
+
if (!ok) {
|
|
439
|
+
res.status(404).json({ error: 'not_found' });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
broadcast({ type: 'tasks:delete', id: req.params.id });
|
|
443
|
+
res.status(204).end();
|
|
444
|
+
}));
|
|
445
|
+
|
|
446
|
+
// v3.3.0 — Task progress updates from agents. Updates the in-store
|
|
447
|
+
// metadata.progress + metadata.currentStep and broadcasts a WS
|
|
448
|
+
// event so the Tasks view can paint the progress bar in real
|
|
449
|
+
// time.
|
|
450
|
+
router.post('/tasks/:id/progress', wrap(async (req, res) => {
|
|
451
|
+
const projectId = req.body?.projectId || readActiveProjectId();
|
|
452
|
+
const { progress, step, agent } = req.body || {};
|
|
453
|
+
const task = await tasksStore.updateProgress(projectId, req.params.id, {
|
|
454
|
+
progress: Number.isFinite(progress) ? Math.max(0, Math.min(100, progress)) : 0,
|
|
455
|
+
step: typeof step === 'string' ? step.slice(0, 200) : null,
|
|
456
|
+
agent: typeof agent === 'string' ? agent.slice(0, 60) : null,
|
|
457
|
+
});
|
|
458
|
+
if (!task) {
|
|
459
|
+
res.status(404).json({ error: 'not_found' });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
broadcast({ type: 'task:progress', taskId: task.id, progress: task.metadata?.progress, step: task.metadata?.currentStep, agent: task.metadata?.progressAgent });
|
|
463
|
+
broadcast({ type: 'tasks:change', task });
|
|
464
|
+
res.json(task);
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
return router;
|
|
468
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/themes.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/themes — list saved themes
|
|
5
|
+
* /api/themes (POST) — add a named theme
|
|
6
|
+
* /api/themes/:name (DELETE) — remove
|
|
7
|
+
*
|
|
8
|
+
* Themes live on the state object (not disk) — the registry is
|
|
9
|
+
* in-memory. The active theme is on settings.json; the named
|
|
10
|
+
* themes here are the user's saved palette presets.
|
|
11
|
+
*/
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
import { wrap } from './_shared.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} deps
|
|
17
|
+
* @param {object} deps.state
|
|
18
|
+
* @returns {import('express').Router}
|
|
19
|
+
*/
|
|
20
|
+
export function createThemesRouter({ state }) {
|
|
21
|
+
const router = Router();
|
|
22
|
+
|
|
23
|
+
router.get('/themes', wrap(async (_req, res) => {
|
|
24
|
+
res.json(state.getThemes());
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
router.post('/themes', wrap(async (req, res) => {
|
|
28
|
+
const { name, colors } = req.body || {};
|
|
29
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
30
|
+
res.status(400).json({ error: 'bad_request', message: 'name is required' });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!colors || typeof colors !== 'object') {
|
|
34
|
+
res.status(400).json({ error: 'bad_request', message: 'colors object is required' });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const out = state.addTheme(name.trim().slice(0, 60), colors);
|
|
38
|
+
res.status(201).json(out);
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
router.delete('/themes/:name', wrap(async (req, res) => {
|
|
42
|
+
const out = state.removeTheme(decodeURIComponent(req.params.name));
|
|
43
|
+
if (!out.removed) {
|
|
44
|
+
res.status(404).json({ error: 'not_found' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
res.status(204).end();
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
return router;
|
|
51
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes-v2/auth.mjs
|
|
3
|
+
*
|
|
4
|
+
* v0.7.0-alpha.1 — HTTP Basic auth middleware for /api/v2/* routes.
|
|
5
|
+
*
|
|
6
|
+
* Username: `opencode` (per opencode convention used elsewhere in Bizar).
|
|
7
|
+
* Password: the dashboard-generated secret in ~/.cache/bizarharness/dash-auth.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Buffer } from 'node:buffer';
|
|
11
|
+
|
|
12
|
+
export const V2_AUTH_REALM = 'Bizar Dashboard v2';
|
|
13
|
+
|
|
14
|
+
export function v2BasicAuth(getPassword) {
|
|
15
|
+
return function (req, res, next) {
|
|
16
|
+
const header = req.headers?.authorization ?? req.headers?.Authorization;
|
|
17
|
+
if (!header || typeof header !== 'string' || !header.startsWith('Basic ')) {
|
|
18
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
|
|
19
|
+
return res.status(401).json({
|
|
20
|
+
name: 'DashboardError',
|
|
21
|
+
data: { statusCode: 401, message: 'Missing Authorization header' },
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expected = getPassword();
|
|
26
|
+
if (typeof expected !== 'string' || expected.length === 0) {
|
|
27
|
+
return res.status(500).json({
|
|
28
|
+
name: 'DashboardError',
|
|
29
|
+
data: { statusCode: 500, message: 'Dashboard auth not initialized' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let decoded;
|
|
34
|
+
try {
|
|
35
|
+
const b64 = header.slice('Basic '.length);
|
|
36
|
+
decoded = Buffer.from(b64, 'base64').toString('utf-8');
|
|
37
|
+
} catch {
|
|
38
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
|
|
39
|
+
return res.status(401).json({
|
|
40
|
+
name: 'DashboardError',
|
|
41
|
+
data: { statusCode: 401, message: 'Malformed Authorization header' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const colonIdx = decoded.indexOf(':');
|
|
46
|
+
const user = colonIdx === -1 ? decoded : decoded.slice(0, colonIdx);
|
|
47
|
+
const pass = colonIdx === -1 ? '' : decoded.slice(colonIdx + 1);
|
|
48
|
+
|
|
49
|
+
if (user !== 'opencode' || pass !== expected) {
|
|
50
|
+
res.setHeader('WWW-Authenticate', `Basic realm="${V2_AUTH_REALM}"`);
|
|
51
|
+
return res.status(401).json({
|
|
52
|
+
name: 'DashboardError',
|
|
53
|
+
data: { statusCode: 401, message: 'Invalid credentials' },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return next();
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes-v2/events.mjs
|
|
3
|
+
*
|
|
4
|
+
* v0.7.0-alpha.1 — SSE event stream + publish endpoint.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/v2/event — subscribe (text/event-stream, sends dashboard.connected first)
|
|
7
|
+
* POST /api/v2/event — publish (returns 204)
|
|
8
|
+
*
|
|
9
|
+
* Matches the opencode SSE wire format:
|
|
10
|
+
* event: <type>
|
|
11
|
+
* data: {"type": "<type>", "properties": {...}}
|
|
12
|
+
*
|
|
13
|
+
* Replay buffer: last 100 events, accessible via `?since=<seq>`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import express from 'express';
|
|
17
|
+
|
|
18
|
+
export function createV2EventsRouter({ eventBus }) {
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
router.get('/event', (req, res) => {
|
|
22
|
+
res.status(200);
|
|
23
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
24
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
25
|
+
res.setHeader('Connection', 'keep-alive');
|
|
26
|
+
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering
|
|
27
|
+
res.flushHeaders?.();
|
|
28
|
+
|
|
29
|
+
// First event: dashboard.connected.
|
|
30
|
+
writeSseEvent(res, { type: 'dashboard.connected', properties: {} });
|
|
31
|
+
|
|
32
|
+
const sinceRaw = req.query?.since;
|
|
33
|
+
const since = typeof sinceRaw === 'string' ? Number.parseInt(sinceRaw, 10) : undefined;
|
|
34
|
+
const validSince = Number.isFinite(since) ? since : undefined;
|
|
35
|
+
|
|
36
|
+
const sub = eventBus.subscribe({ since: validSince });
|
|
37
|
+
|
|
38
|
+
// Async pump: pull events from sub.events and write them to res.
|
|
39
|
+
const pump = (async () => {
|
|
40
|
+
try {
|
|
41
|
+
for await (const record of sub.events) {
|
|
42
|
+
if (res.writableEnded || res.destroyed) break;
|
|
43
|
+
writeSseEvent(res, record);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (!res.writableEnded) {
|
|
47
|
+
try {
|
|
48
|
+
res.end();
|
|
49
|
+
} catch {
|
|
50
|
+
/* ignore */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
|
|
56
|
+
// Heartbeat every 25s to keep the connection alive through proxies.
|
|
57
|
+
const heartbeat = setInterval(() => {
|
|
58
|
+
if (res.writableEnded || res.destroyed) {
|
|
59
|
+
clearInterval(heartbeat);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
res.write(': keepalive\n\n');
|
|
64
|
+
} catch {
|
|
65
|
+
clearInterval(heartbeat);
|
|
66
|
+
}
|
|
67
|
+
}, 25_000);
|
|
68
|
+
|
|
69
|
+
req.on('close', () => {
|
|
70
|
+
clearInterval(heartbeat);
|
|
71
|
+
sub.close();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Don't await pump — it's a long-lived stream.
|
|
75
|
+
void pump;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
router.post('/event', (req, res) => {
|
|
79
|
+
const body = req.body;
|
|
80
|
+
if (!body || typeof body !== 'object' || typeof body.type !== 'string') {
|
|
81
|
+
return res.status(400).json({
|
|
82
|
+
name: 'DashboardError',
|
|
83
|
+
data: { statusCode: 400, message: 'Event must have a string `type` field' },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
eventBus.publish({ type: body.type, properties: body.properties ?? {} });
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return res.status(400).json({
|
|
90
|
+
name: 'DashboardError',
|
|
91
|
+
data: { statusCode: 400, message: err instanceof Error ? err.message : String(err) },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return res.status(204).end();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return router;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeSseEvent(res, record) {
|
|
101
|
+
const payload = JSON.stringify({
|
|
102
|
+
type: record.type,
|
|
103
|
+
properties: record.properties ?? {},
|
|
104
|
+
});
|
|
105
|
+
res.write(`event: ${record.type}\n`);
|
|
106
|
+
res.write(`data: ${payload}\n\n`);
|
|
107
|
+
}
|