@vellumai/assistant 0.8.8 → 0.8.9-staging.2
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/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache for the installable plugin catalog.
|
|
3
|
+
*
|
|
4
|
+
* The catalog (`loadPluginCatalog`) is built from two unauthenticated GitHub
|
|
5
|
+
* Contents API calls — the first-party `experimental/plugins/` directory
|
|
6
|
+
* listing and the `marketplace.json` manifest. GitHub's unauthenticated rate
|
|
7
|
+
* limit is 60 requests/hour per IP, so a long-lived daemon that hit GitHub on
|
|
8
|
+
* every catalog search (on web mount, on every keystroke, across reloads and
|
|
9
|
+
* multiple clients) would exhaust that budget and start getting HTTP 403s.
|
|
10
|
+
*
|
|
11
|
+
* Because the GitHub responses are **query-independent** — `searchPlugins`
|
|
12
|
+
* applies the regex filter in memory — one catalog load serves any number of
|
|
13
|
+
* searches. This cache holds that load per git ref for a short TTL and, on an
|
|
14
|
+
* upstream failure, serves the last good catalog so a transient rate-limit or
|
|
15
|
+
* outage is invisible to callers. Mirrors the skills catalog cache
|
|
16
|
+
* (`src/skills/catalog-cache.ts`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { getLogger } from "../../util/logger.js";
|
|
20
|
+
import {
|
|
21
|
+
loadPluginCatalog,
|
|
22
|
+
type PluginCatalog,
|
|
23
|
+
PluginCatalogUnavailableError,
|
|
24
|
+
type SearchPluginsDeps,
|
|
25
|
+
} from "./search-plugins.js";
|
|
26
|
+
|
|
27
|
+
const log = getLogger("plugin-catalog-cache");
|
|
28
|
+
|
|
29
|
+
/** How long a fetched catalog is served before a refresh is attempted. */
|
|
30
|
+
export const PLUGIN_CATALOG_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
31
|
+
|
|
32
|
+
interface CacheEntry {
|
|
33
|
+
catalog: PluginCatalog;
|
|
34
|
+
timestamp: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cache = new Map<string, CacheEntry>();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the full plugin catalog at {@link ref} with in-memory caching.
|
|
41
|
+
*
|
|
42
|
+
* Within the TTL window the cached catalog is returned without touching the
|
|
43
|
+
* network. After the TTL elapses a refresh is attempted; if it fails with a
|
|
44
|
+
* transient upstream error ({@link PluginCatalogUnavailableError} — rate
|
|
45
|
+
* limiting or a 5xx) and a cached catalog exists, the stale catalog is served
|
|
46
|
+
* (and its TTL window is reset so a sustained outage doesn't re-enter the
|
|
47
|
+
* failing fetch on every call). A hard error (e.g. the canonical plugins
|
|
48
|
+
* prefix returning 404, or a deleted ref) always propagates — serving stale
|
|
49
|
+
* there would silently mask a real misconfiguration. With no cache to fall
|
|
50
|
+
* back on, the underlying error propagates regardless so the caller can
|
|
51
|
+
* surface it (e.g. the route maps a rate-limit failure to 503).
|
|
52
|
+
*/
|
|
53
|
+
export async function getPluginCatalog(
|
|
54
|
+
ref: string,
|
|
55
|
+
deps: SearchPluginsDeps,
|
|
56
|
+
): Promise<PluginCatalog> {
|
|
57
|
+
const cached = cache.get(ref);
|
|
58
|
+
if (cached && Date.now() - cached.timestamp < PLUGIN_CATALOG_CACHE_TTL_MS) {
|
|
59
|
+
return cached.catalog;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const catalog = await loadPluginCatalog({ ref }, deps);
|
|
64
|
+
cache.set(ref, { catalog, timestamp: Date.now() });
|
|
65
|
+
return catalog;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (cached && err instanceof PluginCatalogUnavailableError) {
|
|
68
|
+
log.warn(
|
|
69
|
+
{ err, ref },
|
|
70
|
+
"Failed to refresh plugin catalog, serving stale cache",
|
|
71
|
+
);
|
|
72
|
+
// Reset the TTL window so subsequent calls during the outage are served
|
|
73
|
+
// from cache instead of re-entering loadPluginCatalog() every time.
|
|
74
|
+
cached.timestamp = Date.now();
|
|
75
|
+
return cached.catalog;
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Invalidate the cache (for testing or forced refresh). */
|
|
82
|
+
export function invalidatePluginCatalogCache(): void {
|
|
83
|
+
cache.clear();
|
|
84
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a single plugin's detail view: tracked metadata plus its README.
|
|
3
|
+
*
|
|
4
|
+
* Powers the daemon's `GET /v1/plugins/:name` route and, through it, the web
|
|
5
|
+
* plugin detail page. The detail view unions three sources, preferring the
|
|
6
|
+
* most authoritative for each field:
|
|
7
|
+
* 1. The locally installed copy under `<workspacePluginsDir>/<name>/`, when
|
|
8
|
+
* present — its `package.json` and `README.md` are read straight off disk.
|
|
9
|
+
* 2. The curated `experimental/plugins/marketplace.json` entry, for external
|
|
10
|
+
* ecosystem plugins (description / homepage / license / pinned source).
|
|
11
|
+
* 3. The plugin's own repository at the pinned ref (first-party
|
|
12
|
+
* `experimental/plugins/<name>/` or the external `owner/repo[/path]`),
|
|
13
|
+
* fetched via the GitHub Contents API for the README and any
|
|
14
|
+
* `package.json` fields the manifest doesn't carry.
|
|
15
|
+
*
|
|
16
|
+
* Name-collision precedence matches {@link ./search-plugins} and
|
|
17
|
+
* {@link ./install-from-github}: a first-party in-repo plugin wins a name also
|
|
18
|
+
* claimed by the marketplace, so the detail page advertises the same source the
|
|
19
|
+
* catalog and installer would use.
|
|
20
|
+
*
|
|
21
|
+
* Designed for direct programmatic use with an injected `fetch`, mirroring the
|
|
22
|
+
* sibling plugin libraries.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_PLUGIN_REF,
|
|
31
|
+
type FetchLike,
|
|
32
|
+
sanitizePluginName,
|
|
33
|
+
} from "./install-from-github.js";
|
|
34
|
+
import {
|
|
35
|
+
fetchMarketplaceEntries,
|
|
36
|
+
type MarketplaceEntry,
|
|
37
|
+
} from "./plugin-marketplace.js";
|
|
38
|
+
import type { PluginMatchSource } from "./search-plugins.js";
|
|
39
|
+
|
|
40
|
+
const PLUGIN_SOURCE_OWNER = "vellum-ai";
|
|
41
|
+
const PLUGIN_SOURCE_REPO = "vellum-assistant";
|
|
42
|
+
const PLUGIN_SOURCE_PATH_PREFIX = "experimental/plugins";
|
|
43
|
+
|
|
44
|
+
/** Recognised README filenames, matched case-insensitively against a listing. */
|
|
45
|
+
const README_RE = /^readme(\.md|\.markdown)?$/i;
|
|
46
|
+
|
|
47
|
+
/** Entry shape returned by the GitHub Contents API for a directory listing. */
|
|
48
|
+
interface GitHubContentEntry {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly type: "file" | "dir" | "symlink" | "submodule";
|
|
52
|
+
readonly download_url: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The subset of `package.json` fields the detail view surfaces. */
|
|
56
|
+
interface PluginManifestFields {
|
|
57
|
+
readonly version: string | null;
|
|
58
|
+
readonly description: string | null;
|
|
59
|
+
readonly homepage: string | null;
|
|
60
|
+
readonly license: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Options that control which plugin to resolve and at what ref. */
|
|
64
|
+
export interface PluginDetailsOptions {
|
|
65
|
+
/** Install name (kebab-case directory name). */
|
|
66
|
+
readonly name: string;
|
|
67
|
+
/** Git ref to read catalog metadata / README from. Defaults to {@link DEFAULT_PLUGIN_REF}. */
|
|
68
|
+
readonly ref?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Dependencies injected by the caller. */
|
|
72
|
+
export interface PluginDetailsDeps {
|
|
73
|
+
/** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
|
|
74
|
+
readonly fetch: FetchLike;
|
|
75
|
+
/** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
|
|
76
|
+
readonly workspacePluginsDir?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolved detail view for a single plugin. */
|
|
80
|
+
export interface PluginDetails {
|
|
81
|
+
/** Install name. Matches `assistant plugins install <name>`. */
|
|
82
|
+
readonly name: string;
|
|
83
|
+
/** Whether a copy is materialized under the workspace plugins directory. */
|
|
84
|
+
readonly installed: boolean;
|
|
85
|
+
/** Short description, best-effort across the three sources; `null` when unknown. */
|
|
86
|
+
readonly description: string | null;
|
|
87
|
+
/** Project homepage URL, when known; `null` otherwise. */
|
|
88
|
+
readonly homepage: string | null;
|
|
89
|
+
/** SPDX license expression, when known; `null` otherwise. */
|
|
90
|
+
readonly license: string | null;
|
|
91
|
+
/** Resolved version (installed copy first, then repo `package.json`); `null` when unknown. */
|
|
92
|
+
readonly version: string | null;
|
|
93
|
+
/** Discriminated origin, mirroring the catalog's {@link PluginMatchSource}. */
|
|
94
|
+
readonly source: PluginMatchSource;
|
|
95
|
+
/** README markdown, or `null` when the plugin ships none. */
|
|
96
|
+
readonly readme: string | null;
|
|
97
|
+
/** Git ref the catalog metadata / README were resolved at. */
|
|
98
|
+
readonly ref: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** No installed copy and no catalog/source entry claims the name. */
|
|
102
|
+
export class PluginDetailsNotFoundError extends Error {
|
|
103
|
+
constructor(
|
|
104
|
+
readonly pluginName: string,
|
|
105
|
+
readonly ref: string,
|
|
106
|
+
) {
|
|
107
|
+
super(`Plugin "${pluginName}" not found in the catalog (ref ${ref}).`);
|
|
108
|
+
this.name = "PluginDetailsNotFoundError";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the detail view for {@link opts.name}.
|
|
114
|
+
*
|
|
115
|
+
* Throws {@link PluginDetailsNotFoundError} when the name is neither installed
|
|
116
|
+
* locally nor present in the catalog (first-party directory or marketplace
|
|
117
|
+
* entry). Network failures while enriching from GitHub degrade to the fields
|
|
118
|
+
* already known from disk / the manifest rather than failing the whole view —
|
|
119
|
+
* a detail page that renders metadata without a README beats a hard error.
|
|
120
|
+
*/
|
|
121
|
+
export async function getPluginDetails(
|
|
122
|
+
opts: PluginDetailsOptions,
|
|
123
|
+
deps: PluginDetailsDeps,
|
|
124
|
+
): Promise<PluginDetails> {
|
|
125
|
+
const name = sanitizePluginName(opts.name);
|
|
126
|
+
const ref = opts.ref ?? DEFAULT_PLUGIN_REF;
|
|
127
|
+
const { fetch: fetchFn } = deps;
|
|
128
|
+
|
|
129
|
+
const pluginsDir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
|
|
130
|
+
const local = readLocalPlugin(pluginsDir, name);
|
|
131
|
+
|
|
132
|
+
const marketplaceEntry = await findMarketplaceEntry(name, ref, fetchFn);
|
|
133
|
+
const firstPartyEntries = await listDirSafe(
|
|
134
|
+
PLUGIN_SOURCE_OWNER,
|
|
135
|
+
PLUGIN_SOURCE_REPO,
|
|
136
|
+
`${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
|
|
137
|
+
ref,
|
|
138
|
+
fetchFn,
|
|
139
|
+
);
|
|
140
|
+
const firstPartyExists = firstPartyEntries !== null;
|
|
141
|
+
|
|
142
|
+
if (!local.installed && !firstPartyExists && !marketplaceEntry) {
|
|
143
|
+
throw new PluginDetailsNotFoundError(name, ref);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// First-party wins a name collision, so probe the in-repo directory before
|
|
147
|
+
// honouring a marketplace claim — the same precedence the catalog applies.
|
|
148
|
+
const useExternal = !firstPartyExists && marketplaceEntry !== null;
|
|
149
|
+
|
|
150
|
+
const source: PluginMatchSource = useExternal
|
|
151
|
+
? {
|
|
152
|
+
kind: "github",
|
|
153
|
+
repo: marketplaceEntry!.source.repo,
|
|
154
|
+
ref: marketplaceEntry!.source.ref,
|
|
155
|
+
...(marketplaceEntry!.source.path
|
|
156
|
+
? { path: marketplaceEntry!.source.path }
|
|
157
|
+
: {}),
|
|
158
|
+
}
|
|
159
|
+
: { kind: "first-party" };
|
|
160
|
+
|
|
161
|
+
const remote = await readRemotePlugin(source, firstPartyEntries, fetchFn);
|
|
162
|
+
|
|
163
|
+
const readme = local.readme ?? remote.readme;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
name,
|
|
167
|
+
installed: local.installed,
|
|
168
|
+
description:
|
|
169
|
+
local.manifest.description ??
|
|
170
|
+
marketplaceEntry?.description ??
|
|
171
|
+
remote.manifest.description ??
|
|
172
|
+
null,
|
|
173
|
+
homepage:
|
|
174
|
+
local.manifest.homepage ??
|
|
175
|
+
marketplaceEntry?.homepage ??
|
|
176
|
+
remote.manifest.homepage ??
|
|
177
|
+
null,
|
|
178
|
+
license:
|
|
179
|
+
local.manifest.license ??
|
|
180
|
+
marketplaceEntry?.license ??
|
|
181
|
+
remote.manifest.license ??
|
|
182
|
+
null,
|
|
183
|
+
version: local.manifest.version ?? remote.manifest.version ?? null,
|
|
184
|
+
source,
|
|
185
|
+
readme,
|
|
186
|
+
ref,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface LocalPlugin {
|
|
191
|
+
readonly installed: boolean;
|
|
192
|
+
readonly manifest: PluginManifestFields;
|
|
193
|
+
readonly readme: string | null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Read an installed copy's `package.json` + README off disk, if present. */
|
|
197
|
+
function readLocalPlugin(pluginsDir: string, name: string): LocalPlugin {
|
|
198
|
+
const target = join(pluginsDir, name);
|
|
199
|
+
if (!existsSync(target)) {
|
|
200
|
+
return { installed: false, manifest: emptyManifest(), readme: null };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pkgPath = join(target, "package.json");
|
|
204
|
+
let manifest = emptyManifest();
|
|
205
|
+
if (existsSync(pkgPath)) {
|
|
206
|
+
try {
|
|
207
|
+
manifest = parseManifest(readFileSync(pkgPath, "utf8"));
|
|
208
|
+
} catch {
|
|
209
|
+
// A malformed local manifest degrades to empty fields — the entry is
|
|
210
|
+
// still "installed", we just have nothing extra to surface from it.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { installed: true, manifest, readme: readLocalReadme(target) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Find a README file in a directory listing and read it, if any. */
|
|
218
|
+
function readLocalReadme(dir: string): string | null {
|
|
219
|
+
let names: string[];
|
|
220
|
+
try {
|
|
221
|
+
names = readdirSync(dir);
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const readme = names.find((n) => README_RE.test(n));
|
|
226
|
+
if (!readme) return null;
|
|
227
|
+
try {
|
|
228
|
+
return readFileSync(join(dir, readme), "utf8");
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface RemotePlugin {
|
|
235
|
+
readonly manifest: PluginManifestFields;
|
|
236
|
+
readonly readme: string | null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Fetch README + `package.json` from the plugin's repository.
|
|
241
|
+
*
|
|
242
|
+
* For a first-party plugin the listing is the in-repo directory the caller
|
|
243
|
+
* already fetched at the catalog ref (passed through to avoid a duplicate
|
|
244
|
+
* request). For an external plugin we list its `owner/repo[/path]` directory
|
|
245
|
+
* fresh at the plugin's own pinned `source.ref`.
|
|
246
|
+
*/
|
|
247
|
+
async function readRemotePlugin(
|
|
248
|
+
source: PluginMatchSource,
|
|
249
|
+
firstPartyEntries: readonly GitHubContentEntry[] | null,
|
|
250
|
+
fetchFn: FetchLike,
|
|
251
|
+
): Promise<RemotePlugin> {
|
|
252
|
+
let owner: string;
|
|
253
|
+
let repo: string;
|
|
254
|
+
let entries: readonly GitHubContentEntry[] | null;
|
|
255
|
+
|
|
256
|
+
if (source.kind === "github") {
|
|
257
|
+
[owner, repo] = source.repo.split("/", 2) as [string, string];
|
|
258
|
+
// Read the external repo at its pinned ref, not the assistant catalog ref —
|
|
259
|
+
// the detail page must describe the same artifact the installer resolves
|
|
260
|
+
// from `source.ref`, even when that differs from the catalog ref (`main`).
|
|
261
|
+
entries = await listDirSafe(
|
|
262
|
+
owner,
|
|
263
|
+
repo,
|
|
264
|
+
source.path ?? "",
|
|
265
|
+
source.ref,
|
|
266
|
+
fetchFn,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
owner = PLUGIN_SOURCE_OWNER;
|
|
270
|
+
repo = PLUGIN_SOURCE_REPO;
|
|
271
|
+
entries = firstPartyEntries;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!entries) {
|
|
275
|
+
return { manifest: emptyManifest(), readme: null };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const readmeEntry = entries.find(
|
|
279
|
+
(e) => e.type === "file" && README_RE.test(e.name),
|
|
280
|
+
);
|
|
281
|
+
const pkgEntry = entries.find(
|
|
282
|
+
(e) => e.type === "file" && e.name === "package.json",
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const [readme, manifest] = await Promise.all([
|
|
286
|
+
readmeEntry ? fetchRawFile(readmeEntry, fetchFn) : Promise.resolve(null),
|
|
287
|
+
pkgEntry
|
|
288
|
+
? fetchRawFile(pkgEntry, fetchFn).then((raw) =>
|
|
289
|
+
raw ? safeParseManifest(raw) : emptyManifest(),
|
|
290
|
+
)
|
|
291
|
+
: Promise.resolve(emptyManifest()),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
return { manifest, readme };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** List a repo directory, returning `null` on 404 or any transient failure. */
|
|
298
|
+
async function listDirSafe(
|
|
299
|
+
owner: string,
|
|
300
|
+
repo: string,
|
|
301
|
+
apiPath: string,
|
|
302
|
+
ref: string,
|
|
303
|
+
fetchFn: FetchLike,
|
|
304
|
+
): Promise<readonly GitHubContentEntry[] | null> {
|
|
305
|
+
const suffix = apiPath
|
|
306
|
+
? `/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}`
|
|
307
|
+
: "";
|
|
308
|
+
const url =
|
|
309
|
+
`https://api.github.com/repos/${owner}/${repo}/contents${suffix}` +
|
|
310
|
+
`?ref=${encodeURIComponent(ref)}`;
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const res = await githubFetch(url, "application/vnd.github+json", fetchFn);
|
|
314
|
+
if (!res.ok) return null;
|
|
315
|
+
const body = (await res.json()) as unknown;
|
|
316
|
+
if (!Array.isArray(body)) return null;
|
|
317
|
+
return body as readonly GitHubContentEntry[];
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Download a file entry's raw body, returning `null` on any failure. */
|
|
324
|
+
async function fetchRawFile(
|
|
325
|
+
entry: GitHubContentEntry,
|
|
326
|
+
fetchFn: FetchLike,
|
|
327
|
+
): Promise<string | null> {
|
|
328
|
+
if (!entry.download_url) return null;
|
|
329
|
+
try {
|
|
330
|
+
const res = await githubFetch(
|
|
331
|
+
entry.download_url,
|
|
332
|
+
"application/vnd.github.raw",
|
|
333
|
+
fetchFn,
|
|
334
|
+
);
|
|
335
|
+
if (!res.ok) return null;
|
|
336
|
+
return await res.text();
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function findMarketplaceEntry(
|
|
343
|
+
name: string,
|
|
344
|
+
ref: string,
|
|
345
|
+
fetchFn: FetchLike,
|
|
346
|
+
): Promise<MarketplaceEntry | null> {
|
|
347
|
+
try {
|
|
348
|
+
const entries = await fetchMarketplaceEntries({ fetch: fetchFn }, { ref });
|
|
349
|
+
return entries.find((e) => e.name === name) ?? null;
|
|
350
|
+
} catch {
|
|
351
|
+
// A missing or malformed manifest degrades to "no external entry" — the
|
|
352
|
+
// marketplace is supplementary, never required to render a detail view.
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function githubFetch(
|
|
358
|
+
url: string,
|
|
359
|
+
accept: string,
|
|
360
|
+
fetchFn: FetchLike,
|
|
361
|
+
): Promise<Response> {
|
|
362
|
+
return fetchFn(url, {
|
|
363
|
+
headers: { Accept: accept, "User-Agent": "vellum-assistant-cli" },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function emptyManifest(): PluginManifestFields {
|
|
368
|
+
return { version: null, description: null, homepage: null, license: null };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function safeParseManifest(raw: string): PluginManifestFields {
|
|
372
|
+
try {
|
|
373
|
+
return parseManifest(raw);
|
|
374
|
+
} catch {
|
|
375
|
+
return emptyManifest();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Extract the surfaced fields from a `package.json` body. Throws on bad JSON. */
|
|
380
|
+
function parseManifest(raw: string): PluginManifestFields {
|
|
381
|
+
const parsed: unknown = JSON.parse(raw);
|
|
382
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
383
|
+
return emptyManifest();
|
|
384
|
+
}
|
|
385
|
+
const meta = parsed as Record<string, unknown>;
|
|
386
|
+
return {
|
|
387
|
+
version: typeof meta.version === "string" ? meta.version : null,
|
|
388
|
+
description: typeof meta.description === "string" ? meta.description : null,
|
|
389
|
+
homepage: typeof meta.homepage === "string" ? meta.homepage : null,
|
|
390
|
+
license: normalizeLicense(meta.license),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* `package.json#license` is usually an SPDX string but the legacy object form
|
|
396
|
+
* `{ "type": "MIT" }` still appears in the wild — surface its `type`.
|
|
397
|
+
*/
|
|
398
|
+
function normalizeLicense(value: unknown): string | null {
|
|
399
|
+
if (typeof value === "string") return value;
|
|
400
|
+
if (
|
|
401
|
+
typeof value === "object" &&
|
|
402
|
+
value !== null &&
|
|
403
|
+
"type" in value &&
|
|
404
|
+
typeof (value as { type: unknown }).type === "string"
|
|
405
|
+
) {
|
|
406
|
+
return (value as { type: string }).type;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the curated plugin marketplace manifest from the canonical repo.
|
|
3
|
+
*
|
|
4
|
+
* The manifest at `experimental/plugins/marketplace.json` whitelists external
|
|
5
|
+
* ecosystem plugins so they appear in `assistant plugins search` / the web
|
|
6
|
+
* catalog and become installable by name. Its shape is a subset of the
|
|
7
|
+
* Claude Code marketplace manifest
|
|
8
|
+
* (https://code.claude.com/docs/en/plugin-marketplaces) — `name` + `owner` +
|
|
9
|
+
* a `plugins` array where each entry carries a `name` and a `source`. Only
|
|
10
|
+
* `github` sources are resolved today.
|
|
11
|
+
*
|
|
12
|
+
* Like the first-party plugin listing, the manifest is fetched from the repo
|
|
13
|
+
* at a git ref (via the GitHub Contents API) rather than bundled into the
|
|
14
|
+
* assistant build — so the whitelist can grow without shipping a new release.
|
|
15
|
+
* Every external source pins an explicit `ref` (tag/SHA/branch): the catalog
|
|
16
|
+
* is curated and the fetched code is version-locked, not a floating branch.
|
|
17
|
+
*
|
|
18
|
+
* Designed for direct programmatic use with an injected `fetch`, mirroring
|
|
19
|
+
* {@link ./search-plugins} and {@link ./install-from-github}.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
|
|
24
|
+
// Type-only import: keeps the dependency direction one-way at runtime
|
|
25
|
+
// (install-from-github resolves sources *through* this module) while still
|
|
26
|
+
// sharing the injected-`fetch` shape.
|
|
27
|
+
import type { FetchLike } from "./install-from-github.js";
|
|
28
|
+
|
|
29
|
+
/** Canonical location of the marketplace manifest. */
|
|
30
|
+
const MARKETPLACE_SOURCE_OWNER = "vellum-ai";
|
|
31
|
+
const MARKETPLACE_SOURCE_REPO = "vellum-assistant";
|
|
32
|
+
const MARKETPLACE_FILE_PATH = "experimental/plugins/marketplace.json";
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Manifest schema (subset of the Claude Code marketplace schema)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** `owner/repo`, kebab/underscore/dot segments — matches GitHub's slug rules. */
|
|
39
|
+
const REPO_SLUG_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
40
|
+
/** Install name: a single kebab-case path segment (same rule as the CLI). */
|
|
41
|
+
const PLUGIN_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
42
|
+
|
|
43
|
+
const githubSourceSchema = z.object({
|
|
44
|
+
/** Discriminator. Only GitHub sources are resolved today. */
|
|
45
|
+
source: z.literal("github"),
|
|
46
|
+
/** `owner/repo` of the external plugin repository. */
|
|
47
|
+
repo: z.string().regex(REPO_SLUG_RE, "expected an `owner/repo` slug"),
|
|
48
|
+
/**
|
|
49
|
+
* Directory within the repo that holds the plugin root. Omitted / empty
|
|
50
|
+
* means the repository root. Must not escape the repo.
|
|
51
|
+
*/
|
|
52
|
+
path: z
|
|
53
|
+
.string()
|
|
54
|
+
.refine(
|
|
55
|
+
(p) => !p.split(/[/\\]/).some((seg) => seg === ".." || seg === ""),
|
|
56
|
+
"path must be a clean repo-relative directory",
|
|
57
|
+
)
|
|
58
|
+
.optional(),
|
|
59
|
+
/**
|
|
60
|
+
* Git ref (tag, SHA, or branch) to fetch the plugin from. Required so every
|
|
61
|
+
* whitelisted external plugin is version-pinned.
|
|
62
|
+
*/
|
|
63
|
+
ref: z.string().min(1),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const marketplaceEntrySchema = z.object({
|
|
67
|
+
/** Install name. `assistant plugins install <name>` resolves to this entry. */
|
|
68
|
+
name: z.string().regex(PLUGIN_NAME_RE, "expected a kebab-case install name"),
|
|
69
|
+
source: githubSourceSchema,
|
|
70
|
+
description: z.string().optional(),
|
|
71
|
+
/** Free-form grouping hint (e.g. `productivity`). Informational. */
|
|
72
|
+
category: z.string().optional(),
|
|
73
|
+
homepage: z.string().optional(),
|
|
74
|
+
license: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const marketplaceManifestSchema = z.object({
|
|
78
|
+
name: z.string(),
|
|
79
|
+
owner: z
|
|
80
|
+
.object({
|
|
81
|
+
name: z.string(),
|
|
82
|
+
url: z.string().optional(),
|
|
83
|
+
email: z.string().optional(),
|
|
84
|
+
})
|
|
85
|
+
.optional(),
|
|
86
|
+
plugins: z.array(marketplaceEntrySchema),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** A single whitelisted external plugin entry. */
|
|
90
|
+
export type MarketplaceEntry = z.infer<typeof marketplaceEntrySchema>;
|
|
91
|
+
|
|
92
|
+
/** Concrete GitHub coordinates an entry resolves to for install. */
|
|
93
|
+
export interface ResolvedPluginSource {
|
|
94
|
+
readonly owner: string;
|
|
95
|
+
readonly repo: string;
|
|
96
|
+
/** Directory within the repo holding the plugin root; `""` = repo root. */
|
|
97
|
+
readonly path: string;
|
|
98
|
+
/** Git ref to fetch from. */
|
|
99
|
+
readonly ref: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Options controlling which marketplace revision to read. */
|
|
103
|
+
export interface FetchMarketplaceOptions {
|
|
104
|
+
/** Ref of the canonical repo to read the manifest from. */
|
|
105
|
+
readonly ref: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Dependencies injected by the caller. */
|
|
109
|
+
export interface FetchMarketplaceDeps {
|
|
110
|
+
/** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
|
|
111
|
+
readonly fetch: FetchLike;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The manifest could not be read or did not validate. Distinct from "no
|
|
116
|
+
* manifest at this ref" (a missing file is a normal, empty result) — this
|
|
117
|
+
* signals an upstream/parse problem the caller may choose to surface or
|
|
118
|
+
* degrade past.
|
|
119
|
+
*/
|
|
120
|
+
export class MarketplaceFetchError extends Error {
|
|
121
|
+
constructor(message: string) {
|
|
122
|
+
super(message);
|
|
123
|
+
this.name = "MarketplaceFetchError";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch and validate the marketplace manifest, returning its plugin entries.
|
|
129
|
+
*
|
|
130
|
+
* A missing manifest (HTTP 404) is treated as an empty whitelist — the
|
|
131
|
+
* marketplace is supplementary to the first-party plugin listing, so its
|
|
132
|
+
* absence is not an error. Any other HTTP failure, a non-JSON body, or a
|
|
133
|
+
* schema violation throws {@link MarketplaceFetchError}.
|
|
134
|
+
*/
|
|
135
|
+
export async function fetchMarketplaceEntries(
|
|
136
|
+
deps: FetchMarketplaceDeps,
|
|
137
|
+
opts: FetchMarketplaceOptions,
|
|
138
|
+
): Promise<readonly MarketplaceEntry[]> {
|
|
139
|
+
const { ref } = opts;
|
|
140
|
+
const url =
|
|
141
|
+
`https://api.github.com/repos/${MARKETPLACE_SOURCE_OWNER}/${MARKETPLACE_SOURCE_REPO}` +
|
|
142
|
+
`/contents/${MARKETPLACE_FILE_PATH}?ref=${encodeURIComponent(ref)}`;
|
|
143
|
+
|
|
144
|
+
const res = await deps.fetch(url, {
|
|
145
|
+
headers: {
|
|
146
|
+
// Ask for the raw file body rather than the base64-wrapped Contents
|
|
147
|
+
// envelope, so the response text is the manifest JSON directly.
|
|
148
|
+
Accept: "application/vnd.github.raw",
|
|
149
|
+
"User-Agent": "vellum-assistant-cli",
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (res.status === 404) return [];
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
throw new MarketplaceFetchError(
|
|
156
|
+
`Marketplace manifest fetch failed for ${MARKETPLACE_FILE_PATH} @ ${ref}: HTTP ${res.status}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let json: unknown;
|
|
161
|
+
try {
|
|
162
|
+
json = JSON.parse(await res.text());
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new MarketplaceFetchError(
|
|
165
|
+
`Marketplace manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const parsed = marketplaceManifestSchema.safeParse(json);
|
|
170
|
+
if (!parsed.success) {
|
|
171
|
+
throw new MarketplaceFetchError(
|
|
172
|
+
`Marketplace manifest failed validation: ${parsed.error.message}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return parsed.data.plugins;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Resolve a plugin name to concrete GitHub coordinates using the supplied
|
|
181
|
+
* marketplace entries. Returns `null` when no entry claims the name (the
|
|
182
|
+
* caller then falls back to the first-party source convention).
|
|
183
|
+
*/
|
|
184
|
+
export function resolveMarketplaceSource(
|
|
185
|
+
name: string,
|
|
186
|
+
entries: readonly MarketplaceEntry[],
|
|
187
|
+
): ResolvedPluginSource | null {
|
|
188
|
+
const entry = entries.find((e) => e.name === name);
|
|
189
|
+
if (!entry) return null;
|
|
190
|
+
const [owner, repo] = entry.source.repo.split("/", 2) as [string, string];
|
|
191
|
+
return {
|
|
192
|
+
owner,
|
|
193
|
+
repo,
|
|
194
|
+
path: entry.source.path ?? "",
|
|
195
|
+
ref: entry.source.ref,
|
|
196
|
+
};
|
|
197
|
+
}
|