@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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/lib/path-safe.mjs
|
|
3
|
+
*
|
|
4
|
+
* Helpers for the file-browser / scan endpoints that need to accept
|
|
5
|
+
* an arbitrary absolute path from the authenticated client and decide
|
|
6
|
+
* whether it is safe to operate on.
|
|
7
|
+
*
|
|
8
|
+
* The dashboard runs on a developer's machine, so the boundary here
|
|
9
|
+
* is the user's home directory. Any path outside `os.homedir()` (and
|
|
10
|
+
* any explicitly-listed additional allowed root, validated by the
|
|
11
|
+
* caller) is rejected before we ever call `fs` on it.
|
|
12
|
+
*
|
|
13
|
+
* No `..` traversal escapes — `path.resolve` normalizes first, then
|
|
14
|
+
* we check the resolved string against the allow-list.
|
|
15
|
+
*/
|
|
16
|
+
import { resolve as pathResolve, sep, isAbsolute } from 'node:path';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize an arbitrary list of root paths and dedupe them. Empty /
|
|
22
|
+
* non-string entries are dropped silently so callers can pass
|
|
23
|
+
* `readSettings().dashboard.projectsDirectory` directly.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<string | null | undefined>} roots
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
function normalizeRoots(roots) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const raw of roots) {
|
|
32
|
+
if (typeof raw !== 'string') continue;
|
|
33
|
+
const trimmed = raw.trim();
|
|
34
|
+
if (!trimmed) continue;
|
|
35
|
+
const resolved = pathResolve(trimmed);
|
|
36
|
+
if (seen.has(resolved)) continue;
|
|
37
|
+
seen.add(resolved);
|
|
38
|
+
out.push(resolved);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the default allow-list: the user's home plus any explicit
|
|
45
|
+
* extra roots. The caller is expected to validate the extras against
|
|
46
|
+
* home before calling this (we don't try to be clever about Windows
|
|
47
|
+
* reparse points, symlinks, etc. — this is a developer-tool boundary,
|
|
48
|
+
* not a chroot).
|
|
49
|
+
*
|
|
50
|
+
* @param {object} [opts]
|
|
51
|
+
* @param {string} [opts.home] — override for `os.homedir()`
|
|
52
|
+
* @param {Array<string>} [opts.extras] — additional absolute roots (already resolved)
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
export function defaultAllowedRoots({ home, extras } = {}) {
|
|
56
|
+
return normalizeRoots([home || homedir(), ...(extras || [])]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the per-request allow-list straight from a settings object.
|
|
61
|
+
* Each configured entry is re-validated against home; entries that
|
|
62
|
+
* fail validation (escape home, empty, non-string, NUL/backslash,
|
|
63
|
+
* …) are silently dropped. The intent is to survive a tampered
|
|
64
|
+
* settings.json: the writer (`validateDashboardSettings`) catches
|
|
65
|
+
* the bad values, but the reader has to be robust against anything
|
|
66
|
+
* on disk.
|
|
67
|
+
*
|
|
68
|
+
* Includes both:
|
|
69
|
+
* - `dashboard.allowedRoots[]` (operator-declared extras)
|
|
70
|
+
* - `dashboard.projectsDirectory` (single legacy extra)
|
|
71
|
+
*
|
|
72
|
+
* @param {object} opts
|
|
73
|
+
* @param {Record<string, unknown> | null | undefined} opts.settings
|
|
74
|
+
* @param {string} [opts.home] — override for `os.homedir()`
|
|
75
|
+
* @returns {string[]}
|
|
76
|
+
*/
|
|
77
|
+
export function buildAllowedRootsFromSettings({ settings, home } = {}) {
|
|
78
|
+
const homeResolved = home || homedir();
|
|
79
|
+
const extras = [];
|
|
80
|
+
const configured = settings?.dashboard?.allowedRoots;
|
|
81
|
+
if (Array.isArray(configured)) {
|
|
82
|
+
for (const candidate of configured) {
|
|
83
|
+
if (typeof candidate !== 'string' || !candidate.trim()) continue;
|
|
84
|
+
const safe = resolveSafePath(candidate, [homeResolved]);
|
|
85
|
+
if (safe) extras.push(safe);
|
|
86
|
+
// Silently skip entries that fail validation. The Settings UI
|
|
87
|
+
// (frontend) is responsible for surfacing a warning when a
|
|
88
|
+
// user-typed root is silently dropped.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const proj = settings?.dashboard?.projectsDirectory;
|
|
92
|
+
if (typeof proj === 'string' && proj.trim()) {
|
|
93
|
+
const safe = resolveSafePath(proj, [homeResolved]);
|
|
94
|
+
if (safe) extras.push(safe);
|
|
95
|
+
}
|
|
96
|
+
return defaultAllowedRoots({ home: homeResolved, extras });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve and authorize a requested absolute path against an
|
|
101
|
+
* allow-list of roots.
|
|
102
|
+
*
|
|
103
|
+
* Rules:
|
|
104
|
+
* - The input must be a non-empty string.
|
|
105
|
+
* - The path is `path.resolve()`d — `..` segments collapse.
|
|
106
|
+
* - The resolved path must be `===` to one of the allowed roots,
|
|
107
|
+
* OR live strictly beneath it (segment-aware — `pathA` does NOT
|
|
108
|
+
* count as "inside" `pathB` if `pathB` is just a prefix string).
|
|
109
|
+
* - Dotfile / dotdir roots (e.g. `~/.ssh`, `~/.aws`) are rejected
|
|
110
|
+
* even when they live under home. The frontend can still SEE them
|
|
111
|
+
* in listings (so users know the directory exists) but cannot
|
|
112
|
+
* enter them via the browser.
|
|
113
|
+
*
|
|
114
|
+
* Returns `null` on any failure. The caller is expected to translate
|
|
115
|
+
* `null` into a 403 with a clear message.
|
|
116
|
+
*
|
|
117
|
+
* @param {unknown} requestedPath
|
|
118
|
+
* @param {string[]} allowedRoots
|
|
119
|
+
* @returns {string | null}
|
|
120
|
+
*/
|
|
121
|
+
export function resolveSafePath(requestedPath, allowedRoots) {
|
|
122
|
+
if (typeof requestedPath !== 'string') return null;
|
|
123
|
+
const trimmed = requestedPath.trim();
|
|
124
|
+
if (!trimmed) return null;
|
|
125
|
+
const resolved = pathResolve(trimmed);
|
|
126
|
+
|
|
127
|
+
const roots = Array.isArray(allowedRoots) ? allowedRoots : [];
|
|
128
|
+
for (const root of roots) {
|
|
129
|
+
if (typeof root !== 'string' || !root) continue;
|
|
130
|
+
const resolvedRoot = pathResolve(root);
|
|
131
|
+
if (resolved === resolvedRoot) {
|
|
132
|
+
if (isDotRoot(resolved)) return null;
|
|
133
|
+
return resolved;
|
|
134
|
+
}
|
|
135
|
+
// Strict segment-aware prefix: ensure a separator boundary so
|
|
136
|
+
// `/home/user/foo` is NOT considered inside `/home/user/foobar`.
|
|
137
|
+
const rootWithSep = resolvedRoot.endsWith(sep)
|
|
138
|
+
? resolvedRoot
|
|
139
|
+
: resolvedRoot + sep;
|
|
140
|
+
if (resolved.startsWith(rootWithSep)) {
|
|
141
|
+
if (isDotRoot(resolved)) return null;
|
|
142
|
+
return resolved;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Return `true` if the path is (or lives directly inside) a dotdir
|
|
150
|
+
* at the home root. We deliberately do NOT match deeper dotdirs
|
|
151
|
+
* (`/home/user/projects/.secret` is fine to enter) — only the ones
|
|
152
|
+
* at the top of the home tree are treated as sensitive.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} resolvedPath
|
|
155
|
+
* @param {string} [home]
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function isDotRoot(resolvedPath, home = homedir()) {
|
|
159
|
+
if (typeof resolvedPath !== 'string') return false;
|
|
160
|
+
const homeRoot = pathResolve(home);
|
|
161
|
+
const homeWithSep = homeRoot.endsWith(sep) ? homeRoot : homeRoot + sep;
|
|
162
|
+
if (resolvedPath === homeRoot) return false;
|
|
163
|
+
if (!resolvedPath.startsWith(homeWithSep)) return false;
|
|
164
|
+
const tail = resolvedPath.slice(homeWithSep.length);
|
|
165
|
+
if (!tail) return false;
|
|
166
|
+
// Only first-level dotdirs under home are blocked.
|
|
167
|
+
const first = tail.split(sep)[0];
|
|
168
|
+
return first.startsWith('.') && first.length > 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Background-agent logPath reconstruction ------------------------------
|
|
172
|
+
//
|
|
173
|
+
// The plugin's `bgr_<id>.json` state files include a `logPath` field
|
|
174
|
+
// built as `${worktree}/.opencode/log/${id}.log`. If `worktree` was
|
|
175
|
+
// empty or `/` when the bg instance was spawned, the resulting
|
|
176
|
+
// `logPath` is broken (e.g. `//.opencode/log/...`) and no log file
|
|
177
|
+
// can ever be written. The bg-retry loop calls into this helper to
|
|
178
|
+
// repair the field before re-dispatching.
|
|
179
|
+
//
|
|
180
|
+
// `deriveAbsoluteBgLogPath(worktree, instanceId)` always returns an
|
|
181
|
+
// absolute path. It falls back to the user's home directory when
|
|
182
|
+
// `worktree` is missing or non-absolute. Never throws — a bad input
|
|
183
|
+
// produces a synthetic `~/.cache/bizar/logs/<id>.log` rather than
|
|
184
|
+
// crashing the retry loop.
|
|
185
|
+
//
|
|
186
|
+
// v3.11.1 — Note on log path correctness:
|
|
187
|
+
// The plugin's `LogWriter` (plugins/bizar/src/report.ts:147) writes
|
|
188
|
+
// to `${logDir}/${sessionId}.log` where `logDir` defaults to
|
|
189
|
+
// `~/.cache/bizar/logs` (plugins/bizar/src/options.ts:88). The
|
|
190
|
+
// bg-spawn tool records a DIFFERENT path —
|
|
191
|
+
// `${worktree}/.opencode/log/${instanceId}.log` — in the state
|
|
192
|
+
// file. Nothing ever writes to that path. This module's
|
|
193
|
+
// `deriveAbsoluteBgLogPath` historically returned the same broken
|
|
194
|
+
// path; the new `getActualBgLogPath` below returns the path the
|
|
195
|
+
// LogWriter actually writes to. Use that for any operator-facing
|
|
196
|
+
// `tail -F` (e.g. the tmux wrap in task-delegator.mjs).
|
|
197
|
+
|
|
198
|
+
const FALLBACK_LOG_DIR = pathResolve(homedir(), '.cache', 'bizar', 'logs');
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the per-session log directory used by the plugin's
|
|
202
|
+
* `LogWriter`. The plugin's default is `~/.cache/bizar/logs`
|
|
203
|
+
* (configurable via the `logDir` option in `opencode.json`). We
|
|
204
|
+
* honor the `BIZAR_LOG_DIR` env var first, fall back to the
|
|
205
|
+
* plugin's default, and never throw.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} [opts]
|
|
208
|
+
* @param {string} [opts.env] — env-var bag to read (defaults to process.env)
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
export function getBgLogDir({ env } = {}) {
|
|
212
|
+
const source = env || process.env;
|
|
213
|
+
const fromEnv = source?.BIZAR_LOG_DIR;
|
|
214
|
+
if (typeof fromEnv === 'string' && fromEnv.trim()) {
|
|
215
|
+
const resolved = pathResolve(fromEnv);
|
|
216
|
+
return resolved;
|
|
217
|
+
}
|
|
218
|
+
return FALLBACK_LOG_DIR;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the per-session log path the plugin's `LogWriter` actually
|
|
223
|
+
* writes to. Use this for any operator-facing `tail -F` so the user
|
|
224
|
+
* sees real activity rather than a phantom-file error.
|
|
225
|
+
*
|
|
226
|
+
* Important: the LogWriter keys files by SESSION id (not instance
|
|
227
|
+
* id). When you only have an `instanceId` (e.g. `bg_<sessionId16>`),
|
|
228
|
+
* pass it as `sessionId` — the function will accept any non-empty
|
|
229
|
+
* string and sanitize it.
|
|
230
|
+
*
|
|
231
|
+
* @param {object} [opts]
|
|
232
|
+
* @param {string} [opts.sessionId] — the opencode session id (or any unique key)
|
|
233
|
+
* @param {string} [opts.env] — env-var bag to read
|
|
234
|
+
* @returns {string} absolute path that exists or will exist once the plugin writes
|
|
235
|
+
*/
|
|
236
|
+
export function getActualBgLogPath({ sessionId, env } = {}) {
|
|
237
|
+
const safeId = typeof sessionId === 'string' && sessionId.length > 0
|
|
238
|
+
? sessionId.replace(/[^a-zA-Z0-9_.-]/g, '_')
|
|
239
|
+
: `unknown_${randomBytes(4).toString('hex')}`;
|
|
240
|
+
return pathResolve(getBgLogDir({ env }), `${safeId}.log`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {unknown} worktree
|
|
245
|
+
* @param {string} instanceId
|
|
246
|
+
* @returns {string}
|
|
247
|
+
*/
|
|
248
|
+
export function deriveAbsoluteBgLogPath(worktree, instanceId) {
|
|
249
|
+
const safeId = typeof instanceId === 'string' && instanceId.length > 0
|
|
250
|
+
? instanceId.replace(/[^a-zA-Z0-9_.-]/g, '_')
|
|
251
|
+
: `unknown_${randomBytes(4).toString('hex')}`;
|
|
252
|
+
const base = typeof worktree === 'string' && worktree.length > 0 && isAbsolute(worktree)
|
|
253
|
+
? pathResolve(worktree)
|
|
254
|
+
: FALLBACK_LOG_DIR;
|
|
255
|
+
return pathResolve(base, '.opencode', 'log', `${safeId}.log`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Decide whether a `logPath` value stored on a bg instance needs to
|
|
260
|
+
* be repaired. A path is considered broken when it is:
|
|
261
|
+
* - not a string
|
|
262
|
+
* - empty
|
|
263
|
+
* - not absolute
|
|
264
|
+
* - contains a literal double-slash sequence (`//` outside of the
|
|
265
|
+
* protocol prefix) — a tell-tale sign of `${empty}/${...}`
|
|
266
|
+
* concatenation
|
|
267
|
+
*
|
|
268
|
+
* @param {unknown} logPath
|
|
269
|
+
* @returns {boolean}
|
|
270
|
+
*/
|
|
271
|
+
export function isBrokenBgLogPath(logPath) {
|
|
272
|
+
if (typeof logPath !== 'string' || logPath.length === 0) return true;
|
|
273
|
+
if (!isAbsolute(logPath)) return true;
|
|
274
|
+
// Look for `//` not at the protocol position. Cheap heuristic that
|
|
275
|
+
// catches the user's `//.opencode/log/...` case without requiring
|
|
276
|
+
// a full URL parser.
|
|
277
|
+
const idx = logPath.indexOf('//');
|
|
278
|
+
if (idx === -1) return false;
|
|
279
|
+
// Allow a leading `//` only on Windows-style UNC paths like `\\server\share`.
|
|
280
|
+
// On POSIX, a leading `/` followed by another `/` is always broken.
|
|
281
|
+
if (idx === 0) return true;
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/memory-git.mjs
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around the `git` binary for the Bizar Memory Service.
|
|
5
|
+
* All operations use `child_process.execFileSync` with array args — never
|
|
6
|
+
* string interpolation — to prevent injection vulnerabilities.
|
|
7
|
+
*
|
|
8
|
+
* Operations: isGitInstalled, clone, pull, commit, push, status, acquireLock,
|
|
9
|
+
* addFile, addAll.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @returns {boolean} true if `git --version` exits 0.
|
|
18
|
+
*/
|
|
19
|
+
export function isGitInstalled() {
|
|
20
|
+
try {
|
|
21
|
+
execFileSync('git', ['--version'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clone a remote repository.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} remote — URL or file path of the remote to clone
|
|
32
|
+
* @param {string} targetDir — absolute path to clone into
|
|
33
|
+
* @param {{ branch?: string, depth?: number }} [opts]
|
|
34
|
+
* @returns {{ ok: boolean, output: string, error?: string }}
|
|
35
|
+
*/
|
|
36
|
+
export function clone(remote, targetDir, { branch = 'main', depth = 1 } = {}) {
|
|
37
|
+
try {
|
|
38
|
+
const args = ['clone', '--branch', branch, '--depth', String(depth), remote, targetDir];
|
|
39
|
+
const output = execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
40
|
+
return { ok: true, output };
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return { ok: false, output: '', error: err.message };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pull changes into a local working directory.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} dir — working directory (must be a git repo)
|
|
50
|
+
* @param {{ rebase?: boolean }} [opts]
|
|
51
|
+
* @returns {{ ok: boolean, output: string, error?: string }}
|
|
52
|
+
*/
|
|
53
|
+
export function pull(dir, { rebase = true } = {}) {
|
|
54
|
+
try {
|
|
55
|
+
const args = rebase ? ['pull', '--rebase'] : ['pull', '--ff-only'];
|
|
56
|
+
const output = execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
57
|
+
return { ok: true, output };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { ok: false, output: '', error: err.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Commit staged changes.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} dir — working directory
|
|
67
|
+
* @param {string} message — commit message
|
|
68
|
+
* @param {{ author?: string }} [opts]
|
|
69
|
+
* @returns {{ ok: boolean, output: string, error?: string }}
|
|
70
|
+
*/
|
|
71
|
+
export function commit(dir, message, { author } = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const args = ['commit', '-m', message];
|
|
74
|
+
if (author) {
|
|
75
|
+
args.push('--author', author);
|
|
76
|
+
}
|
|
77
|
+
const output = execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
78
|
+
return { ok: true, output };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return { ok: false, output: '', error: err.message };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Push commits to a remote.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} dir
|
|
88
|
+
* @param {{ remote?: string, branch?: string }} [opts]
|
|
89
|
+
* @returns {{ ok: boolean, output: string, error?: string }}
|
|
90
|
+
*/
|
|
91
|
+
export function push(dir, { remote = 'origin', branch = 'main' } = {}) {
|
|
92
|
+
try {
|
|
93
|
+
const output = execFileSync(
|
|
94
|
+
'git',
|
|
95
|
+
['push', remote, branch],
|
|
96
|
+
{ cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
97
|
+
);
|
|
98
|
+
return { ok: true, output };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return { ok: false, output: '', error: err.message };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the current git status for a repository.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} dir
|
|
108
|
+
* @returns {{ ok: boolean, clean: boolean, branch: string, ahead: number, behind: number, modified: string[], untracked: string[], error?: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function status(dir) {
|
|
111
|
+
try {
|
|
112
|
+
// Get branch name
|
|
113
|
+
let branch = 'main';
|
|
114
|
+
try {
|
|
115
|
+
branch = execFileSync('git', ['branch', '--show-current'], {
|
|
116
|
+
cwd: dir,
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
|
+
}).trim();
|
|
120
|
+
} catch { /* ignore */ }
|
|
121
|
+
|
|
122
|
+
// Get status --porcelain
|
|
123
|
+
const raw = execFileSync('git', ['status', '--porcelain'], {
|
|
124
|
+
cwd: dir,
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const modified = [];
|
|
130
|
+
const untracked = [];
|
|
131
|
+
for (const line of raw.split('\n')) {
|
|
132
|
+
if (!line.trim()) continue;
|
|
133
|
+
const code = line.slice(0, 2);
|
|
134
|
+
const path = line.slice(3);
|
|
135
|
+
if (code === '??') {
|
|
136
|
+
untracked.push(path);
|
|
137
|
+
} else {
|
|
138
|
+
modified.push(path);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get ahead/behind vs tracking branch
|
|
143
|
+
let ahead = 0;
|
|
144
|
+
let behind = 0;
|
|
145
|
+
try {
|
|
146
|
+
const revparse = execFileSync('git', ['revparse', '--abbrev-ref', '@{upstream}'], {
|
|
147
|
+
cwd: dir,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
|
+
}).trim();
|
|
151
|
+
if (revparse) {
|
|
152
|
+
const aheadBehind = execFileSync('git', ['rev-list', '--left-right', '--count', `${branch}...${revparse}`], {
|
|
153
|
+
cwd: dir,
|
|
154
|
+
encoding: 'utf8',
|
|
155
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
|
+
}).trim();
|
|
157
|
+
const [a, b] = aheadBehind.split('\t').map(Number);
|
|
158
|
+
ahead = a || 0;
|
|
159
|
+
behind = b || 0;
|
|
160
|
+
}
|
|
161
|
+
} catch { /* no upstream */ }
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
clean: modified.length === 0 && untracked.length === 0,
|
|
166
|
+
branch,
|
|
167
|
+
ahead,
|
|
168
|
+
behind,
|
|
169
|
+
modified,
|
|
170
|
+
untracked,
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return { ok: false, clean: false, branch: 'main', ahead: 0, behind: 0, modified: [], untracked: [], error: err.message };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Acquire a per-repo mutex lockfile. Returns a release function or an error.
|
|
179
|
+
* The lock file contains { pid, startedAt }. If another live PID holds the lock,
|
|
180
|
+
* returns { error: 'locked' }.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} repoDir — the git repository root
|
|
183
|
+
* @returns {{ release: () => void } | { error: 'locked' }}
|
|
184
|
+
*/
|
|
185
|
+
export function acquireLock(repoDir) {
|
|
186
|
+
const lockPath = join(repoDir, '.sync.lock');
|
|
187
|
+
const pid = process.pid;
|
|
188
|
+
const startedAt = Date.now();
|
|
189
|
+
|
|
190
|
+
if (existsSync(lockPath)) {
|
|
191
|
+
try {
|
|
192
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
193
|
+
// Check if the PID is still alive
|
|
194
|
+
try {
|
|
195
|
+
process.kill(lockData.pid, 0);
|
|
196
|
+
// Still alive — someone else holds the lock
|
|
197
|
+
return { error: 'locked' };
|
|
198
|
+
} catch {
|
|
199
|
+
// PID is dead — stale lock, we'll take over
|
|
200
|
+
}
|
|
201
|
+
} catch { /* corrupt lockfile — take over */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
writeFileSync(lockPath, JSON.stringify({ pid, startedAt }), 'utf8');
|
|
205
|
+
|
|
206
|
+
const release = () => {
|
|
207
|
+
try {
|
|
208
|
+
if (existsSync(lockPath)) {
|
|
209
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
210
|
+
if (lockData.pid === pid) {
|
|
211
|
+
unlinkSync(lockPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch { /* ignore */ }
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return { release };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Stage a single file for commit.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} dir — working directory
|
|
224
|
+
* @param {string} filePath — path relative to dir
|
|
225
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
226
|
+
*/
|
|
227
|
+
export function addFile(dir, filePath) {
|
|
228
|
+
try {
|
|
229
|
+
execFileSync('git', ['add', filePath], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
230
|
+
return { ok: true };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
return { ok: false, error: err.message };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Stage all changes (including untracked) for commit.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} dir
|
|
240
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
241
|
+
*/
|
|
242
|
+
export function addAll(dir) {
|
|
243
|
+
try {
|
|
244
|
+
execFileSync('git', ['add', '-A'], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
245
|
+
return { ok: true };
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return { ok: false, error: err.message };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/memory-schema.mjs
|
|
3
|
+
*
|
|
4
|
+
* Validates frontmatter against the Bizar Memory Schema. The schema requires
|
|
5
|
+
* a set of standard fields (memory_id, type, project_id, status, confidence,
|
|
6
|
+
* created, updated, tags) with enumerated values for type, status, and
|
|
7
|
+
* confidence.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const REQUIRED_FIELDS = [
|
|
11
|
+
'memory_id',
|
|
12
|
+
'type',
|
|
13
|
+
'project_id',
|
|
14
|
+
'status',
|
|
15
|
+
'confidence',
|
|
16
|
+
'created',
|
|
17
|
+
'updated',
|
|
18
|
+
'tags',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const VALID_TYPES = [
|
|
22
|
+
'project_overview',
|
|
23
|
+
'architecture_decision',
|
|
24
|
+
'coding_convention',
|
|
25
|
+
'bug_pattern',
|
|
26
|
+
'command',
|
|
27
|
+
'api_contract',
|
|
28
|
+
'dependency_note',
|
|
29
|
+
'environment_fact',
|
|
30
|
+
'task_summary',
|
|
31
|
+
'session_summary',
|
|
32
|
+
'user_preference',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const VALID_STATUSES = [
|
|
36
|
+
'active',
|
|
37
|
+
'superseded',
|
|
38
|
+
'stale',
|
|
39
|
+
'conflict',
|
|
40
|
+
'draft',
|
|
41
|
+
'archived',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const VALID_CONFIDENCES = ['verified', 'inferred', 'speculative'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validate a note's frontmatter and body against the schema.
|
|
48
|
+
*
|
|
49
|
+
* @param {Record<string, unknown>} frontmatter
|
|
50
|
+
* @param {string} body
|
|
51
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
52
|
+
*/
|
|
53
|
+
export function validateNote(frontmatter, body) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
const warnings = [];
|
|
56
|
+
|
|
57
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
58
|
+
errors.push('frontmatter must be a plain object');
|
|
59
|
+
return { valid: false, errors, warnings };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Required fields
|
|
63
|
+
for (const field of REQUIRED_FIELDS) {
|
|
64
|
+
if (!(field in frontmatter) || frontmatter[field] === undefined || frontmatter[field] === null || frontmatter[field] === '') {
|
|
65
|
+
errors.push(`missing required field: ${field}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Type enum
|
|
70
|
+
if (frontmatter.type && !VALID_TYPES.includes(frontmatter.type)) {
|
|
71
|
+
errors.push(`invalid type: '${frontmatter.type}'. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Status enum
|
|
75
|
+
if (frontmatter.status && !VALID_STATUSES.includes(frontmatter.status)) {
|
|
76
|
+
errors.push(`invalid status: '${frontmatter.status}'. Must be one of: ${VALID_STATUSES.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Confidence enum
|
|
80
|
+
if (frontmatter.confidence && !VALID_CONFIDENCES.includes(frontmatter.confidence)) {
|
|
81
|
+
errors.push(`invalid confidence: '${frontmatter.confidence}'. Must be one of: ${VALID_CONFIDENCES.join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Tags must be an array
|
|
85
|
+
if (frontmatter.tags !== undefined && !Array.isArray(frontmatter.tags)) {
|
|
86
|
+
errors.push('tags must be an array');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// memory_id format check: should be non-empty string
|
|
90
|
+
if (frontmatter.memory_id && typeof frontmatter.memory_id !== 'string') {
|
|
91
|
+
errors.push('memory_id must be a string');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// project_id format check
|
|
95
|
+
if (frontmatter.project_id && typeof frontmatter.project_id !== 'string') {
|
|
96
|
+
errors.push('project_id must be a string');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ISO-8601 date fields
|
|
100
|
+
for (const dateField of ['created', 'updated']) {
|
|
101
|
+
if (frontmatter[dateField]) {
|
|
102
|
+
const val = String(frontmatter[dateField]);
|
|
103
|
+
if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/.test(val)) {
|
|
104
|
+
warnings.push(`${dateField} is not a valid ISO-8601 date: '${val}'`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Body presence check
|
|
110
|
+
if (!body || !body.trim()) {
|
|
111
|
+
warnings.push('body is empty — notes should have substantive content');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
valid: errors.length === 0,
|
|
116
|
+
errors,
|
|
117
|
+
warnings,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a default frontmatter object from a partial override.
|
|
123
|
+
* Fills in all missing required fields with sensible defaults.
|
|
124
|
+
*
|
|
125
|
+
* @param {Partial<Record<string, unknown>>} partial
|
|
126
|
+
* @returns {Record<string, unknown>}
|
|
127
|
+
*/
|
|
128
|
+
export function defaultFrontmatter(partial = {}) {
|
|
129
|
+
const now = new Date().toISOString();
|
|
130
|
+
const timestamp = Date.now();
|
|
131
|
+
const type = partial.type || 'session_summary';
|
|
132
|
+
const memoryId = partial.memory_id || `${type}_${timestamp}`;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
memory_id: memoryId,
|
|
136
|
+
type: type,
|
|
137
|
+
project_id: partial.project_id || '',
|
|
138
|
+
status: partial.status || 'draft',
|
|
139
|
+
confidence: partial.confidence || 'inferred',
|
|
140
|
+
created: partial.created || now,
|
|
141
|
+
updated: partial.updated || now,
|
|
142
|
+
tags: Array.isArray(partial.tags) ? partial.tags : [],
|
|
143
|
+
...partial,
|
|
144
|
+
};
|
|
145
|
+
}
|