@polderlabs/bizar 3.23.0 → 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,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/mods.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/mods — list
|
|
5
|
+
* /api/mods/registry — fetch available mods from public registry
|
|
6
|
+
* /api/mods/audit — tail the mod audit log
|
|
7
|
+
* /api/mods/:id — metadata
|
|
8
|
+
* /api/mods (POST) — install from path OR registry id
|
|
9
|
+
* /api/mods/:id (PUT) — enable/disable
|
|
10
|
+
* /api/mods/:id (DELETE) — uninstall
|
|
11
|
+
* /api/mods/:id/files — file tree
|
|
12
|
+
* /api/mods/:id/mod-file/* (GET) — read single file (named route to avoid :id/* clash)
|
|
13
|
+
* /api/mods/:id/mod-file/* (PUT) — write single file
|
|
14
|
+
* /api/mods/views — mod view registry
|
|
15
|
+
* /api/mods/:id/mod-web/* — serve files from mod's web/ dir
|
|
16
|
+
* /api/mods/:id/instructions — list installed instruction files (v3.20)
|
|
17
|
+
* /api/mods/:id/instructions/reinstall — force-reinstall instructions (v3.20)
|
|
18
|
+
* /api/mods/:id/upgrade — upgrade an installed mod from the registry (v3.20.5)
|
|
19
|
+
* /api/mods/:id/views/* — serve a mod-shipped React view (v3.20.5)
|
|
20
|
+
*
|
|
21
|
+
* Express-ordering note: /mods/views MUST come before /mods/:id/* —
|
|
22
|
+
* actually only the literal /mods (without :id) takes precedence
|
|
23
|
+
* over /mods/:id, but /mods/views is the same shape as /mods/:id so
|
|
24
|
+
* we declare it before /mods/:id to be safe.
|
|
25
|
+
*/
|
|
26
|
+
import { Router } from 'express';
|
|
27
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
28
|
+
import { resolve, relative, join } from 'node:path';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { modsLoader } from '../mods-loader.mjs';
|
|
31
|
+
import { wrap } from './_shared.mjs';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read a file at an absolute path or under $HOME. Returns null if
|
|
35
|
+
* the file doesn't exist or can't be read. The path argument may
|
|
36
|
+
* use `~` as a home-relative shortcut (resolved against homedir()).
|
|
37
|
+
*/
|
|
38
|
+
function tryRead(p) {
|
|
39
|
+
try {
|
|
40
|
+
const full = p.startsWith('~/') || p === '~' ? join(homedir(), p.slice(1)) : p;
|
|
41
|
+
if (!existsSync(full)) return null;
|
|
42
|
+
return readFileSync(full, 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @returns {import('express').Router}
|
|
50
|
+
*/
|
|
51
|
+
export function createModsRouter() {
|
|
52
|
+
const router = Router();
|
|
53
|
+
|
|
54
|
+
router.get('/mods', wrap(async (_req, res) => {
|
|
55
|
+
res.json({ mods: modsLoader.list() });
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// /mods/registry MUST come before /mods/:id — otherwise Express would
|
|
59
|
+
// match "registry" as an :id value.
|
|
60
|
+
router.get('/mods/registry', wrap(async (_req, res) => {
|
|
61
|
+
const installed = new Set(modsLoader.list().map((m) => m.id));
|
|
62
|
+
try {
|
|
63
|
+
const registry = await modsLoader.fetchRegistry();
|
|
64
|
+
// Annotate each entry with installed flag and (if installed) the
|
|
65
|
+
// local version so the dashboard can show upgrade hints.
|
|
66
|
+
const mods = (registry.mods || []).map((m) => {
|
|
67
|
+
const local = installed.has(m.id) ? modsLoader.get(m.id) : null;
|
|
68
|
+
return {
|
|
69
|
+
...m,
|
|
70
|
+
installed: local !== null,
|
|
71
|
+
installedVersion: local ? local.version : null,
|
|
72
|
+
upgradeAvailable:
|
|
73
|
+
local && local.version !== m.latest ? m.latest : null,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
res.json({
|
|
77
|
+
registry: {
|
|
78
|
+
version: registry.version,
|
|
79
|
+
updatedAt: registry.updatedAt,
|
|
80
|
+
source: modsLoader.getRegistryUrl(),
|
|
81
|
+
},
|
|
82
|
+
mods,
|
|
83
|
+
});
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res.status(502).json({
|
|
86
|
+
error: 'registry_unreachable',
|
|
87
|
+
message: err.message,
|
|
88
|
+
registryUrl: modsLoader.getRegistryUrl(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// /mods/audit — tail the mod audit log. Filters by mod id if provided.
|
|
94
|
+
router.get('/mods/audit', wrap(async (req, res) => {
|
|
95
|
+
const logPath = join(homedir(), '.cache', 'bizar', 'logs', 'mod-audit.log');
|
|
96
|
+
const modFilter = req.query?.mod;
|
|
97
|
+
const limit = Math.min(parseInt(req.query?.limit || '200', 10) || 200, 2000);
|
|
98
|
+
if (!existsSync(logPath)) {
|
|
99
|
+
res.json({ entries: [], logPath, modFilter });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = readFileSync(logPath, 'utf8');
|
|
105
|
+
} catch (err) {
|
|
106
|
+
res.status(500).json({ error: 'read_failed', message: err.message });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const lines = text.split(/\r?\n/).filter(Boolean).reverse();
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (out.length >= limit) break;
|
|
113
|
+
try {
|
|
114
|
+
const entry = JSON.parse(line);
|
|
115
|
+
if (modFilter && entry.mod !== modFilter) continue;
|
|
116
|
+
out.push(entry);
|
|
117
|
+
} catch {
|
|
118
|
+
/* skip malformed lines */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
res.json({ entries: out.reverse(), logPath, modFilter });
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// /mods/views MUST come before /mods/:id — otherwise Express would
|
|
125
|
+
// match "views" as an :id value.
|
|
126
|
+
router.get('/mods/views', wrap(async (_req, res) => {
|
|
127
|
+
const views = modsLoader.listModViews();
|
|
128
|
+
res.json({ views });
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
router.get('/mods/:id', wrap(async (req, res) => {
|
|
132
|
+
const mod = modsLoader.get(req.params.id);
|
|
133
|
+
if (!mod) {
|
|
134
|
+
res.status(404).json({ error: 'not_found' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.json(mod);
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
router.post('/mods', wrap(async (req, res) => {
|
|
141
|
+
// Two install paths:
|
|
142
|
+
// { path: "/local/path" } — install from a local directory
|
|
143
|
+
// { id: "registry-id" } — install from the public registry
|
|
144
|
+
if (req.body?.id) {
|
|
145
|
+
try {
|
|
146
|
+
const mod = await modsLoader.installFromRegistry(req.body.id);
|
|
147
|
+
res.status(201).json(mod);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
res.status(500).json({ error: 'install_failed', message: err.message });
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const path = req.body?.path;
|
|
154
|
+
if (!path) {
|
|
155
|
+
res.status(400).json({
|
|
156
|
+
error: 'bad_request',
|
|
157
|
+
message: 'either "id" (registry install) or "path" (local install) is required',
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const mod = modsLoader.installFromPath(path);
|
|
163
|
+
res.status(201).json(mod);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
res.status(500).json({ error: 'install_failed', message: err.message });
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
router.put('/mods/:id', wrap(async (req, res) => {
|
|
170
|
+
const mod = modsLoader.setEnabled(req.params.id, !!req.body?.enabled);
|
|
171
|
+
if (!mod) {
|
|
172
|
+
res.status(404).json({ error: 'not_found' });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
res.json(mod);
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
router.delete('/mods/:id', wrap(async (req, res) => {
|
|
179
|
+
const ok = modsLoader.uninstall(req.params.id);
|
|
180
|
+
if (!ok) {
|
|
181
|
+
res.status(404).json({ error: 'not_found' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
res.status(204).end();
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
router.get('/mods/:id/files', wrap(async (req, res) => {
|
|
188
|
+
res.json({ files: modsLoader.listFiles(req.params.id) });
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
// NOTE: we use /mods/:id/mod-file/* (named route) to avoid conflicting
|
|
192
|
+
// with mod route mounting at /api/mods/:id/*. The wildcard in /* was
|
|
193
|
+
// too greedy and captured paths like /mods/test-mod/hello.
|
|
194
|
+
router.get('/mods/:id/mod-file/*', wrap(async (req, res) => {
|
|
195
|
+
const rel = req.params[0] || '';
|
|
196
|
+
const content = modsLoader.readFile(req.params.id, rel);
|
|
197
|
+
if (content === null) {
|
|
198
|
+
res.status(404).json({ error: 'not_found' });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
res.type('text/plain').send(content);
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
router.put('/mods/:id/mod-file/*', wrap(async (req, res) => {
|
|
205
|
+
const rel = req.params[0] || '';
|
|
206
|
+
const body = typeof req.body === 'string' ? req.body : JSON.stringify(req.body, null, 2);
|
|
207
|
+
modsLoader.writeFile(req.params.id, rel, body);
|
|
208
|
+
res.json({ ok: true });
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
// ── v3.20 — Mod instructions (agents/commands/skills installed by this mod)
|
|
212
|
+
router.get('/mods/:id/instructions', wrap(async (req, res) => {
|
|
213
|
+
const mod = modsLoader.get(req.params.id);
|
|
214
|
+
if (!mod) {
|
|
215
|
+
res.status(404).json({ error: 'not_found' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const installed = modsLoader.listModInstructions(req.params.id);
|
|
219
|
+
// For each installed file, try to read its content so the UI can
|
|
220
|
+
// preview it without an extra round-trip.
|
|
221
|
+
const annotated = {
|
|
222
|
+
agents: installed.agents.map((f) => ({
|
|
223
|
+
filename: f,
|
|
224
|
+
path: join('agents', f),
|
|
225
|
+
fullPath: `~/.config/opencode/agents/${f}`,
|
|
226
|
+
content: tryRead(`~/.config/opencode/agents/${f}`),
|
|
227
|
+
})),
|
|
228
|
+
commands: installed.commands.map((f) => ({
|
|
229
|
+
filename: f,
|
|
230
|
+
path: join('commands', f),
|
|
231
|
+
fullPath: `~/.config/opencode/commands/${f}`,
|
|
232
|
+
content: tryRead(`~/.config/opencode/commands/${f}`),
|
|
233
|
+
})),
|
|
234
|
+
skills: installed.skills.map((d) => ({
|
|
235
|
+
name: d,
|
|
236
|
+
fullPath: `~/.opencode/skills/${d}`,
|
|
237
|
+
content: tryRead(`~/.opencode/skills/${d}/SKILL.md`),
|
|
238
|
+
})),
|
|
239
|
+
};
|
|
240
|
+
res.json({
|
|
241
|
+
modId: req.params.id,
|
|
242
|
+
modName: mod.name,
|
|
243
|
+
total: annotated.agents.length + annotated.commands.length + annotated.skills.length,
|
|
244
|
+
...annotated,
|
|
245
|
+
});
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
// v3.20 — Force-reinstall the mod's instructions into opencode config
|
|
249
|
+
// (useful after editing files inside the mod's agents/commands/skills).
|
|
250
|
+
router.post('/mods/:id/instructions/reinstall', wrap(async (req, res) => {
|
|
251
|
+
const counts = modsLoader.reinstallInstructions(req.params.id);
|
|
252
|
+
if (counts === null) {
|
|
253
|
+
res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not installed` });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
res.json({ ok: true, counts });
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
// v3.20.5 — Upgrade an installed mod from the registry. Body opts:
|
|
260
|
+
// { backup?: boolean, url?: string }
|
|
261
|
+
// backup: if true, copy the existing mod folder into
|
|
262
|
+
// ~/.config/bizar/mods/.backup/<id>-<timestamp>/ before replacing.
|
|
263
|
+
// url: optional override for the registry URL.
|
|
264
|
+
// Returns { from, to, backupPath?, mod } on success.
|
|
265
|
+
router.post('/mods/:id/upgrade', wrap(async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const result = await modsLoader.upgradeFromRegistry(req.params.id, {
|
|
268
|
+
backup: !!req.body?.backup,
|
|
269
|
+
url: typeof req.body?.url === 'string' ? req.body.url : undefined,
|
|
270
|
+
});
|
|
271
|
+
res.json({
|
|
272
|
+
ok: true,
|
|
273
|
+
modId: req.params.id,
|
|
274
|
+
from: result.from,
|
|
275
|
+
to: result.to,
|
|
276
|
+
backupPath: result.backupPath,
|
|
277
|
+
mod: result.mod,
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const code = /not installed/i.test(err.message) ? 404 : 500;
|
|
281
|
+
res.status(code).json({ error: 'upgrade_failed', message: err.message });
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
|
|
285
|
+
// ── /api/mods/:id/mod-web/* and /api/mods/:id/web/* ────────────────
|
|
286
|
+
// Serve files from each mod's web/ directory (for iframe embedding).
|
|
287
|
+
// Two aliases — `mod-web` (explicit, won't collide with mod routes) and
|
|
288
|
+
// bare `web` (intuitive, what users actually type). They serve identical
|
|
289
|
+
// content so the mod author can use either.
|
|
290
|
+
const webHandler = wrap(async (req, res) => {
|
|
291
|
+
const mod = modsLoader.get(req.params.id);
|
|
292
|
+
if (!mod || !mod.enabled) {
|
|
293
|
+
res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const rel = req.params[0] || '';
|
|
297
|
+
const webRoot = resolve(mod.path, 'web');
|
|
298
|
+
const filePath = resolve(webRoot, rel);
|
|
299
|
+
const relPath = relative(webRoot, filePath);
|
|
300
|
+
if (relPath.startsWith('..') || relPath === '') {
|
|
301
|
+
res.status(403).json({ error: 'forbidden' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!existsSync(filePath)) {
|
|
305
|
+
res.status(404).json({ error: 'not_found', message: `file not found: ${rel}` });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
res.sendFile(filePath);
|
|
309
|
+
});
|
|
310
|
+
router.get('/mods/:id/mod-web/*', webHandler);
|
|
311
|
+
router.get('/mods/:id/web/*', webHandler);
|
|
312
|
+
|
|
313
|
+
// v3.20.5 — Serve a mod-shipped React view as an ES module. Mods
|
|
314
|
+
// declare view components in views/registry.json (e.g.
|
|
315
|
+
// { id: "graph", component: "GraphView.js" }
|
|
316
|
+
// ) and ship the built file at views/<component>. The dashboard
|
|
317
|
+
// dynamically imports it on demand and renders it inside ModView.
|
|
318
|
+
//
|
|
319
|
+
// The file is served with Content-Type: application/javascript so
|
|
320
|
+
// the browser treats it as a real ES module. Mods are expected to
|
|
321
|
+
// ship pre-built code (no JSX) and use React.createElement (or htm
|
|
322
|
+
// with React.createElement) for composition. React itself is loaded
|
|
323
|
+
// as part of the dashboard bundle — see /assets/*.js on first load.
|
|
324
|
+
const viewsHandler = wrap(async (req, res) => {
|
|
325
|
+
const mod = modsLoader.get(req.params.id);
|
|
326
|
+
if (!mod || !mod.enabled) {
|
|
327
|
+
res.status(404).json({ error: 'not_found', message: `mod "${req.params.id}" not found or disabled` });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const rel = req.params[0] || '';
|
|
331
|
+
if (!rel || rel.endsWith('/')) {
|
|
332
|
+
res.status(400).json({ error: 'bad_request', message: 'file path required' });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Allow only .js / .mjs / .jsx / .tsx files — anything else is
|
|
336
|
+
// a misconfiguration or attack attempt.
|
|
337
|
+
if (!/\.(js|mjs|jsx|tsx)$/i.test(rel)) {
|
|
338
|
+
res.status(403).json({ error: 'forbidden', message: 'only JS-family files may be served as views' });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const viewsRoot = resolve(mod.path, 'views');
|
|
342
|
+
const filePath = resolve(viewsRoot, rel);
|
|
343
|
+
const relPath = relative(viewsRoot, filePath);
|
|
344
|
+
if (relPath.startsWith('..') || relPath === '') {
|
|
345
|
+
res.status(403).json({ error: 'forbidden' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!existsSync(filePath)) {
|
|
349
|
+
res.status(404).json({ error: 'not_found', message: `view not found: ${rel}` });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
res.set('Content-Type', 'application/javascript; charset=utf-8');
|
|
353
|
+
res.set('Cache-Control', 'no-store');
|
|
354
|
+
res.sendFile(filePath);
|
|
355
|
+
});
|
|
356
|
+
router.get('/mods/:id/views/*', viewsHandler);
|
|
357
|
+
|
|
358
|
+
return router;
|
|
359
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/notifications.mjs
|
|
3
|
+
*
|
|
4
|
+
* /api/notifications — list (?unread=true to filter)
|
|
5
|
+
* /api/notifications/:id/read (POST) — mark one as read
|
|
6
|
+
* /api/notifications/read-all (POST) — mark every as read
|
|
7
|
+
* /api/notifications/:id (DELETE) — dismiss
|
|
8
|
+
*/
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import { notificationsStore } from '../notifications-store.mjs';
|
|
11
|
+
import { wrap } from './_shared.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} deps
|
|
15
|
+
* @param {Function} deps.broadcast
|
|
16
|
+
* @returns {import('express').Router}
|
|
17
|
+
*/
|
|
18
|
+
export function createNotificationsRouter({ broadcast }) {
|
|
19
|
+
const router = Router();
|
|
20
|
+
|
|
21
|
+
// v3.3.0 — Per-user notification stream. Backed by an append-only
|
|
22
|
+
// JSONL log at ~/.config/bizar/notifications.jsonl. Read state is
|
|
23
|
+
// persisted separately at notifications.read.json.
|
|
24
|
+
router.get('/notifications', wrap(async (req, res) => {
|
|
25
|
+
const unread = req.query.unread === 'true' || req.query.unread === '1';
|
|
26
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query.limit || '200', 10) || 200));
|
|
27
|
+
const items = notificationsStore.list({ unread, limit });
|
|
28
|
+
res.json({ notifications: items, stats: notificationsStore.stats() });
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
router.post('/notifications/:id/read', wrap(async (req, res) => {
|
|
32
|
+
const ok = notificationsStore.markRead(req.params.id);
|
|
33
|
+
if (ok) broadcast({ type: 'notifications:change' });
|
|
34
|
+
res.json({ ok });
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
router.post('/notifications/read-all', wrap(async (_req, res) => {
|
|
38
|
+
const marked = notificationsStore.markAllRead();
|
|
39
|
+
broadcast({ type: 'notifications:change' });
|
|
40
|
+
res.json({ ok: true, marked });
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
router.delete('/notifications/:id', wrap(async (req, res) => {
|
|
44
|
+
const ok = notificationsStore.remove(req.params.id);
|
|
45
|
+
if (!ok) {
|
|
46
|
+
res.status(404).json({ error: 'not_found' });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
broadcast({ type: 'notifications:change' });
|
|
50
|
+
res.status(204).end();
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return router;
|
|
54
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/routes/obsidian.mjs
|
|
3
|
+
*
|
|
4
|
+
* v2 — Backward-compatible rewrite. When `.bizar/memory.json` exists, this
|
|
5
|
+
* router delegates to memory-store.mjs. When memory is not configured, it falls
|
|
6
|
+
* back to the original obsidian-store.mjs behaviour.
|
|
7
|
+
*
|
|
8
|
+
* CRITICAL: Preserve legacy response shapes exactly. Do not change the JSON
|
|
9
|
+
* structure returned by any endpoint.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Router } from 'express';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const SERVER_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
|
|
18
|
+
// Lazy imports so circular deps are avoided and we only load memory-store.mjs
|
|
19
|
+
// when it is actually needed.
|
|
20
|
+
let _memoryStore = null;
|
|
21
|
+
let _obsidianStore = null;
|
|
22
|
+
let _wrap = null;
|
|
23
|
+
|
|
24
|
+
async function getMemoryStore() {
|
|
25
|
+
if (!_memoryStore) {
|
|
26
|
+
_memoryStore = await import(`${SERVER_ROOT}/memory-store.mjs`).then((m) => m);
|
|
27
|
+
}
|
|
28
|
+
return _memoryStore;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function getObsidianStore() {
|
|
32
|
+
if (!_obsidianStore) {
|
|
33
|
+
_obsidianStore = await import(`${SERVER_ROOT}/obsidian-store.mjs`).then((m) => m);
|
|
34
|
+
}
|
|
35
|
+
return _obsidianStore;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getShared() {
|
|
39
|
+
if (!_wrap) {
|
|
40
|
+
const mod = await import('./_shared.mjs').then((m) => m);
|
|
41
|
+
_wrap = mod.wrap;
|
|
42
|
+
}
|
|
43
|
+
return { wrap: _wrap };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns true if the project has opted into the memory service via
|
|
48
|
+
* `.bizar/memory.json`.
|
|
49
|
+
*/
|
|
50
|
+
async function hasMemoryConfig(projectRoot) {
|
|
51
|
+
const { safeReadJSON } = await import(`${SERVER_ROOT}/yaml.mjs`).then((m) => m);
|
|
52
|
+
// Use safeReadJSON from yaml.mjs is expensive — use simpler check
|
|
53
|
+
const { existsSync } = await import('node:fs');
|
|
54
|
+
const configPath = join(projectRoot, '.bizar', 'memory.json');
|
|
55
|
+
return existsSync(configPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function createObsidianRouter({ projectRoot }) {
|
|
59
|
+
const router = Router();
|
|
60
|
+
const { wrap } = await getShared();
|
|
61
|
+
const memoryEnabled = await hasMemoryConfig(projectRoot);
|
|
62
|
+
|
|
63
|
+
if (memoryEnabled) {
|
|
64
|
+
// ── Memory service is active — delegate to memory-store.mjs ──────────────
|
|
65
|
+
const ms = await getMemoryStore();
|
|
66
|
+
|
|
67
|
+
// GET /api/obsidian — vault stats
|
|
68
|
+
router.get('/obsidian', wrap(async (_req, res) => {
|
|
69
|
+
const { vaultRoot, mode } = ms.resolveVault(projectRoot);
|
|
70
|
+
const { existsSync, readdirSync, statSync, readFileSync } = await import('node:fs');
|
|
71
|
+
if (!existsSync(vaultRoot)) {
|
|
72
|
+
res.json({ exists: false, vaultDir: vaultRoot, noteCount: 0, totalSize: 0 });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const notes = ms.listNotes(projectRoot);
|
|
76
|
+
const totalSize = notes.reduce((acc, n) => acc + n.size, 0);
|
|
77
|
+
const folders = new Set(notes.map((n) => n.relPath.split('/')[0]));
|
|
78
|
+
res.json({
|
|
79
|
+
exists: true,
|
|
80
|
+
vaultDir: vaultRoot,
|
|
81
|
+
noteCount: notes.length,
|
|
82
|
+
totalSize,
|
|
83
|
+
folderCount: folders.size,
|
|
84
|
+
folders: [...folders].sort(),
|
|
85
|
+
lastModified: notes[0]?.mtime || null,
|
|
86
|
+
});
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// POST /api/obsidian — init vault
|
|
90
|
+
router.post('/obsidian', wrap(async (_req, res) => {
|
|
91
|
+
const result = ms.initVault(projectRoot);
|
|
92
|
+
res.json({ vaultDir: result.vaultRoot, created: result.created });
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
// GET /api/obsidian/notes — legacy shape: { notes: [{ path, relPath, mtime, size }] }
|
|
96
|
+
router.get('/obsidian/notes', wrap(async (_req, res) => {
|
|
97
|
+
const notes = ms.listNotes(projectRoot);
|
|
98
|
+
// Return legacy shape
|
|
99
|
+
const legacyNotes = notes.map((n) => ({
|
|
100
|
+
path: n.relPath,
|
|
101
|
+
relPath: n.relPath,
|
|
102
|
+
mtime: n.mtime,
|
|
103
|
+
size: n.size,
|
|
104
|
+
}));
|
|
105
|
+
res.json({ notes: legacyNotes });
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
// POST /api/obsidian/notes — write note
|
|
109
|
+
// CRITICAL: returns 201 with FULL note shape { relPath, frontmatter, body, raw, mtime, size }
|
|
110
|
+
router.post('/obsidian/notes', wrap(async (req, res) => {
|
|
111
|
+
const { path: relPath, frontmatter, body } = req.body || {};
|
|
112
|
+
if (!relPath || typeof relPath !== 'string') {
|
|
113
|
+
res.status(400).json({ error: 'bad_request', message: 'path is required' });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!relPath.endsWith('.md')) {
|
|
117
|
+
res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const note = ms.writeNote(projectRoot, relPath, { frontmatter: frontmatter || {}, body: body || '' });
|
|
122
|
+
// Return FULL note shape (legacy behavior)
|
|
123
|
+
res.status(201).json(note);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
res.status(400).json({ error: 'bad_request', message: err.message });
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
// GET /api/obsidian/notes/:path — single note
|
|
130
|
+
router.get('/obsidian/notes/*', wrap(async (req, res) => {
|
|
131
|
+
const relPath = req.params[0];
|
|
132
|
+
const note = ms.readNote(projectRoot, relPath);
|
|
133
|
+
if (!note) {
|
|
134
|
+
res.status(404).json({ error: 'not_found' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.json(note);
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// DELETE /api/obsidian/notes/:path
|
|
141
|
+
router.delete('/obsidian/notes/*', wrap(async (req, res) => {
|
|
142
|
+
const relPath = req.params[0];
|
|
143
|
+
const ok = ms.deleteNote(projectRoot, relPath);
|
|
144
|
+
if (!ok) {
|
|
145
|
+
res.status(404).json({ error: 'not_found' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
res.status(204).end();
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
// GET /api/obsidian/search?q=...
|
|
152
|
+
router.get('/obsidian/search', wrap(async (req, res) => {
|
|
153
|
+
const q = req.query.q || '';
|
|
154
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
|
|
155
|
+
const results = ms.searchVault(projectRoot, q, { limit });
|
|
156
|
+
res.json({ query: q, results });
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// POST /api/obsidian/index — rebuild index
|
|
160
|
+
router.post('/obsidian/index', wrap(async (_req, res) => {
|
|
161
|
+
// No-op in memory mode (INDEX.md is not maintained)
|
|
162
|
+
res.json({ ok: true, indexPath: null });
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
} else {
|
|
166
|
+
// ── Legacy obsidian-store.mjs behaviour ───────────────────────────────────
|
|
167
|
+
const os = await getObsidianStore();
|
|
168
|
+
|
|
169
|
+
router.get('/obsidian', wrap(async (_req, res) => {
|
|
170
|
+
res.json(os.vaultStats(projectRoot));
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
router.post('/obsidian', wrap(async (_req, res) => {
|
|
174
|
+
const result = os.initObsidianVault(projectRoot);
|
|
175
|
+
res.json(result);
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
router.get('/obsidian/notes', wrap(async (_req, res) => {
|
|
179
|
+
const notes = os.listVaultNotes(projectRoot);
|
|
180
|
+
res.json({ notes });
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
router.post('/obsidian/notes', wrap(async (req, res) => {
|
|
184
|
+
const { path: relPath, frontmatter, body } = req.body || {};
|
|
185
|
+
if (!relPath || typeof relPath !== 'string') {
|
|
186
|
+
res.status(400).json({ error: 'bad_request', message: 'path is required' });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!relPath.endsWith('.md')) {
|
|
190
|
+
res.status(400).json({ error: 'bad_request', message: 'path must end in .md' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const note = os.writeVaultNote(projectRoot, relPath, { frontmatter, body });
|
|
195
|
+
res.status(201).json(note);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
res.status(400).json({ error: 'bad_request', message: err.message });
|
|
198
|
+
}
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
router.get('/obsidian/notes/*', wrap(async (req, res) => {
|
|
202
|
+
const relPath = req.params[0];
|
|
203
|
+
const note = os.readVaultNote(projectRoot, relPath);
|
|
204
|
+
if (!note) {
|
|
205
|
+
res.status(404).json({ error: 'not_found' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
res.json(note);
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
router.delete('/obsidian/notes/*', wrap(async (req, res) => {
|
|
212
|
+
const relPath = req.params[0];
|
|
213
|
+
const ok = os.deleteVaultNote(projectRoot, relPath);
|
|
214
|
+
if (!ok) {
|
|
215
|
+
res.status(404).json({ error: 'not_found' });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
res.status(204).end();
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
router.get('/obsidian/search', wrap(async (req, res) => {
|
|
222
|
+
const q = req.query.q;
|
|
223
|
+
const limit = Math.min(parseInt(req.query.limit, 10) || 25, 100);
|
|
224
|
+
const results = os.searchVault(projectRoot, q, { limit });
|
|
225
|
+
res.json({ query: q, results });
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
router.post('/obsidian/index', wrap(async (_req, res) => {
|
|
229
|
+
const indexPath = os.rebuildIndex(projectRoot);
|
|
230
|
+
if (!indexPath) {
|
|
231
|
+
res.status(404).json({ error: 'not_initialized' });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
res.json({ ok: true, indexPath });
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return router;
|
|
239
|
+
}
|