@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,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugins/bizar/src/key-rotation.ts
|
|
3
|
+
*
|
|
4
|
+
* Wraps a provider's fetch to automatically rotate through a list of API
|
|
5
|
+
* keys when the upstream returns a retryable status (429 / 402 / 5xx).
|
|
6
|
+
*
|
|
7
|
+
* Why this exists
|
|
8
|
+
* ───────────────
|
|
9
|
+
* MiniMax (like most LLM providers) applies per-key rate limits and
|
|
10
|
+
* quotas. When a key hits its limit, the request fails with 429 or 402
|
|
11
|
+
* and the agent session dies — there is no way to swap keys mid-session
|
|
12
|
+
* because the provider reads its key from auth.json once at init.
|
|
13
|
+
*
|
|
14
|
+
* opencode has no built-in multi-key rotation. This wrapper fills the gap:
|
|
15
|
+
*
|
|
16
|
+
* • Read N keys from env vars (MINIMAX_API_KEY, MINIMAX_API_KEY_2,
|
|
17
|
+
* ..., or MINIMAX_API_KEYS=key1,key2,...)
|
|
18
|
+
* • On the first attempt, use key #0
|
|
19
|
+
* • On 429/402/5xx response, retry with the next key (capped at N)
|
|
20
|
+
* • On 401 (unauthorized) and other 4xx, do NOT rotate — those are
|
|
21
|
+
* client errors that no key will fix
|
|
22
|
+
*
|
|
23
|
+
* Behaviour
|
|
24
|
+
* ─────────
|
|
25
|
+
* • Only POST requests targeting `/chat/completions` on a known provider
|
|
26
|
+
* URL are intercepted. Other requests pass through untouched.
|
|
27
|
+
* • The `Authorization` header on the outgoing request is replaced with
|
|
28
|
+
* the currently-selected key on every attempt. All other headers
|
|
29
|
+
* (Content-Type, Accept, etc.) are preserved from the SDK.
|
|
30
|
+
* • Responses are NEVER consumed by this wrapper. The caller receives
|
|
31
|
+
* the final response (success or last failure) intact, including its
|
|
32
|
+
* unconsumed body stream — safe for SSE.
|
|
33
|
+
* • On a successful response, the "current key" index advances by one so
|
|
34
|
+
* the next incoming request starts on a different key, spreading load.
|
|
35
|
+
* • On all-keys-exhausted, the last error response is returned so the
|
|
36
|
+
* opencode SDK surfaces it normally — no special handling required
|
|
37
|
+
* downstream.
|
|
38
|
+
*
|
|
39
|
+
* Concurrency
|
|
40
|
+
* ───────────
|
|
41
|
+
* Multiple requests can be in flight. The current-key index is shared
|
|
42
|
+
* state but JS is single-threaded, so a simple counter is safe. Each
|
|
43
|
+
* request reads the counter once at entry and writes it on success.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type { FetchLike } from "./reasoning-clean.js";
|
|
47
|
+
|
|
48
|
+
export interface KeyRotationOptions {
|
|
49
|
+
/** Provider id whose requests should be wrapped (e.g. "minimax"). */
|
|
50
|
+
providerId: string;
|
|
51
|
+
/**
|
|
52
|
+
* List of API keys to rotate through. Must have at least 2 entries
|
|
53
|
+
* for rotation to kick in. A single-entry list (or empty) makes the
|
|
54
|
+
* wrapper a pass-through no-op.
|
|
55
|
+
*/
|
|
56
|
+
apiKeys: string[];
|
|
57
|
+
/** Optional debug logger. */
|
|
58
|
+
debug?: (msg: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* HTTP status codes that indicate "this key is exhausted, try the next".
|
|
63
|
+
* 401 (unauthorized) is intentionally excluded — a bad key won't get
|
|
64
|
+
* better by retrying, and surfacing the auth error to the user is more
|
|
65
|
+
* useful than silently rotating past it.
|
|
66
|
+
*/
|
|
67
|
+
const RETRYABLE_STATUSES = new Set([
|
|
68
|
+
429, // Too Many Requests / rate limit
|
|
69
|
+
402, // Payment Required (quota exhausted)
|
|
70
|
+
500, 502, 503, 504, // Server errors that may be account-specific
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
/** Decide whether `url` targets the provider we should rotate for.
|
|
74
|
+
* Matches by substring on the provider id, mirroring the matcher in
|
|
75
|
+
* `reasoning-clean.ts` so the two wrappers stay consistent. */
|
|
76
|
+
function isProviderUrl(url: string, providerId: string): boolean {
|
|
77
|
+
const lower = url.toLowerCase();
|
|
78
|
+
const lp = providerId.toLowerCase();
|
|
79
|
+
return (
|
|
80
|
+
lower.includes(`/${lp}/`) ||
|
|
81
|
+
lower.includes(`/${lp}?`) ||
|
|
82
|
+
lower.includes(`${lp}.`) ||
|
|
83
|
+
lower.includes(`-${lp}.`) ||
|
|
84
|
+
lower.includes(`.${lp}/`)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Return a fresh `RequestInit` whose `Authorization` header is `Bearer <key>`.
|
|
89
|
+
* Other headers from `init` are preserved. */
|
|
90
|
+
function injectKey(init: RequestInit | undefined, key: string): RequestInit {
|
|
91
|
+
const headers = new Headers(init?.headers ?? {});
|
|
92
|
+
headers.set("Authorization", `Bearer ${key}`);
|
|
93
|
+
return { ...init, headers };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Wrap a fetch implementation so that requests to the target provider
|
|
98
|
+
* rotate through `apiKeys` on retryable status codes. Returns a function
|
|
99
|
+
* with the same signature as the original fetch.
|
|
100
|
+
*/
|
|
101
|
+
export function wrapFetchForKeyRotation(
|
|
102
|
+
originalFetch: FetchLike,
|
|
103
|
+
options: KeyRotationOptions,
|
|
104
|
+
): FetchLike {
|
|
105
|
+
const { providerId, apiKeys, debug } = options;
|
|
106
|
+
|
|
107
|
+
if (apiKeys.length < 2) {
|
|
108
|
+
debug?.(
|
|
109
|
+
`key-rotation: apiKeys has ${apiKeys.length} entries; rotation disabled (need >= 2)`,
|
|
110
|
+
);
|
|
111
|
+
return originalFetch;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let currentIndex = 0;
|
|
115
|
+
let successCount = 0;
|
|
116
|
+
let rotationCount = 0;
|
|
117
|
+
|
|
118
|
+
return async (input, init) => {
|
|
119
|
+
const url =
|
|
120
|
+
typeof input === "string"
|
|
121
|
+
? input
|
|
122
|
+
: input instanceof URL
|
|
123
|
+
? input.toString()
|
|
124
|
+
: (input as Request).url;
|
|
125
|
+
|
|
126
|
+
// Only intercept POST chat-completions on the target provider.
|
|
127
|
+
// Everything else passes through untouched so the reasoning-clean
|
|
128
|
+
// wrapper (and any other wrapper) still sees the request.
|
|
129
|
+
if (!/\/chat\/completions(?:\?|$)/.test(url)) return originalFetch(input, init);
|
|
130
|
+
if (!isProviderUrl(url, providerId)) return originalFetch(input, init);
|
|
131
|
+
if ((init?.method ?? "POST").toUpperCase() !== "POST") return originalFetch(input, init);
|
|
132
|
+
|
|
133
|
+
const startIndex = currentIndex;
|
|
134
|
+
let lastResponse: Response | null = null;
|
|
135
|
+
|
|
136
|
+
for (let attempt = 0; attempt < apiKeys.length; attempt++) {
|
|
137
|
+
const keyIndex = (startIndex + attempt) % apiKeys.length;
|
|
138
|
+
const key = apiKeys[keyIndex]!;
|
|
139
|
+
const modifiedInit = injectKey(init, key);
|
|
140
|
+
|
|
141
|
+
let response: Response;
|
|
142
|
+
try {
|
|
143
|
+
response = await originalFetch(input, modifiedInit);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
debug?.(
|
|
146
|
+
`key-rotation: fetch threw for key #${keyIndex}: ${(err as Error).message}`,
|
|
147
|
+
);
|
|
148
|
+
// Network errors also trigger retry with the next key.
|
|
149
|
+
if (attempt < apiKeys.length - 1) continue;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
lastResponse = response;
|
|
154
|
+
|
|
155
|
+
if (!RETRYABLE_STATUSES.has(response.status)) {
|
|
156
|
+
if (response.ok) {
|
|
157
|
+
successCount++;
|
|
158
|
+
// Advance the starting index so the next request begins on
|
|
159
|
+
// a different key — spreads load and avoids one hot key.
|
|
160
|
+
currentIndex = (keyIndex + 1) % apiKeys.length;
|
|
161
|
+
}
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Retryable status. Discard the response body and try the next key.
|
|
166
|
+
rotationCount++;
|
|
167
|
+
debug?.(
|
|
168
|
+
`key-rotation: key #${keyIndex} returned ${response.status}; ` +
|
|
169
|
+
`rotating to key #${(keyIndex + 1) % apiKeys.length}`,
|
|
170
|
+
);
|
|
171
|
+
// Best-effort body drain so the underlying connection can be reused.
|
|
172
|
+
try {
|
|
173
|
+
await response.body?.cancel();
|
|
174
|
+
} catch {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
debug?.(
|
|
180
|
+
`key-rotation: all ${apiKeys.length} keys exhausted; ` +
|
|
181
|
+
`returning last ${lastResponse?.status} response`,
|
|
182
|
+
);
|
|
183
|
+
return lastResponse!;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a list of MiniMax API keys from environment variables.
|
|
189
|
+
*
|
|
190
|
+
* Resolution order (first non-empty wins):
|
|
191
|
+
* 1. `MINIMAX_API_KEYS` — single env var holding a comma-separated list
|
|
192
|
+
* 2. `MINIMAX_API_KEY` + `MINIMAX_API_KEY_2`, `_3`, ..., `_16`
|
|
193
|
+
* 3. Empty array (rotation disabled)
|
|
194
|
+
*
|
|
195
|
+
* Whitespace around entries is trimmed; empty strings are filtered out.
|
|
196
|
+
* At most 16 keys are returned as a defensive cap so a misconfigured
|
|
197
|
+
* env var (e.g. an accidental comma-spam) cannot drive runaway rotation.
|
|
198
|
+
*/
|
|
199
|
+
export function discoverMiniMaxKeys(
|
|
200
|
+
env: Record<string, string | undefined> = process.env,
|
|
201
|
+
): string[] {
|
|
202
|
+
const explicit = env.MINIMAX_API_KEYS;
|
|
203
|
+
if (explicit) {
|
|
204
|
+
return explicit
|
|
205
|
+
.split(",")
|
|
206
|
+
.map((s) => s.trim())
|
|
207
|
+
.filter((s) => s.length > 0)
|
|
208
|
+
.slice(0, 16);
|
|
209
|
+
}
|
|
210
|
+
const keys: string[] = [];
|
|
211
|
+
const primary = env.MINIMAX_API_KEY;
|
|
212
|
+
if (primary && primary.trim().length > 0) keys.push(primary.trim());
|
|
213
|
+
for (let i = 2; i <= 16; i++) {
|
|
214
|
+
const k = env[`MINIMAX_API_KEY_${i}`];
|
|
215
|
+
if (k && k.trim().length > 0) keys.push(k.trim());
|
|
216
|
+
}
|
|
217
|
+
return keys;
|
|
218
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger — thin wrapper over `client.app.log`.
|
|
3
|
+
*
|
|
4
|
+
* Spec requirements satisfied here:
|
|
5
|
+
* - §6.5 — honors `BIZAR_LOG_LEVEL` env var (`debug` | `info` | `warn` | `error`,
|
|
6
|
+
* default `info`, invalid values fall back to `info` with a warning).
|
|
7
|
+
* - §7.6 — never accepts or emits raw tool args / session content. Every call
|
|
8
|
+
* to `client.app.log` is `{ level, message }` (or the SDK's wrapped
|
|
9
|
+
* `{ body: { service, level, message } }` shape) where `message` is a
|
|
10
|
+
* static string the caller constructed. There is no second argument,
|
|
11
|
+
* no metadata object, no structured logging payload.
|
|
12
|
+
*
|
|
13
|
+
* The logger swallows all errors from the underlying `client.app.log` call.
|
|
14
|
+
* A failed log MUST NOT crash the plugin or block the tool hook.
|
|
15
|
+
*
|
|
16
|
+
* The interface is intentionally compatible with the `Logger` shape that
|
|
17
|
+
* Thor's `StateStore` and `LogWriter` accept: a single `log(opts)` method
|
|
18
|
+
* returning `void` (synchronous). We also expose level-specific convenience
|
|
19
|
+
* methods (`debug` / `info` / `warn` / `error`) that all delegate to `log`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
23
|
+
|
|
24
|
+
const LEVEL_ORDER: Record<LogLevel, number> = {
|
|
25
|
+
debug: 0,
|
|
26
|
+
info: 1,
|
|
27
|
+
warn: 2,
|
|
28
|
+
error: 3,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimal client surface the logger depends on. The real opencode client's
|
|
33
|
+
* `app.log` is a generated SDK method with a complex Options<T> type; we
|
|
34
|
+
* accept any object with a compatible `app.log` method and treat the body
|
|
35
|
+
* as an opaque `unknown` at the boundary. The logger internally shapes the
|
|
36
|
+
* call to the SDK's expected `{ body: { service, level, message } }` form.
|
|
37
|
+
*/
|
|
38
|
+
export interface LoggerClient {
|
|
39
|
+
app: {
|
|
40
|
+
log: (input: unknown) => unknown;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The Logger interface matches the one defined in `state.ts` and `report.ts`
|
|
46
|
+
* (Thor's modules). A single `log(opts)` method, synchronous, returns `void`.
|
|
47
|
+
* The convenience methods are additive.
|
|
48
|
+
*/
|
|
49
|
+
export interface Logger {
|
|
50
|
+
log(opts: { level: LogLevel; message: string }): void;
|
|
51
|
+
debug(message: string): void;
|
|
52
|
+
info(message: string): void;
|
|
53
|
+
warn(message: string): void;
|
|
54
|
+
error(message: string): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ParsedLogLevel {
|
|
58
|
+
level: LogLevel;
|
|
59
|
+
wasInvalid: boolean;
|
|
60
|
+
original: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseLogLevel(raw: string | undefined): ParsedLogLevel {
|
|
64
|
+
if (raw === undefined || raw === "") {
|
|
65
|
+
return { level: "info", wasInvalid: false, original: raw };
|
|
66
|
+
}
|
|
67
|
+
const lower = raw.toLowerCase();
|
|
68
|
+
if (lower === "debug" || lower === "info" || lower === "warn" || lower === "error") {
|
|
69
|
+
return { level: lower, wasInvalid: false, original: raw };
|
|
70
|
+
}
|
|
71
|
+
return { level: "info", wasInvalid: true, original: raw };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readEnvLevel(): string | undefined {
|
|
75
|
+
try {
|
|
76
|
+
return typeof process !== "undefined" && process.env
|
|
77
|
+
? process.env.BIZAR_LOG_LEVEL
|
|
78
|
+
: undefined;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Service name stamped onto every log entry. Lets users filter app logs
|
|
86
|
+
* by service (e.g. `bizar`) in their diagnostic tooling.
|
|
87
|
+
*/
|
|
88
|
+
const SERVICE_NAME = "bizar";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Construct a Logger bound to the opencode client.
|
|
92
|
+
*
|
|
93
|
+
* @param client The opencode client (we use only `client.app.log`).
|
|
94
|
+
* @param envLevel Optional override for `BIZAR_LOG_LEVEL`. If omitted, the
|
|
95
|
+
* env var is read once. Env vars are read at plugin init;
|
|
96
|
+
* mid-session changes are ignored (spec §6.5).
|
|
97
|
+
*/
|
|
98
|
+
export function createLogger(client: LoggerClient, envLevel?: string): Logger {
|
|
99
|
+
const raw = envLevel ?? readEnvLevel();
|
|
100
|
+
const parsed = parseLogLevel(raw);
|
|
101
|
+
const threshold = parsed.level;
|
|
102
|
+
|
|
103
|
+
if (parsed.wasInvalid) {
|
|
104
|
+
// Best-effort: warn about the invalid value, then proceed with "info".
|
|
105
|
+
// We call `client.app.log` directly because the logger is not yet wired
|
|
106
|
+
// to the threshold.
|
|
107
|
+
try {
|
|
108
|
+
client.app.log({
|
|
109
|
+
body: { service: SERVICE_NAME, level: "warn", message: `bizar: invalid BIZAR_LOG_LEVEL "${parsed.original ?? ""}", falling back to "info"` },
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore — never let a warning crash init
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function emit(msgLevel: LogLevel, message: string): void {
|
|
117
|
+
if (LEVEL_ORDER[msgLevel] < LEVEL_ORDER[threshold]) return;
|
|
118
|
+
try {
|
|
119
|
+
client.app.log({
|
|
120
|
+
body: { service: SERVICE_NAME, level: msgLevel, message },
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
// Swallow — a failed log MUST NOT crash the plugin.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
log(opts) {
|
|
129
|
+
emit(opts.level, opts.message);
|
|
130
|
+
},
|
|
131
|
+
debug(message) {
|
|
132
|
+
emit("debug", message);
|
|
133
|
+
},
|
|
134
|
+
info(message) {
|
|
135
|
+
emit("info", message);
|
|
136
|
+
},
|
|
137
|
+
warn(message) {
|
|
138
|
+
emit("warn", message);
|
|
139
|
+
},
|
|
140
|
+
error(message) {
|
|
141
|
+
emit("error", message);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop — the threshold decision tree.
|
|
3
|
+
*
|
|
4
|
+
* Spec requirements satisfied here:
|
|
5
|
+
* - §5.1 — algorithm. Count how many of the last `loopWindowSize` tool
|
|
6
|
+
* calls share the given fingerprint. Re-scan the window each call.
|
|
7
|
+
* - §5.4 — threshold table. The action returned by `decide()` corresponds
|
|
8
|
+
* to the highest threshold that the count meets or exceeds.
|
|
9
|
+
* - §5.5 — window vs counter semantics. The count is a re-scan of the
|
|
10
|
+
* window, not a running counter. The intermediate non-matching calls
|
|
11
|
+
* in the window do NOT "reset" anything; they just sit in the window
|
|
12
|
+
* and shift older matching entries out as the window rolls forward.
|
|
13
|
+
* - §7.2 — the `reason` field returned by `decide()` for warn/escalate/
|
|
14
|
+
* block actions is a static message from `handoff.ts` with only the
|
|
15
|
+
* tool name interpolated. No tool args, no LLM output, no agent-
|
|
16
|
+
* controlled content is ever included.
|
|
17
|
+
*
|
|
18
|
+
* The `now` parameter is part of the public signature for API consistency
|
|
19
|
+
* with the spec (§5.1 timing context) but is not consumed by the decision
|
|
20
|
+
* logic — callers use it to timestamp the current tool call when adding it
|
|
21
|
+
* to the state.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { SessionState } from "./state.js";
|
|
25
|
+
import type { NormalizedOptions } from "./options.js";
|
|
26
|
+
import {
|
|
27
|
+
warnMessage,
|
|
28
|
+
escalateMessage,
|
|
29
|
+
blockMessage,
|
|
30
|
+
} from "./handoff.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The decision returned by `decide()`. The `allow` variant carries no
|
|
34
|
+
* payload; the three action variants carry the canonical handoff message
|
|
35
|
+
* (via `reason`), the fingerprint that triggered the decision, and the
|
|
36
|
+
* repetition count in the window.
|
|
37
|
+
*/
|
|
38
|
+
export type Decision =
|
|
39
|
+
| { action: "allow" }
|
|
40
|
+
| {
|
|
41
|
+
action: "warn" | "escalate" | "block";
|
|
42
|
+
reason: string;
|
|
43
|
+
fingerprint: string;
|
|
44
|
+
count: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Implicit "log a warning" threshold from spec §5.4 row 1. This is always
|
|
49
|
+
* two below `loopThresholdWarn` so the gap between log-only and inject-warn
|
|
50
|
+
* stays constant when the user reconfigures. We `max(1, …)` so a user who
|
|
51
|
+
* sets `loopThresholdWarn = 1` does not push the log-only threshold below 1.
|
|
52
|
+
*/
|
|
53
|
+
function logOnlyThreshold(options: NormalizedOptions): number {
|
|
54
|
+
return Math.max(1, options.loopThresholdWarn - 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Look at the last `loopWindowSize` tool calls and return those that share
|
|
59
|
+
* the given fingerprint. The window is a slice of the most recent entries;
|
|
60
|
+
* older entries are ignored (spec §5.1 step 3).
|
|
61
|
+
*/
|
|
62
|
+
function windowMatches(
|
|
63
|
+
state: SessionState,
|
|
64
|
+
fingerprint: string,
|
|
65
|
+
windowSize: number,
|
|
66
|
+
): { calls: SessionState["toolCalls"]; count: number } {
|
|
67
|
+
if (state.toolCalls.length === 0 || windowSize <= 0) {
|
|
68
|
+
return { calls: [], count: 0 };
|
|
69
|
+
}
|
|
70
|
+
const start = Math.max(0, state.toolCalls.length - windowSize);
|
|
71
|
+
const window = state.toolCalls.slice(start);
|
|
72
|
+
const matches = window.filter((c) => c.fingerprint === fingerprint);
|
|
73
|
+
return { calls: matches, count: matches.length };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Recover the tool name from any call in `matches` that shares the
|
|
78
|
+
* fingerprint. The fingerprint is computed from `(tool, normalized args)`,
|
|
79
|
+
* so every call with this fingerprint has the same tool name. If the
|
|
80
|
+
* matches list is empty (should not happen when this is called — the
|
|
81
|
+
* caller only invokes us when `count >= 1`), fall back to `"unknown"`.
|
|
82
|
+
*/
|
|
83
|
+
function toolNameOf(matches: SessionState["toolCalls"]): string {
|
|
84
|
+
const first = matches[0];
|
|
85
|
+
return first ? first.tool : "unknown";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a Decision payload for a non-allow action. Pulls the canonical
|
|
90
|
+
* handoff message for the given action and the tool name recovered from
|
|
91
|
+
* the matching window entries.
|
|
92
|
+
*/
|
|
93
|
+
function payload(
|
|
94
|
+
action: "warn" | "escalate" | "block",
|
|
95
|
+
fingerprint: string,
|
|
96
|
+
count: number,
|
|
97
|
+
matches: SessionState["toolCalls"],
|
|
98
|
+
): Decision {
|
|
99
|
+
const tool = toolNameOf(matches);
|
|
100
|
+
const reason =
|
|
101
|
+
action === "block"
|
|
102
|
+
? blockMessage(tool)
|
|
103
|
+
: action === "escalate"
|
|
104
|
+
? escalateMessage(tool)
|
|
105
|
+
: warnMessage(tool);
|
|
106
|
+
return { action, reason, fingerprint, count };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Decide what action to take for the current tool call.
|
|
111
|
+
*
|
|
112
|
+
* The function is pure: it does not mutate `state` and does not perform any
|
|
113
|
+
* I/O. The caller (the `tool.execute.before` hook in `index.ts`) is
|
|
114
|
+
* responsible for adding the current call to the state before invoking
|
|
115
|
+
* `decide()`, and for acting on the returned decision (log / inject / throw).
|
|
116
|
+
*
|
|
117
|
+
* @param state The current session state. Must already include the
|
|
118
|
+
* current tool call in `state.toolCalls` (the caller
|
|
119
|
+
* appends it before calling `decide()`).
|
|
120
|
+
* @param fingerprint The fingerprint of the current tool call.
|
|
121
|
+
* @param now Current epoch milliseconds. Part of the public signature
|
|
122
|
+
* for API consistency; not consumed by the decision logic.
|
|
123
|
+
* @param options Normalized plugin options.
|
|
124
|
+
*/
|
|
125
|
+
export function decide(
|
|
126
|
+
state: SessionState,
|
|
127
|
+
fingerprint: string,
|
|
128
|
+
now: number,
|
|
129
|
+
options: NormalizedOptions,
|
|
130
|
+
): Decision {
|
|
131
|
+
// Step 1: scan the window.
|
|
132
|
+
const { calls: matches, count } = windowMatches(
|
|
133
|
+
state,
|
|
134
|
+
fingerprint,
|
|
135
|
+
options.loopWindowSize,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Step 2: apply thresholds from highest to lowest.
|
|
139
|
+
if (count >= options.loopThresholdBlock) {
|
|
140
|
+
return payload("block", fingerprint, count, matches);
|
|
141
|
+
}
|
|
142
|
+
if (count >= options.loopThresholdEscalate) {
|
|
143
|
+
return payload("escalate", fingerprint, count, matches);
|
|
144
|
+
}
|
|
145
|
+
if (count >= options.loopThresholdWarn) {
|
|
146
|
+
return payload("warn", fingerprint, count, matches);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Step 3: log-only band (spec §5.4 row 1). Sits between `allow` and the
|
|
150
|
+
// inject-warn threshold. The caller distinguishes this case from
|
|
151
|
+
// inject-warn by inspecting `count` against `options.loopThresholdWarn`.
|
|
152
|
+
const logOnly = logOnlyThreshold(options);
|
|
153
|
+
if (count >= logOnly) {
|
|
154
|
+
return payload("warn", fingerprint, count, matches);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// `now` is accepted for API consistency; reference it so strict no-unused
|
|
158
|
+
// linters do not flag the parameter.
|
|
159
|
+
void now;
|
|
160
|
+
|
|
161
|
+
return { action: "allow" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convenience helper for the caller: given a `warn` decision, was this the
|
|
166
|
+
* log-only band (no injection) or the inject-warn band? The caller uses
|
|
167
|
+
* this to decide whether to write a `client.app.log` line or to set a
|
|
168
|
+
* pending system-transform injection.
|
|
169
|
+
*/
|
|
170
|
+
export function isLogOnlyWarn(
|
|
171
|
+
decision: Decision,
|
|
172
|
+
options: NormalizedOptions,
|
|
173
|
+
): boolean {
|
|
174
|
+
if (decision.action !== "warn") return false;
|
|
175
|
+
return decision.count < options.loopThresholdWarn;
|
|
176
|
+
}
|