@polderlabs/bizar 3.23.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -2
- package/bizar-dash/.bizar/activity.log +3 -0
- package/bizar-dash/.bizar/graph/.graphify_analysis.json +1540 -0
- package/bizar-dash/.bizar/graph/.graphify_labels.json +1 -0
- package/bizar-dash/.bizar/graph/GRAPH_REPORT.md +404 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/0661b816358db84dfdb7f10443db8a61152fd20b3fee950caf8fbf32920a1790.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/16da06d774142a6e5d74829ad982531286a6193ef798de8f593db029e123ec32.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/27e5697c01254c69c64c5a33896669073b40afb5ba88375102ae77fa5404136d.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/532997898027254aa4c0932243dc71d2f55888181232b65974961934997dc85b.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/84e3451a64c7f99f0cd6a88b0748d7426780486f53ab4935b7476b28c08f8293.json +1 -0
- package/bizar-dash/.bizar/graph/cache/ast/v0.8.46/88e5ea5f3e78fa69353fe9e6a8d1a05391d54605e25600cb125531eb5a6432ff.json +1 -0
- package/bizar-dash/.bizar/graph/cache/stat-index.json +1 -0
- package/bizar-dash/.bizar/graph/graph.html +307 -0
- package/bizar-dash/.bizar/graph/graph.json +40267 -0
- package/bizar-dash/.bizar/graph/manifest.json +762 -0
- package/bizar-dash/.graphifyignore +40 -0
- package/bizar-dash/.obsidian/README.md +14 -0
- package/bizar-dash/.obsidian/daily/2026-06-25.md +9 -0
- package/bizar-dash/.obsidian/vault.json +7 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/SKILL.md +103 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/assets/template.html +338 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/example.html +81 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/open-design.json +172 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/checklist.md +44 -0
- package/bizar-dash/.od-skills/web-prototype-e1429c1737/references/layouts.md +247 -0
- package/bizar-dash/CHANGELOG.md +474 -0
- package/bizar-dash/DESIGN.md +136 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js +331 -0
- package/bizar-dash/dist/assets/main-B3RgW6FS.js.map +1 -0
- package/bizar-dash/dist/assets/main-DlJ8wgLR.css +1 -0
- package/bizar-dash/dist/assets/mobile-CsZQAswA.css +1 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js +2 -0
- package/bizar-dash/dist/assets/mobile-DbxIw8BG.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js +354 -0
- package/bizar-dash/dist/assets/mobile-Dvq2d53Y.js.map +1 -0
- package/bizar-dash/dist/index.html +40 -0
- package/bizar-dash/dist/mobile.html +22 -0
- package/bizar-dash/plans/debug-plan/comments.json +1 -0
- package/bizar-dash/plans/debug-plan/meta.json +10 -0
- package/bizar-dash/plans/debug-plan/plan.json +174 -0
- package/bizar-dash/scripts/smoke-bg-retry.mjs +246 -0
- package/bizar-dash/src/cli/dashboard-ports.mjs +488 -0
- package/bizar-dash/src/cli.mjs +522 -0
- package/bizar-dash/src/server/activity-log.mjs +174 -0
- package/bizar-dash/src/server/agents-store.mjs +465 -0
- package/bizar-dash/src/server/api.mjs +138 -0
- package/bizar-dash/src/server/artifact-template.html +810 -0
- package/bizar-dash/src/server/artifacts-store.mjs +733 -0
- package/bizar-dash/src/server/auth.mjs +381 -0
- package/bizar-dash/src/server/background-store.mjs +447 -0
- package/bizar-dash/src/server/bg-poller.mjs +305 -0
- package/bizar-dash/src/server/bg-retry.mjs +574 -0
- package/bizar-dash/src/server/browser.mjs +40 -0
- package/bizar-dash/src/server/diagnostics-store.mjs +148 -0
- package/bizar-dash/src/server/dialog-poller.mjs +105 -0
- package/bizar-dash/src/server/dialog-store.mjs +177 -0
- package/bizar-dash/src/server/glyphs/mdx-compiler.mjs +583 -0
- package/bizar-dash/src/server/lib/path-safe.mjs +283 -0
- package/bizar-dash/src/server/memory-git.mjs +249 -0
- package/bizar-dash/src/server/memory-schema.mjs +145 -0
- package/bizar-dash/src/server/memory-secrets.mjs +84 -0
- package/bizar-dash/src/server/memory-store.mjs +424 -0
- package/bizar-dash/src/server/mod-security.mjs +398 -0
- package/bizar-dash/src/server/mods-loader.mjs +1067 -0
- package/bizar-dash/src/server/notifications-store.mjs +243 -0
- package/bizar-dash/src/server/obsidian-store.mjs +318 -0
- package/bizar-dash/src/server/opencode-runner.mjs +279 -0
- package/bizar-dash/src/server/pair-store.mjs +138 -0
- package/bizar-dash/src/server/projects-store.mjs +364 -0
- package/bizar-dash/src/server/providers-store.mjs +789 -0
- package/bizar-dash/src/server/routes/_shared.mjs +415 -0
- package/bizar-dash/src/server/routes/activity.mjs +110 -0
- package/bizar-dash/src/server/routes/agents.mjs +134 -0
- package/bizar-dash/src/server/routes/artifacts.mjs +279 -0
- package/bizar-dash/src/server/routes/auth.mjs +69 -0
- package/bizar-dash/src/server/routes/background.mjs +165 -0
- package/bizar-dash/src/server/routes/chat.mjs +485 -0
- package/bizar-dash/src/server/routes/config.mjs +129 -0
- package/bizar-dash/src/server/routes/diagnostics.mjs +50 -0
- package/bizar-dash/src/server/routes/dialogs.mjs +86 -0
- package/bizar-dash/src/server/routes/fs.mjs +429 -0
- package/bizar-dash/src/server/routes/history.mjs +78 -0
- package/bizar-dash/src/server/routes/memory.mjs +499 -0
- package/bizar-dash/src/server/routes/misc.mjs +89 -0
- package/bizar-dash/src/server/routes/mods.mjs +359 -0
- package/bizar-dash/src/server/routes/notifications.mjs +54 -0
- package/bizar-dash/src/server/routes/obsidian.mjs +239 -0
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +55 -0
- package/bizar-dash/src/server/routes/overview.mjs +91 -0
- package/bizar-dash/src/server/routes/pair.mjs +67 -0
- package/bizar-dash/src/server/routes/projects.mjs +175 -0
- package/bizar-dash/src/server/routes/providers.mjs +47 -0
- package/bizar-dash/src/server/routes/schedules.mjs +104 -0
- package/bizar-dash/src/server/routes/settings.mjs +51 -0
- package/bizar-dash/src/server/routes/skills.mjs +76 -0
- package/bizar-dash/src/server/routes/tasks.mjs +468 -0
- package/bizar-dash/src/server/routes/themes.mjs +51 -0
- package/bizar-dash/src/server/routes-v2/auth.mjs +59 -0
- package/bizar-dash/src/server/routes-v2/events.mjs +107 -0
- package/bizar-dash/src/server/routes-v2/health.mjs +21 -0
- package/bizar-dash/src/server/routes-v2/index.mjs +90 -0
- package/bizar-dash/src/server/routes-v2/sessions.mjs +111 -0
- package/bizar-dash/src/server/schedules-runner.mjs +369 -0
- package/bizar-dash/src/server/schedules-store.mjs +268 -0
- package/bizar-dash/src/server/search-store.mjs +220 -0
- package/bizar-dash/src/server/serve-info.mjs +684 -0
- package/bizar-dash/src/server/server.mjs +796 -0
- package/bizar-dash/src/server/settings-store.mjs +45 -0
- package/bizar-dash/src/server/skills-store.mjs +300 -0
- package/bizar-dash/src/server/state.mjs +437 -0
- package/bizar-dash/src/server/tailscale-store.mjs +113 -0
- package/bizar-dash/src/server/task-delegator.mjs +824 -0
- package/bizar-dash/src/server/tasks-store.mjs +508 -0
- package/bizar-dash/src/server/tui.mjs +923 -0
- package/bizar-dash/src/server/update-store.mjs +168 -0
- package/bizar-dash/src/server/v2-auth-file.mjs +99 -0
- package/bizar-dash/src/server/v2-event-bus.mjs +128 -0
- package/bizar-dash/src/server/watcher.mjs +81 -0
- package/bizar-dash/src/server/yaml.mjs +238 -0
- package/bizar-dash/src/web/App.tsx +718 -0
- package/bizar-dash/src/web/MobileApp.tsx +408 -0
- package/bizar-dash/src/web/components/ArtifactCreateDialog.tsx +95 -0
- package/bizar-dash/src/web/components/ArtifactListDialog.tsx +55 -0
- package/bizar-dash/src/web/components/ArtifactViewer.tsx +143 -0
- package/bizar-dash/src/web/components/AuditDialog.tsx +79 -0
- package/bizar-dash/src/web/components/BgStatusBadge.tsx +32 -0
- package/bizar-dash/src/web/components/Button.tsx +56 -0
- package/bizar-dash/src/web/components/CanvasContextMenu.tsx +68 -0
- package/bizar-dash/src/web/components/Card.tsx +40 -0
- package/bizar-dash/src/web/components/CollapsibleSection.tsx +72 -0
- package/bizar-dash/src/web/components/CommandDialog.tsx +66 -0
- package/bizar-dash/src/web/components/EmptyState.tsx +30 -0
- package/bizar-dash/src/web/components/FileBrowser.tsx +633 -0
- package/bizar-dash/src/web/components/HelpDialog.tsx +55 -0
- package/bizar-dash/src/web/components/KillConfirmDialog.tsx +88 -0
- package/bizar-dash/src/web/components/Modal.tsx +215 -0
- package/bizar-dash/src/web/components/Notifications.tsx +241 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +224 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +66 -0
- package/bizar-dash/src/web/components/Spinner.tsx +19 -0
- package/bizar-dash/src/web/components/StatusBadge.tsx +25 -0
- package/bizar-dash/src/web/components/Tag.tsx +28 -0
- package/bizar-dash/src/web/components/Toast.tsx +150 -0
- package/bizar-dash/src/web/components/Topbar.tsx +368 -0
- package/bizar-dash/src/web/components/VisualPlanDialog.tsx +58 -0
- package/bizar-dash/src/web/components/chat/AgentChip.tsx +68 -0
- package/bizar-dash/src/web/components/chat/ChatBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +88 -0
- package/bizar-dash/src/web/components/chat/ChatTopBar.tsx +50 -0
- package/bizar-dash/src/web/components/chat/Composer.tsx +164 -0
- package/bizar-dash/src/web/components/chat/ConfirmModal.tsx +54 -0
- package/bizar-dash/src/web/components/chat/EmptyState.tsx +21 -0
- package/bizar-dash/src/web/components/chat/FirstRunGreeting.tsx +18 -0
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +32 -0
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +119 -0
- package/bizar-dash/src/web/components/chat/LoadingSkeleton.tsx +19 -0
- package/bizar-dash/src/web/components/chat/MessageBubble.tsx +60 -0
- package/bizar-dash/src/web/components/chat/SessionList.tsx +153 -0
- package/bizar-dash/src/web/components/chat/StreamingIndicator.tsx +9 -0
- package/bizar-dash/src/web/components/chat/SuggestionCards.tsx +55 -0
- package/bizar-dash/src/web/components/chat/WelcomeScreen.tsx +36 -0
- package/bizar-dash/src/web/components/chat/index.ts +20 -0
- package/bizar-dash/src/web/components/chat/useAutoGrowTextarea.ts +18 -0
- package/bizar-dash/src/web/components/chat/useChat.ts +219 -0
- package/bizar-dash/src/web/components/chat/useSlashCommands.ts +47 -0
- package/bizar-dash/src/web/index.html +37 -0
- package/bizar-dash/src/web/lib/api.ts +193 -0
- package/bizar-dash/src/web/lib/markdown.tsx +59 -0
- package/bizar-dash/src/web/lib/types.ts +668 -0
- package/bizar-dash/src/web/lib/utils.ts +114 -0
- package/bizar-dash/src/web/lib/ws.ts +176 -0
- package/bizar-dash/src/web/main.tsx +34 -0
- package/bizar-dash/src/web/mobile/MobileApp.tsx +278 -0
- package/bizar-dash/src/web/mobile/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/mobile/MobileTopbar.tsx +115 -0
- package/bizar-dash/src/web/mobile/components/MobileBottomSheet.tsx +182 -0
- package/bizar-dash/src/web/mobile/components/MobileListItem.tsx +64 -0
- package/bizar-dash/src/web/mobile/components/MobileModal.tsx +123 -0
- package/bizar-dash/src/web/mobile/views/MobileActivity.tsx +273 -0
- package/bizar-dash/src/web/mobile/views/MobileAgents.tsx +374 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifactCanvas.tsx +439 -0
- package/bizar-dash/src/web/mobile/views/MobileArtifacts.tsx +143 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +180 -0
- package/bizar-dash/src/web/mobile/views/MobileConfig.tsx +175 -0
- package/bizar-dash/src/web/mobile/views/MobileHistory.tsx +191 -0
- package/bizar-dash/src/web/mobile/views/MobileMods.tsx +149 -0
- package/bizar-dash/src/web/mobile/views/MobileMore.tsx +121 -0
- package/bizar-dash/src/web/mobile/views/MobileNotifications.tsx +137 -0
- package/bizar-dash/src/web/mobile/views/MobilePlans.tsx +26 -0
- package/bizar-dash/src/web/mobile/views/MobileSchedules.tsx +685 -0
- package/bizar-dash/src/web/mobile/views/MobileSearchModal.tsx +172 -0
- package/bizar-dash/src/web/mobile/views/MobileSettings.tsx +388 -0
- package/bizar-dash/src/web/mobile/views/MobileSkills.tsx +153 -0
- package/bizar-dash/src/web/mobile/views/MobileTasks.tsx +564 -0
- package/bizar-dash/src/web/mobile.html +20 -0
- package/bizar-dash/src/web/mobile.tsx +13 -0
- package/bizar-dash/src/web/styles/chat.css +198 -0
- package/bizar-dash/src/web/styles/glyphs.css +960 -0
- package/bizar-dash/src/web/styles/main.css +8855 -0
- package/bizar-dash/src/web/styles/mobile-chat.css +5 -0
- package/bizar-dash/src/web/styles/mobile.css +1960 -0
- package/bizar-dash/src/web/views/Activity.tsx +1203 -0
- package/bizar-dash/src/web/views/Agents.tsx +701 -0
- package/bizar-dash/src/web/views/Artifacts.tsx +1480 -0
- package/bizar-dash/src/web/views/BackgroundAgents.tsx +450 -0
- package/bizar-dash/src/web/views/Chat.tsx +190 -0
- package/bizar-dash/src/web/views/Config.tsx +1603 -0
- package/bizar-dash/src/web/views/History.tsx +335 -0
- package/bizar-dash/src/web/views/ModView.tsx +207 -0
- package/bizar-dash/src/web/views/Mods.tsx +693 -0
- package/bizar-dash/src/web/views/Overview.tsx +812 -0
- package/bizar-dash/src/web/views/Providers.tsx +302 -0
- package/bizar-dash/src/web/views/Schedules.tsx +802 -0
- package/bizar-dash/src/web/views/Settings.tsx +1787 -0
- package/bizar-dash/src/web/views/Skills.tsx +431 -0
- package/bizar-dash/src/web/views/Tasks.tsx +1523 -0
- package/bizar-dash/src/web/views/glyphs/GlyphRenderer.tsx +613 -0
- package/bizar-dash/src/web/views/glyphs/components.tsx +1098 -0
- package/bizar-dash/templates/mod/FORMAT.md +76 -0
- package/bizar-dash/templates/mod/hello-mod/README.md +19 -0
- package/bizar-dash/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/bizar-dash/templates/mod/hello-mod/commands/hello.md +6 -0
- package/bizar-dash/templates/mod/hello-mod/mod.json +20 -0
- package/bizar-dash/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/bizar-dash/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/bizar-dash/tests/dashboard-ports.test.mjs +138 -0
- package/bizar-dash/tests/graphify-mod-spawn.node.test.mjs +90 -0
- package/bizar-dash/tests/memory-git.test.mjs +138 -0
- package/bizar-dash/tests/memory-schema.test.mjs +198 -0
- package/bizar-dash/tests/memory-secrets.test.mjs +121 -0
- package/bizar-dash/tests/memory-store.test.mjs +390 -0
- package/bizar-dash/tests/memory-sync.test.mjs +141 -0
- package/bizar-dash/tests/mod-instructions.node.test.mjs +188 -0
- package/bizar-dash/tests/mod-security.test.mjs +273 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +357 -0
- package/bizar-dash/tests/no-agent-browser.node.test.mjs +98 -0
- package/bizar-dash/tests/obsidian-back-compat.test.mjs +199 -0
- package/bizar-dash/tests/opencode-runner.test.mjs +172 -0
- package/bizar-dash/tests/path-safe.test.mjs +108 -0
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +333 -0
- package/bizar-dash/tests/smoke-v2.mjs +212 -0
- package/bizar-dash/tests/strip-thinking.test.mjs +175 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +353 -0
- package/bizar-dash/tests/tmux-wrap.test.mjs +145 -0
- package/bizar-dash/tests/yaml.test.mjs +128 -0
- package/bizar-dash/tsconfig.json +23 -0
- package/bizar-dash/vite.config.ts +27 -0
- package/cli/atomic.mjs +73 -0
- package/cli/bin.mjs +43 -46
- package/cli/copy.mjs +3 -0
- package/cli/dev-link.mjs +17 -1
- package/cli/init.mjs +135 -1
- package/cli/install.mjs +10 -23
- package/cli/memory-constants.mjs +59 -0
- package/cli/memory.mjs +757 -0
- package/cli/service.mjs +5 -6
- package/config/skills/obsidian/SKILL.md +213 -65
- package/install.sh +48 -18
- package/package.json +49 -14
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +80 -0
- package/packages/sdk/src/client.ts +183 -0
- package/packages/sdk/src/errors.ts +120 -0
- package/packages/sdk/src/events.ts +153 -0
- package/packages/sdk/src/index.ts +63 -0
- package/packages/sdk/src/types.ts +176 -0
- package/packages/sdk/src/version.ts +4 -0
- package/packages/sdk/tests/client.test.ts +217 -0
- package/packages/sdk/tests/errors.test.ts +108 -0
- package/packages/sdk/tests/events.test.ts +139 -0
- package/packages/sdk/tests/fixtures/fetch-mock.ts +152 -0
- package/packages/sdk/tests/fixtures/sse-mock.ts +30 -0
- package/packages/sdk/tsconfig.json +25 -0
- package/packages/sdk/vitest.config.ts +9 -0
- package/plugins/bizar/LICENSE +21 -0
- package/plugins/bizar/README.md +448 -0
- package/plugins/bizar/dist/index.js +29901 -0
- package/plugins/bizar/index.ts +1484 -0
- package/plugins/bizar/scripts/check-forbidden-imports.sh +33 -0
- package/plugins/bizar/src/background-state.ts +551 -0
- package/plugins/bizar/src/background.ts +1250 -0
- package/plugins/bizar/src/commands-impl.ts +464 -0
- package/plugins/bizar/src/commands.ts +1127 -0
- package/plugins/bizar/src/dashboard-client.ts +233 -0
- package/plugins/bizar/src/event-stream.ts +606 -0
- package/plugins/bizar/src/fingerprint.ts +120 -0
- package/plugins/bizar/src/handoff.ts +79 -0
- package/plugins/bizar/src/http-client.ts +467 -0
- package/plugins/bizar/src/key-rotation.ts +218 -0
- package/plugins/bizar/src/logger.ts +144 -0
- package/plugins/bizar/src/loop.ts +176 -0
- package/plugins/bizar/src/opencode-runner.ts +390 -0
- package/plugins/bizar/src/options.ts +421 -0
- package/plugins/bizar/src/plan-fs.ts +323 -0
- package/plugins/bizar/src/reasoning-clean.ts +454 -0
- package/plugins/bizar/src/report.ts +178 -0
- package/plugins/bizar/src/research-prompt.ts +35 -0
- package/plugins/bizar/src/serve-info.ts +228 -0
- package/plugins/bizar/src/serve.ts +496 -0
- package/plugins/bizar/src/settings.ts +349 -0
- package/plugins/bizar/src/state.ts +298 -0
- package/plugins/bizar/src/tools/bg-collect.ts +104 -0
- package/plugins/bizar/src/tools/bg-get-comments.ts +239 -0
- package/plugins/bizar/src/tools/bg-kill.ts +87 -0
- package/plugins/bizar/src/tools/bg-spawn.ts +401 -0
- package/plugins/bizar/src/tools/bg-status.ts +99 -0
- package/plugins/bizar/src/tools/plan-action.ts +767 -0
- package/plugins/bizar/src/tools/read-glyph-feedback.ts +170 -0
- package/plugins/bizar/src/tools/wait-for-feedback.ts +402 -0
- package/plugins/bizar/tests/attach-handler-bug.test.ts +169 -0
- package/plugins/bizar/tests/background-state.test.ts +277 -0
- package/plugins/bizar/tests/background.test.ts +402 -0
- package/plugins/bizar/tests/block.test.ts +195 -0
- package/plugins/bizar/tests/canonical-key-order.test.ts +75 -0
- package/plugins/bizar/tests/commands-impl.test.ts +442 -0
- package/plugins/bizar/tests/commands.test.ts +548 -0
- package/plugins/bizar/tests/config.test.ts +122 -0
- package/plugins/bizar/tests/dashboard-client.test.ts +159 -0
- package/plugins/bizar/tests/dispose.test.ts +336 -0
- package/plugins/bizar/tests/event-stream.test.ts +409 -0
- package/plugins/bizar/tests/event.test.ts +262 -0
- package/plugins/bizar/tests/fingerprint.test.ts +162 -0
- package/plugins/bizar/tests/http-client.test.ts +404 -0
- package/plugins/bizar/tests/init-helpers.test.ts +203 -0
- package/plugins/bizar/tests/integration/slash-command.test.ts +348 -0
- package/plugins/bizar/tests/integration/tool-routing.test.ts +314 -0
- package/plugins/bizar/tests/key-rotation.test.ts +396 -0
- package/plugins/bizar/tests/loop.test.ts +397 -0
- package/plugins/bizar/tests/options.test.ts +276 -0
- package/plugins/bizar/tests/reasoning-clean.test.ts +422 -0
- package/plugins/bizar/tests/serve.test.ts +339 -0
- package/plugins/bizar/tests/settings.test.ts +351 -0
- package/plugins/bizar/tests/stall-think.test.ts +750 -0
- package/plugins/bizar/tests/state.test.ts +276 -0
- package/plugins/bizar/tests/tools/bg-collect.test.ts +337 -0
- package/plugins/bizar/tests/tools/bg-get-comments.test.ts +485 -0
- package/plugins/bizar/tests/tools/bg-kill.test.ts +235 -0
- package/plugins/bizar/tests/tools/bg-spawn-delegation.test.ts +147 -0
- package/plugins/bizar/tests/tools/bg-spawn.test.ts +311 -0
- package/plugins/bizar/tests/tools/bg-status.test.ts +217 -0
- package/plugins/bizar/tests/tools/opencode-runner.test.ts +115 -0
- package/plugins/bizar/tests/tools/plan-action.test.ts +599 -0
- package/plugins/bizar/tests/tools/read-glyph-feedback.test.ts +253 -0
- package/plugins/bizar/tests/tools/wait-for-feedback.test.ts +390 -0
- package/plugins/bizar/tests/update-deadlock.test.ts +145 -0
- package/plugins/bizar/tsconfig.json +29 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/memory-secrets.mjs
|
|
3
|
+
*
|
|
4
|
+
* Regex-based secret scanner for structured long-term memory notes.
|
|
5
|
+
* Detects HIGH and MEDIUM severity patterns (API keys, tokens, private keys,
|
|
6
|
+
* absolute paths, private IPv4 addresses). Designed to run before every
|
|
7
|
+
* commit to prevent accidental secret leakage.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All secret patterns. severity is 'HIGH' or 'MEDIUM'.
|
|
12
|
+
*
|
|
13
|
+
* @type {Array<{ id: string, severity: 'HIGH' | 'MEDIUM', pattern: RegExp }>}
|
|
14
|
+
*/
|
|
15
|
+
export const SECRET_PATTERNS = [
|
|
16
|
+
// HIGH severity
|
|
17
|
+
{ id: 'private_key_pem', severity: 'HIGH', pattern: /-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----/ },
|
|
18
|
+
{ id: 'aws_access_key', severity: 'HIGH', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
19
|
+
{ id: 'aws_secret_key', severity: 'HIGH', pattern: /\baws_secret_access_key\s*[:=]\s*[A-Za-z0-9\/+=]{40}\b/ },
|
|
20
|
+
{ id: 'github_pat_classic', severity: 'HIGH', pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
21
|
+
{ id: 'github_pat_fine', severity: 'HIGH', pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
|
|
22
|
+
{ id: 'slack_token', severity: 'HIGH', pattern: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/ },
|
|
23
|
+
{ id: 'stripe_live', severity: 'HIGH', pattern: /\bsk_live_[0-9a-zA-Z]{24,}\b/ },
|
|
24
|
+
{ id: 'stripe_test', severity: 'HIGH', pattern: /\bsk_test_[0-9a-zA-Z]{24,}\b/ },
|
|
25
|
+
// Bearer token: refined to require base64-like chars after "Bearer "
|
|
26
|
+
// and terminate on whitespace, punctuation, or end-of-string
|
|
27
|
+
{
|
|
28
|
+
id: 'bearer_token',
|
|
29
|
+
severity: 'HIGH',
|
|
30
|
+
pattern: /\bBearer\s+[A-Za-z0-9+\/=_\-]{20,}(?:\s|$|[,;.])/,
|
|
31
|
+
},
|
|
32
|
+
// MEDIUM severity
|
|
33
|
+
{
|
|
34
|
+
id: 'api_key_assignment',
|
|
35
|
+
severity: 'MEDIUM',
|
|
36
|
+
// Non-greedy {16,}? so the end-of-line assertion has something to match
|
|
37
|
+
pattern: /\b(api[_-]?key|apikey|secret)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{16,}?['"]?(?:\s*$|[,;.\n])/im,
|
|
38
|
+
},
|
|
39
|
+
{ id: 'absolute_path', severity: 'MEDIUM', pattern: /\/(Users|home)\/[A-Za-z0-9._-]+/ },
|
|
40
|
+
{ id: 'private_ipv4', severity: 'MEDIUM', pattern: /\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+)\b/ },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan text for secret patterns. Returns a structured scan result.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} text — raw text to scan (usually the full body of a note)
|
|
47
|
+
* @returns {{ safe: boolean, findings: Array<{ id: string, severity: 'HIGH' | 'MEDIUM', line: number, snippet: string }> }}
|
|
48
|
+
*/
|
|
49
|
+
export function scan(text) {
|
|
50
|
+
if (typeof text !== 'string') return { safe: true, findings: [] };
|
|
51
|
+
|
|
52
|
+
const findings = [];
|
|
53
|
+
const lines = text.split('\n');
|
|
54
|
+
|
|
55
|
+
for (const { id, severity, pattern } of SECRET_PATTERNS) {
|
|
56
|
+
// Reset lastIndex each iteration for global patterns
|
|
57
|
+
pattern.lastIndex = 0;
|
|
58
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
59
|
+
const line = lines[lineNum];
|
|
60
|
+
pattern.lastIndex = 0;
|
|
61
|
+
const match = pattern.exec(line);
|
|
62
|
+
if (match) {
|
|
63
|
+
const snippet = line.slice(Math.max(0, match.index - 20), match.index + match[0].length + 20);
|
|
64
|
+
findings.push({ id, severity, line: lineNum + 1, snippet });
|
|
65
|
+
// Only report first match per pattern per line
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
safe: findings.filter((f) => f.severity === 'HIGH').length === 0,
|
|
72
|
+
findings,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns true if the scan result contains any HIGH-severity findings.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ safe: boolean, findings: Array<{ severity: string }> }} result
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
export function hasHighFindings(result) {
|
|
83
|
+
return result.findings.some((f) => f.severity === 'HIGH');
|
|
84
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/memory-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* MarkdownMemoryStore — reads/writes Obsidian-compatible `.md` notes with YAML
|
|
5
|
+
* frontmatter for the Bizar Memory Service. Supports three modes:
|
|
6
|
+
* local-only — vault at <projectRoot>/.obsidian/
|
|
7
|
+
* managed — vault at ~/.local/share/bizar/memory/<repoName>/projects/<projectId>/
|
|
8
|
+
* linked — alias for managed
|
|
9
|
+
*
|
|
10
|
+
* The project namespace directory is created LAZY on first writeNote, not at
|
|
11
|
+
* initVault time (F7 invariant).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { join, dirname, relative, resolve as pathResolve, sep } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { parseFrontmatter, serializeFrontmatter } from './yaml.mjs';
|
|
19
|
+
import { validateNote } from './memory-schema.mjs';
|
|
20
|
+
import { scan as scanSecrets, hasHighFindings } from './memory-secrets.mjs';
|
|
21
|
+
import { atomicWriteJson, safeReadJSON, safeReadText } from '../../../cli/atomic.mjs';
|
|
22
|
+
|
|
23
|
+
const HOME = homedir();
|
|
24
|
+
const BIZAR_MEMORY_ROOT = join(HOME, '.local', 'share', 'bizar', 'memory');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Path safety: reject any relPath that would escape the vault root.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} vaultRoot
|
|
30
|
+
* @param {string} relPath
|
|
31
|
+
* @returns {string | null} — resolved absolute path, or null if unsafe
|
|
32
|
+
*/
|
|
33
|
+
function resolveSafe(vaultRoot, relPath) {
|
|
34
|
+
if (!relPath || relPath.includes('\0')) return null;
|
|
35
|
+
const abs = pathResolve(vaultRoot, relPath);
|
|
36
|
+
const rel = relative(vaultRoot, abs);
|
|
37
|
+
if (rel.startsWith('..') || abs !== pathResolve(abs)) return null;
|
|
38
|
+
return abs;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Read the per-project memory config file (.bizar/memory.json).
|
|
43
|
+
*
|
|
44
|
+
* @param {string} projectRoot
|
|
45
|
+
* @returns {{ exists: boolean, config: Record<string, unknown>, error?: string }}
|
|
46
|
+
*/
|
|
47
|
+
export function loadConfig(projectRoot) {
|
|
48
|
+
const configPath = join(projectRoot, '.bizar', 'memory.json');
|
|
49
|
+
const raw = safeReadJSON(configPath, null);
|
|
50
|
+
if (raw === null) {
|
|
51
|
+
return {
|
|
52
|
+
exists: false,
|
|
53
|
+
config: {
|
|
54
|
+
mode: 'local-only',
|
|
55
|
+
projectId: projectRoot.split('/').pop() || 'unknown',
|
|
56
|
+
repoName: '',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { exists: true, config: raw };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Write the per-project memory config file.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectRoot
|
|
67
|
+
* @param {Record<string, unknown>} config
|
|
68
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
69
|
+
*/
|
|
70
|
+
export function saveConfig(projectRoot, config) {
|
|
71
|
+
try {
|
|
72
|
+
const configDir = join(projectRoot, '.bizar');
|
|
73
|
+
if (!existsSync(configDir)) {
|
|
74
|
+
mkdirSync(configDir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
const configPath = join(configDir, 'memory.json');
|
|
77
|
+
atomicWriteJson(configPath, config);
|
|
78
|
+
return { ok: true };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { ok: false, error: err.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the effective vault root, namespaces, and related paths based on mode.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} projectRoot
|
|
88
|
+
* @returns {{ mode: string, projectId: string, vaultRoot: string, repoPath: string | null, configDir: string, gitRemote: string | null, branch: string, lightragDir: string, namespaces: object }}
|
|
89
|
+
*/
|
|
90
|
+
export function resolveVault(projectRoot) {
|
|
91
|
+
const { config } = loadConfig(projectRoot);
|
|
92
|
+
const mr = config.memoryRepo || {};
|
|
93
|
+
const mode = mr.mode || config.mode || 'local-only';
|
|
94
|
+
const projectId = config.projectId || projectRoot.split('/').pop() || 'unknown';
|
|
95
|
+
const branch = mr.branch || config.branch || 'main';
|
|
96
|
+
const gitRemote = mr.remote || config.gitRemote || null;
|
|
97
|
+
|
|
98
|
+
let vaultRoot;
|
|
99
|
+
let repoPath;
|
|
100
|
+
if (mode === 'local-only') {
|
|
101
|
+
vaultRoot = join(projectRoot, '.obsidian');
|
|
102
|
+
repoPath = null;
|
|
103
|
+
} else {
|
|
104
|
+
// managed or linked — path may be absolute, ~-relative, or a sibling of projectRoot
|
|
105
|
+
const rawPath = mr.path || config.repoName || join(projectRoot, '.bizar', 'memory');
|
|
106
|
+
const expanded = rawPath.startsWith('~') ? join(HOME, rawPath.slice(1)) : rawPath;
|
|
107
|
+
repoPath = expanded;
|
|
108
|
+
vaultRoot = join(expanded, 'projects', projectId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const configDir = join(projectRoot, '.bizar');
|
|
112
|
+
const lightragDir = join(projectRoot, '.bizar', 'memory-cache');
|
|
113
|
+
const namespaces = config.namespaces || {
|
|
114
|
+
project: `projects/${projectId}`,
|
|
115
|
+
global: 'global/bizar',
|
|
116
|
+
user: `users/${process.env.USER || process.env.USERNAME || 'local'}`,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return { mode, projectId, vaultRoot, repoPath, configDir, gitRemote, branch, lightragDir, namespaces };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initialize the vault for a project. In local-only mode, creates .obsidian/
|
|
124
|
+
* and standard subdirectories. In managed mode, the shared repo is assumed to
|
|
125
|
+
* already exist; this function only ensures the .bizar/memory.json is set up.
|
|
126
|
+
*
|
|
127
|
+
* NOTE: The project namespace directory (projects/<id>/) is NOT created here.
|
|
128
|
+
* It is created lazily on first writeNote (F7 invariant).
|
|
129
|
+
*
|
|
130
|
+
* @param {string} projectRoot
|
|
131
|
+
* @returns {{ ok: boolean, vaultRoot: string, created: string[], error?: string }}
|
|
132
|
+
*/
|
|
133
|
+
export function initVault(projectRoot) {
|
|
134
|
+
const { config } = loadConfig(projectRoot);
|
|
135
|
+
const mr = config.memoryRepo || {};
|
|
136
|
+
const mode = mr.mode || config.mode || 'local-only';
|
|
137
|
+
const created = [];
|
|
138
|
+
|
|
139
|
+
if (mode === 'local-only') {
|
|
140
|
+
const vaultRoot = join(projectRoot, '.obsidian');
|
|
141
|
+
const subdirs = ['daily', 'decisions', 'patterns', 'api', 'tasks', 'global', 'users'];
|
|
142
|
+
if (!existsSync(vaultRoot)) {
|
|
143
|
+
mkdirSync(vaultRoot, { recursive: true });
|
|
144
|
+
created.push('.obsidian/');
|
|
145
|
+
}
|
|
146
|
+
for (const sub of subdirs) {
|
|
147
|
+
const subPath = join(vaultRoot, sub);
|
|
148
|
+
if (!existsSync(subPath)) {
|
|
149
|
+
mkdirSync(subPath, { recursive: true });
|
|
150
|
+
created.push(`.obsidian/${sub}/`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, vaultRoot, created };
|
|
154
|
+
} else {
|
|
155
|
+
// managed / linked — create the shared repo at repoPath with `git init`
|
|
156
|
+
const { repoPath, vaultRoot } = resolveVault(projectRoot);
|
|
157
|
+
if (!existsSync(repoPath)) {
|
|
158
|
+
mkdirSync(repoPath, { recursive: true });
|
|
159
|
+
created.push(repoPath);
|
|
160
|
+
}
|
|
161
|
+
// git init if not already a git repo
|
|
162
|
+
const isGit = existsSync(join(repoPath, '.git'));
|
|
163
|
+
if (!isGit) {
|
|
164
|
+
try {
|
|
165
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: repoPath, stdio: 'pipe' });
|
|
166
|
+
created.push(`${repoPath}/.git/`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return { ok: false, vaultRoot, created, error: `git init failed: ${err.message}` };
|
|
169
|
+
}
|
|
170
|
+
// Write and commit a .gitignore for runtime artifacts
|
|
171
|
+
const gitignorePath = join(repoPath, '.gitignore');
|
|
172
|
+
if (!existsSync(gitignorePath)) {
|
|
173
|
+
writeFileSync(gitignorePath,
|
|
174
|
+
'# Lock files — runtime state, not memory\n' +
|
|
175
|
+
'**/.sync.lock\n' +
|
|
176
|
+
'**/*.pid\n' +
|
|
177
|
+
'**/*.log\n' +
|
|
178
|
+
'**/.DS_Store\n' +
|
|
179
|
+
'\n' +
|
|
180
|
+
'# Local-only metadata\n' +
|
|
181
|
+
'**/.bizar-memory/cache/\n' +
|
|
182
|
+
'**/.bizar-memory/index-ledger.local.json\n',
|
|
183
|
+
'utf8');
|
|
184
|
+
created.push(`${repoPath}/.gitignore`);
|
|
185
|
+
try {
|
|
186
|
+
execFileSync('git', ['add', '.gitignore'], { cwd: repoPath });
|
|
187
|
+
execFileSync('git', ['commit', '-m', 'chore: initial .gitignore for memory repo'], {
|
|
188
|
+
cwd: repoPath,
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
GIT_AUTHOR_NAME: 'Bizar Memory',
|
|
192
|
+
GIT_AUTHOR_EMAIL: 'bizar-memory@local',
|
|
193
|
+
GIT_COMMITTER_NAME: 'Bizar Memory',
|
|
194
|
+
GIT_COMMITTER_EMAIL: 'bizar-memory@local',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// best-effort; not fatal
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Ensure projects/, global/, users/ exist so writeNote can find them without
|
|
203
|
+
// creating the project-specific namespace (F7 invariant — only the projects/<id>
|
|
204
|
+
// subdirectory is created lazily on first writeNote)
|
|
205
|
+
const projectsDir = join(repoPath, 'projects');
|
|
206
|
+
if (!existsSync(projectsDir)) {
|
|
207
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
208
|
+
created.push(`${repoPath}/projects/`);
|
|
209
|
+
}
|
|
210
|
+
const globalDir = join(repoPath, 'global', 'bizar');
|
|
211
|
+
if (!existsSync(globalDir)) {
|
|
212
|
+
mkdirSync(globalDir, { recursive: true });
|
|
213
|
+
created.push(`${repoPath}/global/bizar/`);
|
|
214
|
+
}
|
|
215
|
+
const usersDir = join(repoPath, 'users');
|
|
216
|
+
if (!existsSync(usersDir)) {
|
|
217
|
+
mkdirSync(usersDir, { recursive: true });
|
|
218
|
+
created.push(`${repoPath}/users/`);
|
|
219
|
+
}
|
|
220
|
+
return { ok: true, vaultRoot, created };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* List all notes in the vault. Returns enriched note metadata.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} projectRoot
|
|
228
|
+
* @param {{ namespace?: string }} [opts]
|
|
229
|
+
* @returns {Array<{ relPath: string, mtime: number, size: number, frontmatter: Record<string, unknown>, body: string, schemaValid: boolean }>}
|
|
230
|
+
*/
|
|
231
|
+
export function listNotes(projectRoot, opts = {}) {
|
|
232
|
+
const { vaultRoot } = resolveVault(projectRoot);
|
|
233
|
+
if (!existsSync(vaultRoot)) return [];
|
|
234
|
+
|
|
235
|
+
const namespace = opts.namespace || '';
|
|
236
|
+
const searchRoot = namespace ? join(vaultRoot, namespace) : vaultRoot;
|
|
237
|
+
if (!existsSync(searchRoot)) return [];
|
|
238
|
+
|
|
239
|
+
const out = [];
|
|
240
|
+
function walk(dir, prefix) {
|
|
241
|
+
let entries;
|
|
242
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
243
|
+
catch { return; }
|
|
244
|
+
for (const e of entries) {
|
|
245
|
+
const full = join(dir, e.name);
|
|
246
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
247
|
+
if (e.isDirectory()) {
|
|
248
|
+
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
249
|
+
walk(full, rel);
|
|
250
|
+
} else if (e.name.endsWith('.md')) {
|
|
251
|
+
try {
|
|
252
|
+
const st = statSync(full);
|
|
253
|
+
const raw = readFileSync(full, 'utf8');
|
|
254
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
255
|
+
const { valid } = validateNote(frontmatter, body);
|
|
256
|
+
out.push({ relPath: rel, mtime: st.mtimeMs, size: st.size, frontmatter, body, schemaValid: valid });
|
|
257
|
+
} catch { /* skip unreadable */ }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
walk(searchRoot, '');
|
|
262
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read a single note by relative path. Returns null if not found.
|
|
267
|
+
*
|
|
268
|
+
* @param {string} projectRoot
|
|
269
|
+
* @param {string} relPath
|
|
270
|
+
* @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean } | null}
|
|
271
|
+
*/
|
|
272
|
+
export function readNote(projectRoot, relPath) {
|
|
273
|
+
const { vaultRoot } = resolveVault(projectRoot);
|
|
274
|
+
const filePath = resolveSafe(vaultRoot, relPath);
|
|
275
|
+
if (!filePath || !existsSync(filePath)) return null;
|
|
276
|
+
try {
|
|
277
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
278
|
+
const st = statSync(filePath);
|
|
279
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
280
|
+
const { valid } = validateNote(frontmatter, body);
|
|
281
|
+
return { relPath, frontmatter, body, raw, mtime: st.mtimeMs, size: st.size, schemaValid: valid };
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Write a note. Validates frontmatter, scans for secrets, writes the file.
|
|
289
|
+
*
|
|
290
|
+
* @param {string} projectRoot
|
|
291
|
+
* @param {string} relPath
|
|
292
|
+
* @param {{ frontmatter: Record<string, unknown>, body: string }} note
|
|
293
|
+
* @returns {{ relPath: string, frontmatter: Record<string, unknown>, body: string, raw: string, mtime: number, size: number, schemaValid: boolean }}
|
|
294
|
+
* @throws {Error} if schema validation fails or HIGH-severity secret found
|
|
295
|
+
*/
|
|
296
|
+
export function writeNote(projectRoot, relPath, { frontmatter, body }) {
|
|
297
|
+
const { vaultRoot } = resolveVault(projectRoot);
|
|
298
|
+
|
|
299
|
+
// Validate frontmatter
|
|
300
|
+
const { valid, errors, warnings } = validateNote(frontmatter, body);
|
|
301
|
+
if (!valid) {
|
|
302
|
+
const err = new Error(`schema validation failed: ${errors.join('; ')}`);
|
|
303
|
+
err.code = 'SCHEMA_VALIDATION_FAILED';
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Scan for secrets
|
|
308
|
+
const secretResult = scanSecrets(body);
|
|
309
|
+
if (hasHighFindings(secretResult)) {
|
|
310
|
+
const snippets = secretResult.findings
|
|
311
|
+
.filter((f) => f.severity === 'HIGH')
|
|
312
|
+
.map((f) => `line ${f.line}: ${f.snippet}`)
|
|
313
|
+
.join('\n');
|
|
314
|
+
const err = new Error(`HIGH-severity secret detected:\n${snippets}`);
|
|
315
|
+
err.code = 'SECRET_DETECTED';
|
|
316
|
+
err.findings = secretResult.findings;
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Build file content
|
|
321
|
+
const filePath = resolveSafe(vaultRoot, relPath);
|
|
322
|
+
if (!filePath) {
|
|
323
|
+
throw new Error(`Invalid note path: ${relPath}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Lazily create the project namespace directory (F7 — not created at initVault)
|
|
327
|
+
const nsDir = dirname(filePath);
|
|
328
|
+
if (!existsSync(nsDir)) {
|
|
329
|
+
mkdirSync(nsDir, { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const yamlBlock = serializeFrontmatter(frontmatter);
|
|
333
|
+
const content = `---\n${yamlBlock}\n---\n\n${body}`;
|
|
334
|
+
writeFileSync(filePath, content, 'utf8');
|
|
335
|
+
|
|
336
|
+
const st = statSync(filePath);
|
|
337
|
+
return {
|
|
338
|
+
relPath,
|
|
339
|
+
frontmatter,
|
|
340
|
+
body,
|
|
341
|
+
raw: content,
|
|
342
|
+
mtime: st.mtimeMs,
|
|
343
|
+
size: st.size,
|
|
344
|
+
schemaValid: true,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Delete a note by relative path.
|
|
350
|
+
*
|
|
351
|
+
* @param {string} projectRoot
|
|
352
|
+
* @param {string} relPath
|
|
353
|
+
* @returns {boolean}
|
|
354
|
+
*/
|
|
355
|
+
export function deleteNote(projectRoot, relPath) {
|
|
356
|
+
const { vaultRoot } = resolveVault(projectRoot);
|
|
357
|
+
const filePath = resolveSafe(vaultRoot, relPath);
|
|
358
|
+
if (!filePath || !existsSync(filePath)) return false;
|
|
359
|
+
unlinkSync(filePath);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Full-text search across vault notes.
|
|
365
|
+
*
|
|
366
|
+
* @param {string} projectRoot
|
|
367
|
+
* @param {string} query
|
|
368
|
+
* @param {{ limit?: number }} [opts]
|
|
369
|
+
* @returns {Array<{ relPath: string, snippet: string, score: number, mtime: number }>}
|
|
370
|
+
*/
|
|
371
|
+
export function searchVault(projectRoot, query, { limit = 25 } = {}) {
|
|
372
|
+
if (!query || typeof query !== 'string') return [];
|
|
373
|
+
const q = query.toLowerCase();
|
|
374
|
+
const tokens = q.split(/\s+/).filter((t) => t.length >= 2);
|
|
375
|
+
if (tokens.length === 0) return [];
|
|
376
|
+
|
|
377
|
+
const results = [];
|
|
378
|
+
for (const note of listNotes(projectRoot)) {
|
|
379
|
+
const lower = (note.body + ' ' + JSON.stringify(note.frontmatter)).toLowerCase();
|
|
380
|
+
let score = 0;
|
|
381
|
+
for (const t of tokens) {
|
|
382
|
+
if (lower.includes(t)) score += (lower.split(t).length - 1);
|
|
383
|
+
}
|
|
384
|
+
if (score === 0) continue;
|
|
385
|
+
const idx = lower.indexOf(tokens[0]);
|
|
386
|
+
const start = Math.max(0, idx - 60);
|
|
387
|
+
const end = Math.min(lower.length, idx + 160);
|
|
388
|
+
const snippet = (start > 0 ? '…' : '') + lower.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
389
|
+
results.push({ relPath: note.relPath, snippet, score, mtime: note.mtime });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Scan a single note for secrets.
|
|
397
|
+
*
|
|
398
|
+
* @param {string} projectRoot
|
|
399
|
+
* @param {string} relPath
|
|
400
|
+
* @returns {{ safe: boolean, findings: Array<{ id: string, severity: string, line: number, snippet: string }> }}
|
|
401
|
+
*/
|
|
402
|
+
export function scanForSecrets(projectRoot, relPath) {
|
|
403
|
+
const note = readNote(projectRoot, relPath);
|
|
404
|
+
if (!note) return { safe: true, findings: [] };
|
|
405
|
+
return scanSecrets(note.body);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Validate all notes in the vault against the schema.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} projectRoot
|
|
412
|
+
* @returns {Array<{ relPath: string, errors: string[] }>}
|
|
413
|
+
*/
|
|
414
|
+
export function validateAll(projectRoot) {
|
|
415
|
+
const notes = listNotes(projectRoot);
|
|
416
|
+
const results = [];
|
|
417
|
+
for (const note of notes) {
|
|
418
|
+
const { errors } = validateNote(note.frontmatter, note.body);
|
|
419
|
+
if (errors.length > 0) {
|
|
420
|
+
results.push({ relPath: note.relPath, errors });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return results;
|
|
424
|
+
}
|