@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,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/chat.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/chat — get chat history (per session)
|
|
5
|
+
* /api/chat (POST) — send a message; dispatches to opencode plugin
|
|
6
|
+
* /api/chat/sessions — list sessions for the active project
|
|
7
|
+
* /api/chat/sessions (POST) — create a new session
|
|
8
|
+
* /api/chat/regenerate (POST) — re-dispatch the last user message
|
|
9
|
+
*
|
|
10
|
+
* The POST /api/chat handler is the most complex endpoint in the
|
|
11
|
+
* codebase: it persists the user message, mints an opencode session
|
|
12
|
+
* if needed, posts the prompt, polls up to 90s for an assistant
|
|
13
|
+
* reply, and persists + broadcasts the result. On plugin offline,
|
|
14
|
+
* it falls back to a "queued" 202 response.
|
|
15
|
+
*/
|
|
16
|
+
import { Router } from 'express';
|
|
17
|
+
import {
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
appendFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
statSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { projectsStore } from '../projects-store.mjs';
|
|
28
|
+
import {
|
|
29
|
+
readServeInfo,
|
|
30
|
+
createOpencodeSession,
|
|
31
|
+
sendOpencodePrompt,
|
|
32
|
+
listOpencodeMessages,
|
|
33
|
+
extractContentFromOpencodeMessage,
|
|
34
|
+
} from '../serve-info.mjs';
|
|
35
|
+
import { wrap } from './_shared.mjs';
|
|
36
|
+
|
|
37
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,120}$/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} deps
|
|
41
|
+
* @param {object} deps.state
|
|
42
|
+
* @param {Function} deps.broadcast
|
|
43
|
+
* @returns {import('express').Router}
|
|
44
|
+
*/
|
|
45
|
+
export function createChatRouter({ state, broadcast }) {
|
|
46
|
+
const router = Router();
|
|
47
|
+
|
|
48
|
+
router.get('/chat', wrap(async (req, res) => {
|
|
49
|
+
const sessionId = req.query.session ? String(req.query.session) : null;
|
|
50
|
+
const requestedLimit = req.query.limit ? Number(req.query.limit) : 200;
|
|
51
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(requestedLimit) ? requestedLimit : 200));
|
|
52
|
+
res.json(state.getChat({ sessionId, limit }));
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// v3.5.5 — POST /api/chat now actually invokes the agent.
|
|
56
|
+
//
|
|
57
|
+
// Flow:
|
|
58
|
+
// 1. Persist the user message to the per-project .jsonl log (same
|
|
59
|
+
// as before — keeps the chat history intact even when the plugin
|
|
60
|
+
// is offline).
|
|
61
|
+
// 2. Resolve a chat session id (use the body-provided one, or mint
|
|
62
|
+
// a new `sess_*`).
|
|
63
|
+
// 3. Look up (or create) the opencode session id that backs this
|
|
64
|
+
// chat session — stored in a sidecar file at
|
|
65
|
+
// `<sessions>/<chatSessionId>.opencode.json`.
|
|
66
|
+
// 4. POST the prompt to the opencode session via the plugin's
|
|
67
|
+
// opencode serve child.
|
|
68
|
+
// 5. Poll for the assistant response (up to 90s) and persist it.
|
|
69
|
+
// 6. Broadcast both messages on the WS `chat:message` channel.
|
|
70
|
+
//
|
|
71
|
+
// Failure modes:
|
|
72
|
+
// - No active project: we still broadcast the message but skip
|
|
73
|
+
// persistence and return 202 (legacy behavior).
|
|
74
|
+
// - No serve-info: we fall back to the legacy "queued" path —
|
|
75
|
+
// the user message is persisted and broadcast but no agent
|
|
76
|
+
// invocation happens. The client can poll GET /api/chat for an
|
|
77
|
+
// eventual reply if the plugin comes up.
|
|
78
|
+
// - createSession / sendPrompt failure: 502 with the underlying
|
|
79
|
+
// error, the user message is still persisted.
|
|
80
|
+
// - Polling timeout: 202 with `timeout: true` so the client knows
|
|
81
|
+
// the agent is still working and can poll again.
|
|
82
|
+
router.post('/chat', wrap(async (req, res) => {
|
|
83
|
+
const body = req.body || {};
|
|
84
|
+
const message = typeof body.message === 'string' ? body.message.trim() : '';
|
|
85
|
+
if (!message) {
|
|
86
|
+
res.status(400).json({ error: 'bad_request', message: 'message is required' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const active = projectsStore.active();
|
|
90
|
+
|
|
91
|
+
// 1. Persist the user message to the per-project .jsonl log.
|
|
92
|
+
// (No-op when no project is active — the legacy fallback path
|
|
93
|
+
// only broadcast.)
|
|
94
|
+
let chatSessionId = null;
|
|
95
|
+
let file = null;
|
|
96
|
+
let record = null;
|
|
97
|
+
if (active) {
|
|
98
|
+
const dir = projectsStore.ensureProjectDir(active.id);
|
|
99
|
+
const sessionsDir = join(dir, 'sessions');
|
|
100
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
101
|
+
const requestedSessionId = typeof body.session === 'string' ? body.session.trim() : '';
|
|
102
|
+
chatSessionId = SESSION_ID_RE.test(requestedSessionId)
|
|
103
|
+
? requestedSessionId
|
|
104
|
+
: `sess_${Date.now().toString(36)}`;
|
|
105
|
+
file = join(sessionsDir, `${chatSessionId}.jsonl`);
|
|
106
|
+
record = {
|
|
107
|
+
id: `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
108
|
+
ts: new Date().toISOString(),
|
|
109
|
+
role: 'user',
|
|
110
|
+
agent: body.agent || null,
|
|
111
|
+
model: body.model || null,
|
|
112
|
+
content: message,
|
|
113
|
+
attachments: body.attachments || [],
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
|
|
117
|
+
} catch {
|
|
118
|
+
// best effort
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// No project — synthesize an id so the response shape is consistent.
|
|
122
|
+
const requestedSessionId = typeof body.session === 'string' ? body.session.trim() : '';
|
|
123
|
+
chatSessionId = SESSION_ID_RE.test(requestedSessionId)
|
|
124
|
+
? requestedSessionId
|
|
125
|
+
: `sess_${Date.now().toString(36)}`;
|
|
126
|
+
record = {
|
|
127
|
+
id: `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
128
|
+
ts: new Date().toISOString(),
|
|
129
|
+
role: 'user',
|
|
130
|
+
agent: body.agent || null,
|
|
131
|
+
model: body.model || null,
|
|
132
|
+
content: message,
|
|
133
|
+
attachments: body.attachments || [],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
state.appendActivity({
|
|
138
|
+
kind: 'chat.message',
|
|
139
|
+
agent: body.agent || null,
|
|
140
|
+
message: message.slice(0, 500),
|
|
141
|
+
});
|
|
142
|
+
broadcast({ type: 'chat:message', sessionId: chatSessionId, message: record });
|
|
143
|
+
|
|
144
|
+
// 2. No active project → legacy 202. Don't try to dispatch.
|
|
145
|
+
if (!active) {
|
|
146
|
+
return res.status(202).json({
|
|
147
|
+
accepted: true,
|
|
148
|
+
agent: body.agent || null,
|
|
149
|
+
queued: true,
|
|
150
|
+
reason: 'no_active_project',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. No plugin running → fall back to queued. The user message is
|
|
155
|
+
// already persisted + broadcast; the next time the plugin comes
|
|
156
|
+
// up the user can re-send or POST /api/chat/regenerate.
|
|
157
|
+
const serveInfo = readServeInfo();
|
|
158
|
+
if (!serveInfo) {
|
|
159
|
+
return res.status(202).json({
|
|
160
|
+
accepted: true,
|
|
161
|
+
agent: body.agent || null,
|
|
162
|
+
queued: true,
|
|
163
|
+
session: chatSessionId,
|
|
164
|
+
reason: 'plugin_offline',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sessionsDir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
|
|
169
|
+
const sidecarPath = join(sessionsDir, `${chatSessionId}.opencode.json`);
|
|
170
|
+
|
|
171
|
+
// 4. Resolve or create the opencode session that backs this chat.
|
|
172
|
+
let opencodeSessionId = null;
|
|
173
|
+
try {
|
|
174
|
+
if (existsSync(sidecarPath)) {
|
|
175
|
+
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'));
|
|
176
|
+
opencodeSessionId = sidecar?.opencodeSessionId || null;
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
opencodeSessionId = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!opencodeSessionId) {
|
|
183
|
+
const agentName = body.agent || active.defaultAgent || 'odin';
|
|
184
|
+
const create = await createOpencodeSession(
|
|
185
|
+
serveInfo,
|
|
186
|
+
{ title: `Chat: ${agentName}`, agent: agentName },
|
|
187
|
+
active.path || serveInfo.worktree,
|
|
188
|
+
);
|
|
189
|
+
if (!create.ok || !create.sessionId) {
|
|
190
|
+
return res.status(502).json({
|
|
191
|
+
error: 'create_session_failed',
|
|
192
|
+
message: create.error || 'failed to create opencode session',
|
|
193
|
+
session: chatSessionId,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
opencodeSessionId = create.sessionId;
|
|
197
|
+
try {
|
|
198
|
+
const sidecar = {
|
|
199
|
+
opencodeSessionId,
|
|
200
|
+
agent: agentName,
|
|
201
|
+
createdAt: Date.now(),
|
|
202
|
+
chatSessionId,
|
|
203
|
+
};
|
|
204
|
+
writeFileSync(sidecarPath, JSON.stringify(sidecar, null, 2) + '\n', 'utf8');
|
|
205
|
+
} catch {
|
|
206
|
+
// best effort
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 5. POST the prompt. We use the opencode session id as the
|
|
211
|
+
// messageID — the polling loop below uses it to detect the
|
|
212
|
+
// "before vs after" boundary.
|
|
213
|
+
const agentName = body.agent || active.defaultAgent || 'odin';
|
|
214
|
+
const send = await sendOpencodePrompt(
|
|
215
|
+
serveInfo,
|
|
216
|
+
{
|
|
217
|
+
sessionId: opencodeSessionId,
|
|
218
|
+
agent: agentName,
|
|
219
|
+
text: message,
|
|
220
|
+
messageID: record.id,
|
|
221
|
+
},
|
|
222
|
+
active.path || serveInfo.worktree,
|
|
223
|
+
);
|
|
224
|
+
if (!send.ok) {
|
|
225
|
+
return res.status(502).json({
|
|
226
|
+
error: 'send_prompt_failed',
|
|
227
|
+
message: send.error || 'failed to send prompt to opencode',
|
|
228
|
+
session: chatSessionId,
|
|
229
|
+
opencodeSessionId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 6. Poll for the assistant response. We look for the LAST
|
|
234
|
+
// assistant message; if its creation time is greater than the
|
|
235
|
+
// user's messageID timestamp, we consider it our reply. This
|
|
236
|
+
// is the same heuristic the plugin's own event stream uses.
|
|
237
|
+
const promptSentAt = Date.now();
|
|
238
|
+
const deadline = promptSentAt + 90_000;
|
|
239
|
+
let assistantRecord = null;
|
|
240
|
+
let lastSeenAssistantId = null;
|
|
241
|
+
while (Date.now() < deadline) {
|
|
242
|
+
await new Promise((r) => setTimeout(r, 2_000));
|
|
243
|
+
const list = await listOpencodeMessages(
|
|
244
|
+
serveInfo,
|
|
245
|
+
opencodeSessionId,
|
|
246
|
+
active.path || serveInfo.worktree,
|
|
247
|
+
);
|
|
248
|
+
if (!list.ok || !Array.isArray(list.messages) || list.messages.length === 0) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// Walk newest → oldest; pick the first assistant message that
|
|
252
|
+
// was created at or after promptSentAt.
|
|
253
|
+
const assistants = list.messages
|
|
254
|
+
.filter((m) => (m?.info?.role || m?.role) === 'assistant')
|
|
255
|
+
.sort((a, b) => {
|
|
256
|
+
const ta = a?.info?.time?.created || 0;
|
|
257
|
+
const tb = b?.info?.time?.created || 0;
|
|
258
|
+
return tb - ta;
|
|
259
|
+
});
|
|
260
|
+
for (const m of assistants) {
|
|
261
|
+
const created = m?.info?.time?.created || 0;
|
|
262
|
+
if (created >= promptSentAt - 1_000) {
|
|
263
|
+
const id = m?.info?.id || '';
|
|
264
|
+
if (id && id === lastSeenAssistantId) continue;
|
|
265
|
+
lastSeenAssistantId = id;
|
|
266
|
+
assistantRecord = {
|
|
267
|
+
id: id || `asst_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
|
268
|
+
ts: new Date(created || Date.now()).toISOString(),
|
|
269
|
+
role: 'assistant',
|
|
270
|
+
agent: agentName,
|
|
271
|
+
content: extractContentFromOpencodeMessage(m),
|
|
272
|
+
opencodeSessionId,
|
|
273
|
+
inReplyTo: record.id,
|
|
274
|
+
};
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (assistantRecord) break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (assistantRecord) {
|
|
282
|
+
// 7. Persist + broadcast the assistant message.
|
|
283
|
+
try {
|
|
284
|
+
appendFileSync(file, JSON.stringify(assistantRecord) + '\n', 'utf8');
|
|
285
|
+
} catch {
|
|
286
|
+
// best effort
|
|
287
|
+
}
|
|
288
|
+
broadcast({ type: 'chat:message', sessionId: chatSessionId, message: assistantRecord });
|
|
289
|
+
state.appendActivity({
|
|
290
|
+
kind: 'chat.response',
|
|
291
|
+
agent: agentName,
|
|
292
|
+
message: (assistantRecord.content || '').slice(0, 500),
|
|
293
|
+
});
|
|
294
|
+
return res.json({
|
|
295
|
+
accepted: true,
|
|
296
|
+
session: chatSessionId,
|
|
297
|
+
opencodeSessionId,
|
|
298
|
+
userMessage: record,
|
|
299
|
+
assistantMessage: assistantRecord,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 8. Timeout — the agent is still working. The next poll of GET
|
|
304
|
+
// /api/chat?session=… will pick up whatever has arrived by
|
|
305
|
+
// then. Return 202 so the client knows nothing failed.
|
|
306
|
+
return res.status(202).json({
|
|
307
|
+
accepted: true,
|
|
308
|
+
session: chatSessionId,
|
|
309
|
+
opencodeSessionId,
|
|
310
|
+
userMessage: record,
|
|
311
|
+
queued: true,
|
|
312
|
+
timeout: true,
|
|
313
|
+
});
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
router.get('/chat/sessions', wrap(async (_req, res) => {
|
|
317
|
+
const active = projectsStore.active();
|
|
318
|
+
if (!active) {
|
|
319
|
+
res.json({ sessions: [] });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const dir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
|
|
323
|
+
if (!existsSync(dir)) {
|
|
324
|
+
res.json({ sessions: [] });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const sessions = readdirSync(dir)
|
|
328
|
+
.filter((f) => f.endsWith('.jsonl'))
|
|
329
|
+
.map((f) => {
|
|
330
|
+
const st = statSync(join(dir, f));
|
|
331
|
+
return { id: f.replace(/\.jsonl$/, ''), file: f, mtime: st.mtimeMs, size: st.size };
|
|
332
|
+
});
|
|
333
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
334
|
+
res.json({ sessions });
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
// v3.0.4 — Create a new chat session. Generates an id, ensures the
|
|
338
|
+
// sessions dir + empty .jsonl file exist, and returns the session
|
|
339
|
+
// metadata. Idempotent: if the session already exists, returns it.
|
|
340
|
+
router.post('/chat/sessions', wrap(async (req, res) => {
|
|
341
|
+
const active = projectsStore.active();
|
|
342
|
+
if (!active) {
|
|
343
|
+
res.status(400).json({ error: 'no_active_project', message: 'No active project. Pick one in Overview first.' });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const requestedId = typeof req.body?.id === 'string' ? req.body.id.trim() : '';
|
|
347
|
+
const sessionId = requestedId && /^[\w-]+$/.test(requestedId)
|
|
348
|
+
? requestedId
|
|
349
|
+
: `sess_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
350
|
+
const dir = join(projectsStore.ensureProjectDir(active.id), 'sessions');
|
|
351
|
+
mkdirSync(dir, { recursive: true });
|
|
352
|
+
const file = join(dir, `${sessionId}.jsonl`);
|
|
353
|
+
if (!existsSync(file)) {
|
|
354
|
+
writeFileSync(file, '', 'utf8');
|
|
355
|
+
}
|
|
356
|
+
state.appendActivity({ kind: 'chat.session.create', id: sessionId });
|
|
357
|
+
broadcast({ type: 'chat:session:create', sessionId });
|
|
358
|
+
res.status(201).json({
|
|
359
|
+
id: sessionId,
|
|
360
|
+
file: `${sessionId}.jsonl`,
|
|
361
|
+
mtime: Date.now(),
|
|
362
|
+
size: 0,
|
|
363
|
+
});
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
// ── /api/chat/regenerate ─────────────────────────────────────────────
|
|
367
|
+
// v3.0.0: re-dispatches the last user message before messageId via POST /chat.
|
|
368
|
+
// Full opencode re-dispatch lands in v3.1 when the plugin exposes a stable HTTP API.
|
|
369
|
+
router.post('/chat/regenerate', wrap(async (req, res) => {
|
|
370
|
+
const { sessionId, messageId } = req.body || {};
|
|
371
|
+
if (!messageId) {
|
|
372
|
+
res.status(400).json({ error: 'bad_request', message: 'messageId is required' });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const active = projectsStore.active();
|
|
376
|
+
if (!active) {
|
|
377
|
+
res.status(400).json({ error: 'no_active_project', message: 'no active project' });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const dir = projectsStore.ensureProjectDir(active.id);
|
|
381
|
+
const sessionsDir = join(dir, 'sessions');
|
|
382
|
+
if (!existsSync(sessionsDir)) {
|
|
383
|
+
res.status(404).json({ error: 'not_found', message: 'no sessions found' });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const allFiles = readdirSync(sessionsDir).filter((f) => f.endsWith('.jsonl'));
|
|
387
|
+
const targetFiles = sessionId ? allFiles.filter((f) => f === `${sessionId}.jsonl`) : allFiles;
|
|
388
|
+
if (!targetFiles.length) {
|
|
389
|
+
res.status(404).json({ error: 'not_found', message: 'session not found' });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Read the session file and find the last user message before messageId
|
|
393
|
+
const full = join(sessionsDir, targetFiles[0]);
|
|
394
|
+
let lastUserMessage = null;
|
|
395
|
+
let foundTarget = false;
|
|
396
|
+
try {
|
|
397
|
+
const lines = readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
398
|
+
for (let i = 0; i < lines.length; i++) {
|
|
399
|
+
try {
|
|
400
|
+
const msg = JSON.parse(lines[i]);
|
|
401
|
+
if (msg.id === messageId || (messageId && String(msg.ts) === String(messageId))) {
|
|
402
|
+
foundTarget = true;
|
|
403
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
404
|
+
try {
|
|
405
|
+
const prev = JSON.parse(lines[j]);
|
|
406
|
+
if (prev.role === 'user') {
|
|
407
|
+
lastUserMessage = prev;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
} catch {
|
|
411
|
+
/* skip */
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
/* skip */
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
res.status(500).json({ error: 'read_failed', message: err.message });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
// Fallback: find last user message
|
|
425
|
+
if (!lastUserMessage) {
|
|
426
|
+
try {
|
|
427
|
+
const lines = readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean).reverse();
|
|
428
|
+
for (const line of lines) {
|
|
429
|
+
try {
|
|
430
|
+
const msg = JSON.parse(line);
|
|
431
|
+
if (msg.role === 'user') {
|
|
432
|
+
lastUserMessage = msg;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
/* ignore */
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
/* ignore */
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!lastUserMessage) {
|
|
444
|
+
res.status(404).json({ error: 'not_found', message: 'no user message found to regenerate' });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// Re-post via POST /chat (queued for agent processing)
|
|
448
|
+
const record = {
|
|
449
|
+
ts: new Date().toISOString(),
|
|
450
|
+
role: 'user',
|
|
451
|
+
agent: lastUserMessage.agent || null,
|
|
452
|
+
model: lastUserMessage.model || null,
|
|
453
|
+
content: lastUserMessage.content || lastUserMessage.message || '',
|
|
454
|
+
attachments: lastUserMessage.attachments || [],
|
|
455
|
+
};
|
|
456
|
+
try {
|
|
457
|
+
const lines = existsSync(full) ? readFileSync(full, 'utf8').split(/\r?\n/).filter(Boolean) : [];
|
|
458
|
+
lines.push(JSON.stringify(record));
|
|
459
|
+
writeFileSync(full, lines.join('\n') + '\n', 'utf8');
|
|
460
|
+
} catch {
|
|
461
|
+
/* best effort */
|
|
462
|
+
}
|
|
463
|
+
state.appendActivity({
|
|
464
|
+
kind: 'chat.regenerate',
|
|
465
|
+
agent: lastUserMessage.agent || null,
|
|
466
|
+
message: (lastUserMessage.content || '').slice(0, 500),
|
|
467
|
+
});
|
|
468
|
+
broadcast({ type: 'chat:regenerate', message: record });
|
|
469
|
+
res.status(202).json({ accepted: true, regeneratedMessage: record });
|
|
470
|
+
}));
|
|
471
|
+
|
|
472
|
+
// S3 — /chat/audit: thin endpoint that AuditDialog.tsx calls.
|
|
473
|
+
// The heavy lifting is the existing `/audit` slash command routed to
|
|
474
|
+
// forseti via opencode.json.template. This keeps the dialog functional
|
|
475
|
+
// without re-implementing audit logic.
|
|
476
|
+
router.post('/chat/audit', wrap(async (req, res) => {
|
|
477
|
+
res.json({
|
|
478
|
+
ok: true,
|
|
479
|
+
note: 'audit dispatched — see command.audit in config/opencode.json.template',
|
|
480
|
+
audit: { status: 'queued' },
|
|
481
|
+
});
|
|
482
|
+
}));
|
|
483
|
+
|
|
484
|
+
return router;
|
|
485
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/config.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/config — read ~/.config/opencode/opencode.json
|
|
5
|
+
* /api/config (PUT) — write opencode.json
|
|
6
|
+
* /api/config/reload (POST) — re-read
|
|
7
|
+
* /api/config/providers — list providers
|
|
8
|
+
* /api/config/providers (POST) — add
|
|
9
|
+
* /api/config/providers/:id (PUT) — update
|
|
10
|
+
* /api/config/providers/:id (DELETE) — remove
|
|
11
|
+
* /api/config/mcps — list MCP servers
|
|
12
|
+
* /api/config/mcps (POST) — add
|
|
13
|
+
* /api/config/mcps/:id (PUT) — update
|
|
14
|
+
* /api/config/mcps/:id (DELETE) — remove
|
|
15
|
+
*
|
|
16
|
+
* Two stores are involved: the on-disk opencode.json (read+write)
|
|
17
|
+
* and the providersStore/mcpsStore in-memory registries. /api/config
|
|
18
|
+
* is the former; /api/config/providers and /api/config/mcps are
|
|
19
|
+
* the latter. /api/providers (without the config/ prefix) is the
|
|
20
|
+
* dashboard's aggregated provider list and lives in providers.mjs.
|
|
21
|
+
*/
|
|
22
|
+
import { Router } from 'express';
|
|
23
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { dirname } from 'node:path';
|
|
25
|
+
import { providersStore, mcpsStore } from '../providers-store.mjs';
|
|
26
|
+
import { OPENCODE_JSON, atomicWriteJson, safeReadJSON, wrap } from './_shared.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} deps
|
|
30
|
+
* @param {object} deps.state
|
|
31
|
+
* @param {object} deps.watcher
|
|
32
|
+
* @returns {import('express').Router}
|
|
33
|
+
*/
|
|
34
|
+
export function createConfigRouter({ state, watcher }) {
|
|
35
|
+
const router = Router();
|
|
36
|
+
|
|
37
|
+
router.get('/config', wrap(async (_req, res) => {
|
|
38
|
+
const data = safeReadJSON(OPENCODE_JSON, null);
|
|
39
|
+
res.json({
|
|
40
|
+
path: OPENCODE_JSON,
|
|
41
|
+
data,
|
|
42
|
+
raw: data === null ? '' : JSON.stringify(data, null, 2),
|
|
43
|
+
exists: existsSync(OPENCODE_JSON),
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
router.put('/config', wrap(async (req, res) => {
|
|
48
|
+
const body = req.body;
|
|
49
|
+
let parsed;
|
|
50
|
+
if (typeof body === 'string') {
|
|
51
|
+
try {
|
|
52
|
+
parsed = JSON.parse(body);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
res.status(400).json({ error: 'invalid_json', message: err.message });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} else if (body && typeof body === 'object') {
|
|
58
|
+
parsed = body;
|
|
59
|
+
} else {
|
|
60
|
+
res.status(400).json({ error: 'bad_request', message: 'body must be JSON' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
mkdirSync(dirname(OPENCODE_JSON), { recursive: true });
|
|
64
|
+
atomicWriteJson(OPENCODE_JSON, parsed);
|
|
65
|
+
state.appendActivity({ kind: 'config.update' });
|
|
66
|
+
watcher.poke('change', OPENCODE_JSON);
|
|
67
|
+
res.json({ path: OPENCODE_JSON, data: parsed, exists: true, raw: JSON.stringify(parsed, null, 2) });
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
router.post('/config/reload', wrap(async (_req, res) => {
|
|
71
|
+
const data = safeReadJSON(OPENCODE_JSON, null);
|
|
72
|
+
res.json({
|
|
73
|
+
path: OPENCODE_JSON,
|
|
74
|
+
data,
|
|
75
|
+
raw: data === null ? '' : JSON.stringify(data, null, 2),
|
|
76
|
+
exists: existsSync(OPENCODE_JSON),
|
|
77
|
+
});
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// ── /api/config/providers ─────────────────────────────────────────────
|
|
81
|
+
router.get('/config/providers', wrap(async (_req, res) => {
|
|
82
|
+
res.json({ providers: providersStore.list() });
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
router.post('/config/providers', wrap(async (req, res) => {
|
|
86
|
+
const provider = providersStore.add(req.body || {});
|
|
87
|
+
res.status(201).json(provider);
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
router.put('/config/providers/:id', wrap(async (req, res) => {
|
|
91
|
+
const provider = providersStore.update(req.params.id, req.body || {});
|
|
92
|
+
res.json(provider);
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
router.delete('/config/providers/:id', wrap(async (req, res) => {
|
|
96
|
+
const ok = providersStore.remove(req.params.id);
|
|
97
|
+
if (!ok) {
|
|
98
|
+
res.status(404).json({ error: 'not_found' });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
res.status(204).end();
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
// ── /api/config/mcps ───────────────────────────────────────────────────
|
|
105
|
+
router.get('/config/mcps', wrap(async (_req, res) => {
|
|
106
|
+
res.json({ mcps: mcpsStore.list() });
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
router.post('/config/mcps', wrap(async (req, res) => {
|
|
110
|
+
const mcp = mcpsStore.add(req.body || {});
|
|
111
|
+
res.status(201).json(mcp);
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
router.put('/config/mcps/:id', wrap(async (req, res) => {
|
|
115
|
+
const mcp = mcpsStore.update(req.params.id, req.body || {});
|
|
116
|
+
res.json(mcp);
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
router.delete('/config/mcps/:id', wrap(async (req, res) => {
|
|
120
|
+
const ok = mcpsStore.remove(req.params.id);
|
|
121
|
+
if (!ok) {
|
|
122
|
+
res.status(404).json({ error: 'not_found' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
res.status(204).end();
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
return router;
|
|
129
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/diagnostics.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/diagnostics — full snapshot
|
|
5
|
+
* /api/diagnostics/health — health check
|
|
6
|
+
* /api/diagnostics/logs — tail of the active log file
|
|
7
|
+
*/
|
|
8
|
+
import { Router } from 'express';
|
|
9
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { diagnosticsStore } from '../diagnostics-store.mjs';
|
|
12
|
+
import { BIZAR_HOME, wrap } from './_shared.mjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @returns {import('express').Router}
|
|
16
|
+
*/
|
|
17
|
+
export function createDiagnosticsRouter() {
|
|
18
|
+
const router = Router();
|
|
19
|
+
|
|
20
|
+
router.get('/diagnostics', wrap(async (_req, res) => {
|
|
21
|
+
res.json(diagnosticsStore.snapshot());
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
router.get('/diagnostics/health', wrap(async (_req, res) => {
|
|
25
|
+
res.json(diagnosticsStore.health());
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
router.get('/diagnostics/logs', wrap(async (req, res) => {
|
|
29
|
+
const requestedTail = parseInt(String(req.query.tail || '100'), 10);
|
|
30
|
+
const tail = Math.min(5000, Math.max(1, Number.isFinite(requestedTail) ? requestedTail : 100));
|
|
31
|
+
const serviceLog = join(BIZAR_HOME, 'service.log');
|
|
32
|
+
const dashboardLog = join(BIZAR_HOME, 'dashboard.log');
|
|
33
|
+
// Prefer service.log, fall back to dashboard.log
|
|
34
|
+
const logFile = existsSync(serviceLog) ? serviceLog : existsSync(dashboardLog) ? dashboardLog : null;
|
|
35
|
+
if (!logFile) {
|
|
36
|
+
res.json({ lines: [], file: null, total: 0 });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const text = readFileSync(logFile, 'utf8');
|
|
41
|
+
const allLines = text.split(/\r?\n/).filter(Boolean);
|
|
42
|
+
const lines = allLines.slice(-tail);
|
|
43
|
+
res.json({ lines, file: logFile, total: allLines.length });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
res.status(500).json({ error: 'read_failed', message: err.message });
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
return router;
|
|
50
|
+
}
|