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