@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,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/dialogs.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.9.0 — Dialog-request HTTP routes.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/dialogs — list recent dialog history (debug)
|
|
7
|
+
* GET /api/dialogs/queue — list currently-queued (undelivered) dialogs
|
|
8
|
+
* POST /api/dialogs — enqueue a dialog (used by the slash-command plugin)
|
|
9
|
+
* DELETE /api/dialogs/:id — remove a dialog from the queue
|
|
10
|
+
*
|
|
11
|
+
* WIRING (REQUIRED — read carefully):
|
|
12
|
+
*
|
|
13
|
+
* This router is OWNED BY THE DIALOG SCOPE (Tyr). The route is
|
|
14
|
+
* MOUNTED BY THE SLASH-COMMAND SIBLING SCOPE (Thor). Thor owns
|
|
15
|
+
* `server.mjs`; I do NOT modify that file.
|
|
16
|
+
*
|
|
17
|
+
* When Thor lands, they need to add the following to `server.mjs`:
|
|
18
|
+
*
|
|
19
|
+
* import { createDialogsRouter } from './routes/dialogs.mjs';
|
|
20
|
+
* import { startDialogPoller } from './dialog-poller.mjs';
|
|
21
|
+
*
|
|
22
|
+
* // …inside createServer or whatever the bootstrap is…
|
|
23
|
+
* app.use(createDialogsRouter({ broadcast }));
|
|
24
|
+
* startDialogPoller();
|
|
25
|
+
*
|
|
26
|
+
* Until that's done, the router is wired but unreachable and the
|
|
27
|
+
* poller never starts. This is intentional — sibling separation
|
|
28
|
+
* prevents both of us from racing on the same file.
|
|
29
|
+
*
|
|
30
|
+
* Service-mode gap: `cli/service.mjs` also does NOT start the dialog
|
|
31
|
+
* poller. If you want slash-command dialogs to flow in a service-only
|
|
32
|
+
* deployment (no dashboard), add `startDialogPoller()` to
|
|
33
|
+
* `cli/service.mjs`. Out of scope for this PR.
|
|
34
|
+
*/
|
|
35
|
+
import { Router } from 'express';
|
|
36
|
+
import { dialogStore } from '../dialog-store.mjs';
|
|
37
|
+
import { wrap } from './_shared.mjs';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} deps
|
|
41
|
+
* @param {Function} deps.broadcast
|
|
42
|
+
* @returns {import('express').Router}
|
|
43
|
+
*/
|
|
44
|
+
export function createDialogsRouter({ broadcast }) {
|
|
45
|
+
const router = Router();
|
|
46
|
+
|
|
47
|
+
// Debug / introspection — what dialogs are currently queued?
|
|
48
|
+
router.get('/dialogs/queue', wrap(async (_req, res) => {
|
|
49
|
+
const queue = dialogStore.list();
|
|
50
|
+
res.json({ count: queue.length, queue });
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// Debug / history — what's been delivered recently? Since dialogs
|
|
54
|
+
// are one-shot and removed after broadcast, this endpoint returns
|
|
55
|
+
// the *currently queued* set as well; a true history view would
|
|
56
|
+
// need a separate append-only log (out of scope here).
|
|
57
|
+
router.get('/dialogs', wrap(async (_req, res) => {
|
|
58
|
+
const queue = dialogStore.list();
|
|
59
|
+
res.json({ dialogs: queue, dir: dialogStore.DIALOG_DIR });
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Used by the slash-command plugin (Thor owns that, I just provide
|
|
63
|
+
// the route). The plugin POSTs the dialog descriptor and the route
|
|
64
|
+
// persists it; the poller picks it up on the next tick.
|
|
65
|
+
router.post('/dialogs', wrap(async (req, res) => {
|
|
66
|
+
const dialog = dialogStore.add(req.body || {});
|
|
67
|
+
// Best-effort immediate broadcast — the poller will re-broadcast on
|
|
68
|
+
// the next tick if this fails, but a successful immediate broadcast
|
|
69
|
+
// cuts latency to ~ms for already-connected dashboards.
|
|
70
|
+
try {
|
|
71
|
+
broadcast({ type: 'dialog:show', dialog });
|
|
72
|
+
} catch { /* best-effort */ }
|
|
73
|
+
res.status(201).json(dialog);
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
router.delete('/dialogs/:id', wrap(async (req, res) => {
|
|
77
|
+
const ok = dialogStore.remove(req.params.id);
|
|
78
|
+
if (!ok) {
|
|
79
|
+
res.status(404).json({ error: 'not_found' });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.status(204).end();
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
return router;
|
|
86
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/fs.mjs
|
|
3
|
+
*
|
|
4
|
+
* GET /api/fs?path=<absolute> — list directory entries
|
|
5
|
+
* POST /api/fs/mkdir — create a new subdirectory
|
|
6
|
+
*
|
|
7
|
+
* Read-only filesystem browser for the Add Project dialog. The
|
|
8
|
+
* frontend passes an absolute path; the server resolves it against
|
|
9
|
+
* the allow-list (`os.homedir()` + the user's configured
|
|
10
|
+
* `dashboard.projectsDirectory` + each entry in
|
|
11
|
+
* `dashboard.allowedRoots`) and returns the immediate children.
|
|
12
|
+
*
|
|
13
|
+
* Security boundaries:
|
|
14
|
+
* - Only absolute, resolved paths under an allowed root are
|
|
15
|
+
* honoured. Anything else returns 403.
|
|
16
|
+
* - First-level dotdirs under home (`.ssh`, `.aws`, `.gnupg`,
|
|
17
|
+
* …) are silently skipped in listings and refused as roots.
|
|
18
|
+
* - The GET endpoint never writes to disk. The POST endpoint
|
|
19
|
+
* creates exactly one new directory; nothing else.
|
|
20
|
+
* - All filesystem errors map to structured JSON, never an
|
|
21
|
+
* unhandled exception.
|
|
22
|
+
* - GET responses are capped at `MAX_ENTRIES` so a directory
|
|
23
|
+
* with millions of children (a `node_modules` clone, a Go
|
|
24
|
+
* module cache) cannot OOM the browser.
|
|
25
|
+
*/
|
|
26
|
+
import { Router } from 'express';
|
|
27
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
28
|
+
import { dirname, join } from 'node:path';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { wrap, readSettings } from './_shared.mjs';
|
|
31
|
+
import {
|
|
32
|
+
resolveSafePath,
|
|
33
|
+
buildAllowedRootsFromSettings,
|
|
34
|
+
} from '../lib/path-safe.mjs';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* v3.11.0 — Hard cap on the number of entries returned by GET
|
|
38
|
+
* /api/fs. Sliced (not paginated) to keep the response shape
|
|
39
|
+
* stable and the round-trip cheap; the client gets a `truncated`
|
|
40
|
+
* flag so it can show "showing 500 of N" feedback.
|
|
41
|
+
*/
|
|
42
|
+
const MAX_ENTRIES = 500;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* v3.11.0 — Hard cap on the new-directory name length. 255 chars
|
|
46
|
+
* matches the typical ext4/NTFS max-component limit; longer names
|
|
47
|
+
* are almost certainly a bug or an attack.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_NAME_LENGTH = 255;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the per-request allow-list. We always include `os.homedir()`,
|
|
53
|
+
* and additionally include every entry in `settings.dashboard.allowedRoots`
|
|
54
|
+
* plus `settings.dashboard.projectsDirectory` — but only after
|
|
55
|
+
* re-validating each one against home so a tampered settings file
|
|
56
|
+
* can't widen the boundary.
|
|
57
|
+
*
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
function buildAllowedRoots() {
|
|
61
|
+
const settings = readSettings();
|
|
62
|
+
return buildAllowedRootsFromSettings({
|
|
63
|
+
settings: settings.data,
|
|
64
|
+
home: homedir(),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decide whether a given directory entry should be visible to the
|
|
70
|
+
* file browser. We hide dotfiles / dotdirs at every depth — keeps
|
|
71
|
+
* the browser focused on real user-visible projects and prevents
|
|
72
|
+
* accidental click-throughs into things like `node_modules/.cache`.
|
|
73
|
+
*
|
|
74
|
+
* (Dotdir *roots* under home are blocked at a different layer by
|
|
75
|
+
* resolveSafePath; this is the in-listing filter.)
|
|
76
|
+
*
|
|
77
|
+
* @param {string} name
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function isHiddenEntry(name) {
|
|
81
|
+
return typeof name === 'string' && name.startsWith('.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sort entries: directories first (case-insensitive), then files
|
|
86
|
+
* (case-insensitive). Stable for ties.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ isDir: boolean, name: string }[]} entries
|
|
89
|
+
*/
|
|
90
|
+
function sortEntries(entries) {
|
|
91
|
+
return [...entries].sort((a, b) => {
|
|
92
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
93
|
+
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* v3.11.0 — Validate a new-directory name. Defense in depth: the
|
|
99
|
+
* frontend should validate too, but the server is the boundary.
|
|
100
|
+
*
|
|
101
|
+
* Returns `null` on success, or a human-readable error message on
|
|
102
|
+
* failure. Designed to surface as `400 { error: "bad_request",
|
|
103
|
+
* message }` to the client.
|
|
104
|
+
*
|
|
105
|
+
* Rules:
|
|
106
|
+
* - must be a non-empty string
|
|
107
|
+
* - length 1..255
|
|
108
|
+
* - must NOT be `.` or `..`
|
|
109
|
+
* - must NOT start with `-` (could be confused with a CLI flag)
|
|
110
|
+
* - must NOT contain `/`, `\`, NUL, `:`, `*`, `?`, `"`, `<`, `>`, `|`
|
|
111
|
+
*
|
|
112
|
+
* @param {unknown} name
|
|
113
|
+
* @returns {string | null}
|
|
114
|
+
*/
|
|
115
|
+
function validateDirName(name) {
|
|
116
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
117
|
+
return 'name is required';
|
|
118
|
+
}
|
|
119
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
120
|
+
return `name is too long (max ${MAX_NAME_LENGTH} characters)`;
|
|
121
|
+
}
|
|
122
|
+
if (name === '.' || name === '..') {
|
|
123
|
+
return 'name may not be "." or ".."';
|
|
124
|
+
}
|
|
125
|
+
if (name.startsWith('-')) {
|
|
126
|
+
return 'name may not start with "-"';
|
|
127
|
+
}
|
|
128
|
+
// Denylist of characters that have a special meaning on the shell,
|
|
129
|
+
// Windows, or POSIX paths. Listed as discrete substrings (some are
|
|
130
|
+
// multi-character escapes) for an explicit denylist with clear
|
|
131
|
+
// error messages.
|
|
132
|
+
const denylist = ['/', '\\', '\0', ':', '*', '?', '"', '<', '>', '|'];
|
|
133
|
+
for (const c of denylist) {
|
|
134
|
+
if (name.includes(c)) {
|
|
135
|
+
return `name may not contain "${c}"`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* v3.11.0 — Build the absolute path of the would-be new directory,
|
|
143
|
+
* verifying it remains inside the allow-list. A symlink or
|
|
144
|
+
* pre-existing weirdness in the parent could in principle make
|
|
145
|
+
* `path.join` produce a target that escapes; the re-check is
|
|
146
|
+
* cheap and prevents the race.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} safeParent
|
|
149
|
+
* @param {string} name
|
|
150
|
+
* @param {string[]} allowedRoots
|
|
151
|
+
* @returns {string | null}
|
|
152
|
+
*/
|
|
153
|
+
function resolveTargetUnderParent(safeParent, name, allowedRoots) {
|
|
154
|
+
const target = join(safeParent, name);
|
|
155
|
+
return resolveSafePath(target, allowedRoots);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {object} deps
|
|
160
|
+
* @param {object} deps.state
|
|
161
|
+
* @returns {import('express').Router}
|
|
162
|
+
*/
|
|
163
|
+
export function createFsRouter({ state }) {
|
|
164
|
+
const router = Router();
|
|
165
|
+
|
|
166
|
+
router.get('/fs', wrap(async (req, res) => {
|
|
167
|
+
const requested = typeof req.query.path === 'string' ? req.query.path : '';
|
|
168
|
+
const allowedRoots = buildAllowedRoots();
|
|
169
|
+
const safe = resolveSafePath(requested, allowedRoots);
|
|
170
|
+
if (!safe) {
|
|
171
|
+
res.status(403).json({
|
|
172
|
+
error: 'forbidden',
|
|
173
|
+
message: 'path is outside the allowed roots (home + dashboard.projectsDirectory + dashboard.allowedRoots)',
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let dirStat;
|
|
179
|
+
try {
|
|
180
|
+
dirStat = await stat(safe);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
if (err && err.code === 'ENOENT') {
|
|
183
|
+
res.status(404).json({ error: 'not_found' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
187
|
+
res.status(403).json({ error: 'permission_denied' });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!dirStat.isDirectory()) {
|
|
194
|
+
res.status(400).json({ error: 'not_a_directory' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let dirents;
|
|
199
|
+
try {
|
|
200
|
+
dirents = await readdir(safe, { withFileTypes: true });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err && err.code === 'ENOENT') {
|
|
203
|
+
res.status(404).json({ error: 'not_found' });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
207
|
+
res.status(403).json({ error: 'permission_denied' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const entries = [];
|
|
214
|
+
for (const dirent of dirents) {
|
|
215
|
+
const name = dirent.name;
|
|
216
|
+
// Hide dotfile / dotdir entries at every depth.
|
|
217
|
+
if (isHiddenEntry(name)) continue;
|
|
218
|
+
const sep = safe.includes('\\') && !safe.includes('/') ? '\\' : '/';
|
|
219
|
+
const entryPath = safe.endsWith(sep) ? safe + name : safe + sep + name;
|
|
220
|
+
// Defaults — used when stat fails. dirent.isDirectory() is a
|
|
221
|
+
// cheap, reliable indicator that does not require an extra
|
|
222
|
+
// syscall, so prefer it for isDir.
|
|
223
|
+
let isDir = dirent.isDirectory();
|
|
224
|
+
let isSymlink = dirent.isSymbolicLink();
|
|
225
|
+
try {
|
|
226
|
+
// Follow the symlink (if any) so the user can see whether
|
|
227
|
+
// it resolves to a real directory.
|
|
228
|
+
const s = await stat(entryPath);
|
|
229
|
+
if (s.isDirectory()) isDir = true;
|
|
230
|
+
if (s.isSymbolicLink()) isSymlink = true;
|
|
231
|
+
} catch {
|
|
232
|
+
// Best-effort. Common cases:
|
|
233
|
+
// - EACCES on a sibling we can't stat (parent only readable
|
|
234
|
+
// to us, entry mode 000). Trust the dirent.
|
|
235
|
+
// - ENOENT means the entry disappeared between readdir and
|
|
236
|
+
// stat (rare race). Drop it.
|
|
237
|
+
if (dirent.isSymbolicLink() && !isDir) {
|
|
238
|
+
// Symlink whose target we can't read — show it as a
|
|
239
|
+
// symlink, not a directory, so the user knows to be
|
|
240
|
+
// careful before entering.
|
|
241
|
+
isDir = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
entries.push({ name, path: entryPath, isDir, isSymlink });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const sorted = sortEntries(entries);
|
|
248
|
+
const totalEntries = sorted.length;
|
|
249
|
+
// v3.11.0 — Cap the response so a 50k-entry `node_modules`
|
|
250
|
+
// clone cannot OOM the browser. We always include the
|
|
251
|
+
// `truncated` flag (and `totalEntries`) for type stability —
|
|
252
|
+
// the client can rely on the shape even when no truncation
|
|
253
|
+
// happened.
|
|
254
|
+
const truncated = totalEntries > MAX_ENTRIES;
|
|
255
|
+
const capped = truncated ? sorted.slice(0, MAX_ENTRIES) : sorted;
|
|
256
|
+
const parent = dirname(safe);
|
|
257
|
+
// The "parent" we return should be null when we're already at
|
|
258
|
+
// the filesystem root — Express/Node will give us '/' there.
|
|
259
|
+
const isRoot = parent === safe;
|
|
260
|
+
res.json({
|
|
261
|
+
path: safe,
|
|
262
|
+
parent: isRoot ? null : parent,
|
|
263
|
+
entries: capped,
|
|
264
|
+
truncated,
|
|
265
|
+
totalEntries,
|
|
266
|
+
});
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* v3.11.0 — Create a new directory. This is the FIRST write
|
|
271
|
+
* endpoint in the file-browser surface, so it is paranoid about
|
|
272
|
+
* race conditions: check-then-create (EEXIST → 409), and
|
|
273
|
+
* validate the result via `stat` after creation.
|
|
274
|
+
*
|
|
275
|
+
* Auth: mounted under `/api`, so `requireAuth` in server.mjs
|
|
276
|
+
* gates this automatically.
|
|
277
|
+
*
|
|
278
|
+
* Rate limiting: not yet implemented at the server side. The
|
|
279
|
+
* frontend should debounce the click. Follow-up: add a per-IP
|
|
280
|
+
* token bucket if abuse becomes a concern.
|
|
281
|
+
*/
|
|
282
|
+
router.post('/fs/mkdir', wrap(async (req, res) => {
|
|
283
|
+
const parent = typeof req.body?.parent === 'string' ? req.body.parent : '';
|
|
284
|
+
const name = typeof req.body?.name === 'string' ? req.body.name : '';
|
|
285
|
+
|
|
286
|
+
if (!parent) {
|
|
287
|
+
res.status(400).json({
|
|
288
|
+
error: 'bad_request',
|
|
289
|
+
message: 'parent is required',
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const allowedRoots = buildAllowedRoots();
|
|
295
|
+
|
|
296
|
+
// 1. Parent must be inside the allow-list.
|
|
297
|
+
const safeParent = resolveSafePath(parent, allowedRoots);
|
|
298
|
+
if (!safeParent) {
|
|
299
|
+
res.status(403).json({
|
|
300
|
+
error: 'forbidden',
|
|
301
|
+
message: 'parent is outside the allowed roots',
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 2. Name must pass the denylist validation.
|
|
307
|
+
const nameError = validateDirName(name);
|
|
308
|
+
if (nameError) {
|
|
309
|
+
res.status(400).json({ error: 'bad_request', message: nameError });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 3. Compute target and re-validate it is in the allow-list
|
|
314
|
+
// (defense against symlinks or pre-existing weirdness in
|
|
315
|
+
// the parent).
|
|
316
|
+
const safeTarget = resolveTargetUnderParent(safeParent, name, allowedRoots);
|
|
317
|
+
if (!safeTarget) {
|
|
318
|
+
res.status(403).json({
|
|
319
|
+
error: 'forbidden',
|
|
320
|
+
message: 'target is outside the allowed roots',
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 4. Parent must exist and be a directory.
|
|
326
|
+
let parentStat;
|
|
327
|
+
try {
|
|
328
|
+
parentStat = await stat(safeParent);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (err && err.code === 'ENOENT') {
|
|
331
|
+
res.status(404).json({ error: 'not_found', message: 'parent does not exist' });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
335
|
+
res.status(403).json({ error: 'permission_denied' });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
if (!parentStat.isDirectory()) {
|
|
341
|
+
res.status(400).json({ error: 'not_a_directory' });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 5. Pre-check existence (best-effort; EEXIST from mkdir is
|
|
346
|
+
// authoritative).
|
|
347
|
+
try {
|
|
348
|
+
await stat(safeTarget);
|
|
349
|
+
res.status(409).json({
|
|
350
|
+
error: 'already_exists',
|
|
351
|
+
message: `an entry named "${name}" already exists in this directory`,
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (err && err.code !== 'ENOENT') throw err;
|
|
356
|
+
// ENOENT is the expected "good to go" case.
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 6. Create the directory. recursive:false — we want a flat
|
|
360
|
+
// one-level create, never a multi-level path.
|
|
361
|
+
try {
|
|
362
|
+
await mkdir(safeTarget, { recursive: false });
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err && err.code === 'EEXIST') {
|
|
365
|
+
// Race: someone else created the dir between our stat
|
|
366
|
+
// and our mkdir. Surface as 409.
|
|
367
|
+
res.status(409).json({
|
|
368
|
+
error: 'already_exists',
|
|
369
|
+
message: `an entry named "${name}" already exists in this directory`,
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
374
|
+
res.status(403).json({
|
|
375
|
+
error: 'permission_denied',
|
|
376
|
+
message: err.message,
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (err && err.code === 'ENOSPC') {
|
|
381
|
+
res.status(500).json({
|
|
382
|
+
error: 'mkdir_failed',
|
|
383
|
+
message: 'no space left on device',
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
res.status(500).json({
|
|
388
|
+
error: 'mkdir_failed',
|
|
389
|
+
message: err?.message || String(err),
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 7. Verify the result via stat. Catches a vanishingly rare
|
|
395
|
+
// race where something removed the dir between mkdir and
|
|
396
|
+
// stat, and any other weirdness (e.g. EPERM on a follow-up
|
|
397
|
+
// stat due to a mode change).
|
|
398
|
+
let createdStat;
|
|
399
|
+
try {
|
|
400
|
+
createdStat = await stat(safeTarget);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
res.status(500).json({
|
|
403
|
+
error: 'mkdir_verify_failed',
|
|
404
|
+
message: err?.message || String(err),
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (!createdStat.isDirectory()) {
|
|
409
|
+
res.status(409).json({
|
|
410
|
+
error: 'already_exists',
|
|
411
|
+
message: `an entry named "${name}" is no longer a directory`,
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
res.status(201).json({
|
|
417
|
+
path: safeTarget,
|
|
418
|
+
parent: safeParent,
|
|
419
|
+
name,
|
|
420
|
+
});
|
|
421
|
+
}));
|
|
422
|
+
|
|
423
|
+
// Keep `state` referenced so this router follows the same factory
|
|
424
|
+
// signature as the others (settings/overview/etc.). Future fs
|
|
425
|
+
// operations may broadcast state changes.
|
|
426
|
+
void state;
|
|
427
|
+
|
|
428
|
+
return router;
|
|
429
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/history.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/history — cross-project timeline
|
|
5
|
+
*
|
|
6
|
+
* Aggregates activity log events with per-project task / plan counts.
|
|
7
|
+
* Supports ?since=<iso> date filtering. Used by the History view.
|
|
8
|
+
*/
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import { tasksStore } from '../tasks-store.mjs';
|
|
11
|
+
import { artifactsStore } from '../artifacts-store.mjs';
|
|
12
|
+
import { projectsStore } from '../projects-store.mjs';
|
|
13
|
+
import { wrap } from './_shared.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} deps
|
|
17
|
+
* @param {string} deps.projectRoot
|
|
18
|
+
* @returns {import('express').Router}
|
|
19
|
+
*/
|
|
20
|
+
export function createHistoryRouter({ projectRoot }) {
|
|
21
|
+
const router = Router();
|
|
22
|
+
|
|
23
|
+
router.get('/history', wrap(async (req, res) => {
|
|
24
|
+
const { activityLog } = await import('../activity-log.mjs');
|
|
25
|
+
const limit = Math.min(2000, Math.max(1, parseInt(req.query.limit || '500', 10) || 500));
|
|
26
|
+
const sinceStr = req.query.since ? String(req.query.since) : null;
|
|
27
|
+
const since = sinceStr ? new Date(sinceStr).getTime() : null;
|
|
28
|
+
const events = activityLog.recent(limit)
|
|
29
|
+
.filter((e) => {
|
|
30
|
+
if (!since) return true;
|
|
31
|
+
const t = new Date(e.ts).getTime();
|
|
32
|
+
return Number.isFinite(t) && t >= since;
|
|
33
|
+
})
|
|
34
|
+
.reverse(); // oldest-first for timeline display
|
|
35
|
+
|
|
36
|
+
// Per-project rollups
|
|
37
|
+
const projectTasks = {};
|
|
38
|
+
const projectPlans = {};
|
|
39
|
+
// projectsStore.list() returns { projects, active }, not a bare
|
|
40
|
+
// array. Pull .projects off before iterating. (Bug carried over
|
|
41
|
+
// from the original api.mjs — fixed here while splitting.)
|
|
42
|
+
const projects = projectsStore.list().projects || [];
|
|
43
|
+
for (const p of projects) {
|
|
44
|
+
try {
|
|
45
|
+
const tasks = tasksStore.loadTasks(p.id);
|
|
46
|
+
projectTasks[p.id] = {
|
|
47
|
+
total: tasks.length,
|
|
48
|
+
done: tasks.filter((t) => t.status === 'done').length,
|
|
49
|
+
doing: tasks.filter((t) => t.status === 'doing').length,
|
|
50
|
+
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
|
51
|
+
queued: tasks.filter((t) => t.status === 'queued').length,
|
|
52
|
+
};
|
|
53
|
+
} catch {
|
|
54
|
+
projectTasks[p.id] = { total: 0, done: 0, doing: 0, blocked: 0, queued: 0 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const plans = artifactsStore.list(projectRoot);
|
|
59
|
+
for (const plan of plans) {
|
|
60
|
+
const pid = plan.projectId || 'global';
|
|
61
|
+
projectPlans[pid] = (projectPlans[pid] || 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
} catch { /* best-effort */ }
|
|
64
|
+
|
|
65
|
+
res.json({
|
|
66
|
+
events,
|
|
67
|
+
projects: projects.map((p) => ({
|
|
68
|
+
...p,
|
|
69
|
+
tasks: projectTasks[p.id] || { total: 0, done: 0, doing: 0, blocked: 0, queued: 0 },
|
|
70
|
+
plans: projectPlans[p.id] || 0,
|
|
71
|
+
})),
|
|
72
|
+
stats: activityLog.stats(),
|
|
73
|
+
generatedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return router;
|
|
78
|
+
}
|