@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -436,6 +436,108 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
436
436
|
expect(meta.contentHash).toMatch(/^v2:[0-9a-f]{64}$/);
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
+
test("installs a plugin rooted at a sub-path, copying only that subtree", async () => {
|
|
440
|
+
// GIVEN a marketplace entry whose source pins a directory *within* a repo
|
|
441
|
+
// (a monorepo that ships several plugins) rather than the repo root.
|
|
442
|
+
const NESTED_SHA = "0f".repeat(20);
|
|
443
|
+
const NESTED_MANIFEST = {
|
|
444
|
+
name: "vellum-assistant",
|
|
445
|
+
plugins: [
|
|
446
|
+
{
|
|
447
|
+
name: "nested-plugin",
|
|
448
|
+
source: {
|
|
449
|
+
source: "github",
|
|
450
|
+
repo: "example-org/monorepo",
|
|
451
|
+
path: "packages/my-plugin",
|
|
452
|
+
ref: NESTED_SHA,
|
|
453
|
+
},
|
|
454
|
+
description: "A plugin that lives in a monorepo sub-directory.",
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
};
|
|
458
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: NESTED_MANIFEST });
|
|
459
|
+
// The clone carries files both at the repo root and under the pinned
|
|
460
|
+
// sub-path; only the sub-path subtree should be materialized.
|
|
461
|
+
const runGit = fakeGitRunner({
|
|
462
|
+
tree: {
|
|
463
|
+
"package.json": '{"name":"monorepo-root"}',
|
|
464
|
+
"README.md": "# monorepo root",
|
|
465
|
+
"packages/my-plugin/package.json": '{"name":"nested-plugin"}',
|
|
466
|
+
"packages/my-plugin/README.md": "# nested plugin",
|
|
467
|
+
"packages/my-plugin/hooks/init.ts": "export default async () => {};",
|
|
468
|
+
"packages/other-plugin/package.json": '{"name":"other"}',
|
|
469
|
+
},
|
|
470
|
+
commit: NESTED_SHA,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// WHEN we install by name
|
|
474
|
+
const result = await installPlugin(
|
|
475
|
+
{ name: "nested-plugin", ref: "main" },
|
|
476
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// THEN only the sub-path subtree lands, rooted at <pluginsDir>/nested-plugin
|
|
480
|
+
const target = join(pluginsDir, "nested-plugin");
|
|
481
|
+
expect(result.target).toBe(target);
|
|
482
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
483
|
+
'{"name":"nested-plugin"}',
|
|
484
|
+
);
|
|
485
|
+
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
486
|
+
expect(existsSync(join(target, "hooks", "init.ts"))).toBe(true);
|
|
487
|
+
// AND the repo-root and sibling-package files are NOT copied in — the
|
|
488
|
+
// install is scoped to the pinned directory.
|
|
489
|
+
expect(result.fileCount).toBe(3);
|
|
490
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).not.toContain(
|
|
491
|
+
"monorepo-root",
|
|
492
|
+
);
|
|
493
|
+
expect(existsSync(join(target, "packages"))).toBe(false);
|
|
494
|
+
|
|
495
|
+
// AND provenance records the sub-path so an upgrade/diff re-resolves the
|
|
496
|
+
// same directory rather than the repo root.
|
|
497
|
+
const meta = readInstallMeta(target);
|
|
498
|
+
expect(meta?.source.owner).toBe("example-org");
|
|
499
|
+
expect(meta?.source.repo).toBe("monorepo");
|
|
500
|
+
expect(meta?.source.path).toBe("packages/my-plugin");
|
|
501
|
+
expect(meta?.source.ref).toBe(NESTED_SHA);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("a sub-path that does not exist in the clone surfaces a clean not-found", async () => {
|
|
505
|
+
// GIVEN an entry pinning a directory the cloned ref doesn't contain
|
|
506
|
+
const MISSING_SHA = "1a".repeat(20);
|
|
507
|
+
const MISSING_PATH_MANIFEST = {
|
|
508
|
+
name: "vellum-assistant",
|
|
509
|
+
plugins: [
|
|
510
|
+
{
|
|
511
|
+
name: "nested-plugin",
|
|
512
|
+
source: {
|
|
513
|
+
source: "github",
|
|
514
|
+
repo: "example-org/monorepo",
|
|
515
|
+
path: "packages/does-not-exist",
|
|
516
|
+
ref: MISSING_SHA,
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
const fetch = makeContentsFetch({
|
|
522
|
+
tree: {},
|
|
523
|
+
manifest: MISSING_PATH_MANIFEST,
|
|
524
|
+
});
|
|
525
|
+
const runGit = fakeGitRunner({
|
|
526
|
+
tree: { "package.json": '{"name":"monorepo-root"}' },
|
|
527
|
+
commit: MISSING_SHA,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// WHEN we install
|
|
531
|
+
// THEN the absent sub-path yields a hard not-found and nothing is staged
|
|
532
|
+
await expect(
|
|
533
|
+
installPlugin(
|
|
534
|
+
{ name: "nested-plugin", ref: "main" },
|
|
535
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
536
|
+
),
|
|
537
|
+
).rejects.toBeInstanceOf(PluginNotFoundError);
|
|
538
|
+
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
539
|
+
});
|
|
540
|
+
|
|
439
541
|
test("refuses to install when the checked-out commit differs from the pinned SHA", async () => {
|
|
440
542
|
// GIVEN a clone whose resolved HEAD does not match the manifest's pinned
|
|
441
543
|
// commit SHA — i.e. the upstream object served something unexpected
|
|
@@ -17,7 +17,7 @@ import { tmpdir } from "node:os";
|
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
19
19
|
|
|
20
|
-
import { listInstalledPlugins } from "../list-installed-plugins.js";
|
|
20
|
+
import { listAllPlugins, listInstalledPlugins } from "../list-installed-plugins.js";
|
|
21
21
|
|
|
22
22
|
let pluginsDir: string;
|
|
23
23
|
|
|
@@ -152,3 +152,162 @@ describe("listInstalledPlugins", () => {
|
|
|
152
152
|
expect(result.map((p) => p.name)).toEqual(["valid"]);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe("listAllPlugins", () => {
|
|
157
|
+
test("includes default plugins with source=default", () => {
|
|
158
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
159
|
+
const defaults = result.filter((p) => p.source === "default");
|
|
160
|
+
// All 15 default plugins should be present.
|
|
161
|
+
expect(defaults.length).toBe(15);
|
|
162
|
+
// Names should all start with "default-".
|
|
163
|
+
expect(defaults.every((p) => p.name.startsWith("default-"))).toBe(true);
|
|
164
|
+
// None should be disabled by default in a fresh temp dir.
|
|
165
|
+
expect(defaults.every((p) => !p.disabled)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("includes user plugins with source=user", () => {
|
|
169
|
+
mkdirSync(join(pluginsDir, "my-plugin"));
|
|
170
|
+
writeFileSync(
|
|
171
|
+
join(pluginsDir, "my-plugin", "package.json"),
|
|
172
|
+
JSON.stringify({ name: "my-plugin", version: "1.0.0" }),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
176
|
+
const user = result.filter((p) => p.source === "user");
|
|
177
|
+
expect(user).toHaveLength(1);
|
|
178
|
+
expect(user[0]!.name).toBe("my-plugin");
|
|
179
|
+
expect(user[0]!.disabled).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("user plugins appear before default plugins", () => {
|
|
183
|
+
mkdirSync(join(pluginsDir, "zzz-user"));
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(pluginsDir, "zzz-user", "package.json"),
|
|
186
|
+
JSON.stringify({ name: "zzz-user", version: "1.0.0" }),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
190
|
+
const firstDefaultIdx = result.findIndex((p) => p.source === "default");
|
|
191
|
+
const lastUserIdx = result
|
|
192
|
+
.map((p, i) => (p.source === "user" ? i : -1))
|
|
193
|
+
.filter((i) => i >= 0)
|
|
194
|
+
.pop();
|
|
195
|
+
|
|
196
|
+
expect(lastUserIdx).toBeDefined();
|
|
197
|
+
expect(firstDefaultIdx).toBeGreaterThan(lastUserIdx!);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("detects disabled state for user plugins", () => {
|
|
201
|
+
mkdirSync(join(pluginsDir, "my-plugin"));
|
|
202
|
+
writeFileSync(
|
|
203
|
+
join(pluginsDir, "my-plugin", "package.json"),
|
|
204
|
+
JSON.stringify({ name: "my-plugin", version: "1.0.0" }),
|
|
205
|
+
);
|
|
206
|
+
writeFileSync(join(pluginsDir, "my-plugin", ".disabled"), "");
|
|
207
|
+
|
|
208
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
209
|
+
const entry = result.find((p) => p.name === "my-plugin");
|
|
210
|
+
expect(entry).toBeDefined();
|
|
211
|
+
expect(entry!.disabled).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("detects disabled state for default plugins via stub directory", () => {
|
|
215
|
+
mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
|
|
216
|
+
writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
|
|
217
|
+
|
|
218
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
219
|
+
const advisor = result.find((p) => p.name === "default-advisor");
|
|
220
|
+
expect(advisor).toBeDefined();
|
|
221
|
+
expect(advisor!.disabled).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("default stub directory is excluded from user listing", () => {
|
|
225
|
+
mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
|
|
226
|
+
writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
|
|
227
|
+
|
|
228
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
229
|
+
// Should appear exactly once, as a default entry (not a user entry).
|
|
230
|
+
const advisorEntries = result.filter((p) => p.name === "default-advisor");
|
|
231
|
+
expect(advisorEntries).toHaveLength(1);
|
|
232
|
+
expect(advisorEntries[0]!.source).toBe("default");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("sort order: enabled user, disabled user, enabled default, disabled default", () => {
|
|
236
|
+
// User plugins
|
|
237
|
+
mkdirSync(join(pluginsDir, "aaa-enabled"));
|
|
238
|
+
writeFileSync(
|
|
239
|
+
join(pluginsDir, "aaa-enabled", "package.json"),
|
|
240
|
+
JSON.stringify({ name: "aaa-enabled", version: "1.0.0" }),
|
|
241
|
+
);
|
|
242
|
+
mkdirSync(join(pluginsDir, "bbb-disabled"));
|
|
243
|
+
writeFileSync(
|
|
244
|
+
join(pluginsDir, "bbb-disabled", "package.json"),
|
|
245
|
+
JSON.stringify({ name: "bbb-disabled", version: "1.0.0" }),
|
|
246
|
+
);
|
|
247
|
+
writeFileSync(join(pluginsDir, "bbb-disabled", ".disabled"), "");
|
|
248
|
+
|
|
249
|
+
// Disable one default plugin
|
|
250
|
+
mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
|
|
251
|
+
writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
|
|
252
|
+
|
|
253
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
254
|
+
|
|
255
|
+
const enabledUserIdx = result.findIndex(
|
|
256
|
+
(p) => p.source === "user" && !p.disabled,
|
|
257
|
+
);
|
|
258
|
+
const disabledUserIdx = result.findIndex(
|
|
259
|
+
(p) => p.source === "user" && p.disabled,
|
|
260
|
+
);
|
|
261
|
+
const enabledDefaultIdx = result.findIndex(
|
|
262
|
+
(p) => p.source === "default" && !p.disabled,
|
|
263
|
+
);
|
|
264
|
+
const disabledDefaultIdx = result.findIndex(
|
|
265
|
+
(p) => p.source === "default" && p.disabled,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// All groups present.
|
|
269
|
+
expect(enabledUserIdx).toBeGreaterThanOrEqual(0);
|
|
270
|
+
expect(disabledUserIdx).toBeGreaterThan(enabledUserIdx);
|
|
271
|
+
expect(enabledDefaultIdx).toBeGreaterThan(disabledUserIdx);
|
|
272
|
+
expect(disabledDefaultIdx).toBeGreaterThan(enabledDefaultIdx);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("user plugins sorted by install date within each group", () => {
|
|
276
|
+
// Create two user plugins with different install dates.
|
|
277
|
+
// First plugin (older).
|
|
278
|
+
mkdirSync(join(pluginsDir, "older-plugin"));
|
|
279
|
+
writeFileSync(
|
|
280
|
+
join(pluginsDir, "older-plugin", "package.json"),
|
|
281
|
+
JSON.stringify({ name: "older-plugin", version: "1.0.0" }),
|
|
282
|
+
);
|
|
283
|
+
writeFileSync(
|
|
284
|
+
join(pluginsDir, "older-plugin", "install-meta.json"),
|
|
285
|
+
JSON.stringify({ installedAt: "2025-01-01T00:00:00.000Z" }),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Second plugin (newer).
|
|
289
|
+
mkdirSync(join(pluginsDir, "newer-plugin"));
|
|
290
|
+
writeFileSync(
|
|
291
|
+
join(pluginsDir, "newer-plugin", "package.json"),
|
|
292
|
+
JSON.stringify({ name: "newer-plugin", version: "1.0.0" }),
|
|
293
|
+
);
|
|
294
|
+
writeFileSync(
|
|
295
|
+
join(pluginsDir, "newer-plugin", "install-meta.json"),
|
|
296
|
+
JSON.stringify({ installedAt: "2025-06-01T00:00:00.000Z" }),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
300
|
+
const userPlugins = result.filter((p) => p.source === "user");
|
|
301
|
+
expect(userPlugins).toHaveLength(2);
|
|
302
|
+
// Older plugin should come first (install date ascending).
|
|
303
|
+
expect(userPlugins[0]!.name).toBe("older-plugin");
|
|
304
|
+
expect(userPlugins[1]!.name).toBe("newer-plugin");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("default plugins have version from their manifest", () => {
|
|
308
|
+
const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
|
|
309
|
+
const advisor = result.find((p) => p.name === "default-advisor");
|
|
310
|
+
expect(advisor).toBeDefined();
|
|
311
|
+
expect(advisor!.packageJson?.version).toBeTruthy();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -13,10 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
17
|
|
|
18
18
|
import { getWorkspacePluginsDir } from "../../util/platform.js";
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Directory containing first-party default plugin packages. Each subdirectory
|
|
22
|
+
* has a `package.json` with `name` (prefixed `default-`) and `version`.
|
|
23
|
+
* Read from the filesystem at call time to avoid pulling hook/tool
|
|
24
|
+
* implementations into the CLI process (which would create circular
|
|
25
|
+
* dependencies in test environments).
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_PLUGINS_DIR = join(
|
|
28
|
+
dirname(new URL(import.meta.url).pathname),
|
|
29
|
+
"..",
|
|
30
|
+
"..",
|
|
31
|
+
"plugins",
|
|
32
|
+
"defaults",
|
|
33
|
+
);
|
|
34
|
+
|
|
20
35
|
/** Minimal manifest fields surfaced to the CLI. */
|
|
21
36
|
export interface PluginPackageMetadata {
|
|
22
37
|
readonly name?: string;
|
|
@@ -40,6 +55,20 @@ export interface InstalledPluginInfo {
|
|
|
40
55
|
readonly issues: readonly string[];
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
/** Where the plugin comes from. */
|
|
59
|
+
export type PluginSource = "user" | "default";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extended plugin entry that includes source (`user` vs `default`) and
|
|
63
|
+
* disabled status. Used by {@link listAllPlugins}.
|
|
64
|
+
*/
|
|
65
|
+
export interface AllPluginInfo extends InstalledPluginInfo {
|
|
66
|
+
/** Whether this is a user-installed or first-party default plugin. */
|
|
67
|
+
readonly source: PluginSource;
|
|
68
|
+
/** Whether the plugin is disabled via a `.disabled` sentinel file. */
|
|
69
|
+
readonly disabled: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
/** Options accepted by {@link listInstalledPlugins}. */
|
|
44
73
|
export interface ListInstalledPluginsOptions {
|
|
45
74
|
/** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
|
|
@@ -146,3 +175,152 @@ function readPluginEntry(
|
|
|
146
175
|
|
|
147
176
|
return { name, target, packageJson, issues };
|
|
148
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* List all plugins — both user-installed (from `<workspace>/plugins/`) and
|
|
181
|
+
* first-party defaults (from the source tree). Each entry is annotated with
|
|
182
|
+
* its `source` (`"user"` or `"default"`) and `disabled` status (whether a
|
|
183
|
+
* `.disabled` sentinel file exists in the plugin's workspace directory).
|
|
184
|
+
*
|
|
185
|
+
* For user plugins, the `.disabled` file lives in the plugin's own install
|
|
186
|
+
* directory. For default plugins, it lives in a stub directory at
|
|
187
|
+
* `<workspace>/plugins/<manifest-name>/` (created by `plugins disable`).
|
|
188
|
+
*
|
|
189
|
+
* Stub directories created by `plugins disable <default-name>` are excluded
|
|
190
|
+
* from the user listing so a disabled default plugin appears only once (as a
|
|
191
|
+
* default entry, not a duplicate user entry with "missing package.json").
|
|
192
|
+
*
|
|
193
|
+
* Sort order:
|
|
194
|
+
* 1. Enabled user plugins (by install date, oldest first — matches
|
|
195
|
+
* hook/tool resolution order)
|
|
196
|
+
* 2. Disabled user plugins (by install date)
|
|
197
|
+
* 3. Enabled default plugins (by repo array order — matches registration
|
|
198
|
+
* order which fixes hook-chain order)
|
|
199
|
+
* 4. Disabled default plugins (by repo array order)
|
|
200
|
+
*/
|
|
201
|
+
export function listAllPlugins(
|
|
202
|
+
opts: ListInstalledPluginsOptions = {},
|
|
203
|
+
): AllPluginInfo[] {
|
|
204
|
+
const pluginsDir = opts.workspacePluginsDir ?? getWorkspacePluginsDir();
|
|
205
|
+
|
|
206
|
+
// ── User plugins ───────────────────────────────────────────────────────
|
|
207
|
+
// Filter out default-plugin stub directories (created by `plugins disable
|
|
208
|
+
// default-<name>`) so they don't show up as duplicate user entries.
|
|
209
|
+
const defaultNames = new Set(
|
|
210
|
+
readDefaultPluginManifests().map((m) => m.name),
|
|
211
|
+
);
|
|
212
|
+
const userPlugins: AllPluginInfo[] = listInstalledPlugins(opts)
|
|
213
|
+
.filter((entry) => !defaultNames.has(entry.name))
|
|
214
|
+
.map((entry) => ({
|
|
215
|
+
...entry,
|
|
216
|
+
source: "user" as const,
|
|
217
|
+
disabled: existsSync(join(entry.target, ".disabled")),
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
// ── Default plugins ────────────────────────────────────────────────────
|
|
221
|
+
// Default plugins live in the source tree at src/plugins/defaults/<name>/.
|
|
222
|
+
// Read each package.json from the filesystem to get name+version without
|
|
223
|
+
// importing hook/tool implementations (which would create circular
|
|
224
|
+
// dependencies in test environments). The .disabled sentinel lives in a
|
|
225
|
+
// stub directory at <workspace>/plugins/<manifest-name>/.
|
|
226
|
+
// readDefaultPluginManifests returns in repo array (registration) order.
|
|
227
|
+
const defaultPlugins: AllPluginInfo[] = readDefaultPluginManifests().map(
|
|
228
|
+
(manifest) => {
|
|
229
|
+
const target = join(pluginsDir, manifest.name);
|
|
230
|
+
const disabled = existsSync(join(target, ".disabled"));
|
|
231
|
+
return {
|
|
232
|
+
name: manifest.name,
|
|
233
|
+
target,
|
|
234
|
+
packageJson: {
|
|
235
|
+
name: manifest.name,
|
|
236
|
+
version: manifest.version,
|
|
237
|
+
},
|
|
238
|
+
issues: [],
|
|
239
|
+
source: "default" as const,
|
|
240
|
+
disabled,
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Sort: enabled user (install date), disabled user (install date),
|
|
246
|
+
// enabled default (repo order), disabled default (repo order).
|
|
247
|
+
const enabledUser = userPlugins.filter((p) => !p.disabled);
|
|
248
|
+
const disabledUser = userPlugins.filter((p) => p.disabled);
|
|
249
|
+
const enabledDefault = defaultPlugins.filter((p) => !p.disabled);
|
|
250
|
+
const disabledDefault = defaultPlugins.filter((p) => p.disabled);
|
|
251
|
+
|
|
252
|
+
enabledUser.sort((a, b) => getPluginInstallDate(a) - getPluginInstallDate(b));
|
|
253
|
+
disabledUser.sort(
|
|
254
|
+
(a, b) => getPluginInstallDate(a) - getPluginInstallDate(b),
|
|
255
|
+
);
|
|
256
|
+
// enabledDefault and disabledDefault keep repo array order (no sort).
|
|
257
|
+
|
|
258
|
+
return [...enabledUser, ...disabledUser, ...enabledDefault, ...disabledDefault];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface DefaultPluginManifest {
|
|
262
|
+
readonly name: string;
|
|
263
|
+
readonly version?: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Read first-party default plugin manifests from the filesystem. Each
|
|
268
|
+
* subdirectory under {@link DEFAULT_PLUGINS_DIR} that has a `package.json`
|
|
269
|
+
* with a `name` field is included. This avoids importing `defaults/index.ts`
|
|
270
|
+
* (which would pull in hook/tool implementations and create circular
|
|
271
|
+
* dependencies in test environments).
|
|
272
|
+
*/
|
|
273
|
+
function readDefaultPluginManifests(): readonly DefaultPluginManifest[] {
|
|
274
|
+
if (!existsSync(DEFAULT_PLUGINS_DIR)) return [];
|
|
275
|
+
|
|
276
|
+
const entries = readdirSync(DEFAULT_PLUGINS_DIR, { withFileTypes: true })
|
|
277
|
+
.filter((e) => e.isDirectory())
|
|
278
|
+
.map((e) => e.name)
|
|
279
|
+
.sort();
|
|
280
|
+
|
|
281
|
+
const manifests: DefaultPluginManifest[] = [];
|
|
282
|
+
for (const name of entries) {
|
|
283
|
+
const pkgJsonPath = join(DEFAULT_PLUGINS_DIR, name, "package.json");
|
|
284
|
+
if (!existsSync(pkgJsonPath)) continue;
|
|
285
|
+
try {
|
|
286
|
+
const raw = readFileSync(pkgJsonPath, "utf8");
|
|
287
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
288
|
+
if (typeof parsed.name === "string") {
|
|
289
|
+
manifests.push({
|
|
290
|
+
name: parsed.name,
|
|
291
|
+
version:
|
|
292
|
+
typeof parsed.version === "string" ? parsed.version : undefined,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
// Skip malformed entries — lenient like listInstalledPlugins.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return manifests;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolve the install date for a user plugin directory, in epoch ms.
|
|
304
|
+
* Reads `install-meta.json`'s `installedAt` field first, falling back to
|
|
305
|
+
* the directory's birthtime. Mirrors the logic in mtime-cache's
|
|
306
|
+
* `getInstallDate` so the sort order matches hook/tool resolution order.
|
|
307
|
+
*/
|
|
308
|
+
function getPluginInstallDate(plugin: AllPluginInfo): number {
|
|
309
|
+
const metaPath = join(plugin.target, "install-meta.json");
|
|
310
|
+
try {
|
|
311
|
+
if (existsSync(metaPath)) {
|
|
312
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<string, unknown>;
|
|
313
|
+
if (typeof raw.installedAt === "string") {
|
|
314
|
+
const ms = Date.parse(raw.installedAt);
|
|
315
|
+
if (Number.isFinite(ms)) return ms;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Fall through to birthtime.
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
return statSync(plugin.target).birthtimeMs;
|
|
323
|
+
} catch {
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
afterAll,
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
mock,
|
|
10
|
+
test,
|
|
11
|
+
} from "bun:test";
|
|
12
|
+
|
|
13
|
+
const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
14
|
+
const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
|
|
15
|
+
|
|
16
|
+
function ensureTestDir(): void {
|
|
17
|
+
const dirs = [
|
|
18
|
+
WORKSPACE_DIR,
|
|
19
|
+
join(WORKSPACE_DIR, "data"),
|
|
20
|
+
join(WORKSPACE_DIR, "data", "memory"),
|
|
21
|
+
join(WORKSPACE_DIR, "data", "logs"),
|
|
22
|
+
];
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeLoggerStub(): Record<string, unknown> {
|
|
29
|
+
const stub: Record<string, unknown> = {};
|
|
30
|
+
for (const m of [
|
|
31
|
+
"info",
|
|
32
|
+
"warn",
|
|
33
|
+
"error",
|
|
34
|
+
"debug",
|
|
35
|
+
"trace",
|
|
36
|
+
"fatal",
|
|
37
|
+
"silent",
|
|
38
|
+
"child",
|
|
39
|
+
]) {
|
|
40
|
+
stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
|
|
41
|
+
}
|
|
42
|
+
return stub;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
mock.module("../../util/logger.js", () => ({
|
|
46
|
+
getLogger: () => makeLoggerStub(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
afterAll(() => {
|
|
50
|
+
mock.restore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
import { resolveCallSiteConfig } from "../llm-resolver.js";
|
|
54
|
+
import { invalidateConfigCache, loadConfig } from "../loader.js";
|
|
55
|
+
|
|
56
|
+
function writeConfig(obj: unknown): void {
|
|
57
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Base config where the active profile resolves to a DIFFERENT model than the
|
|
62
|
+
* `balanced` profile that the shipped `memoryV3SelectL2` call-site default
|
|
63
|
+
* points at. This lets each test distinguish the two failure modes:
|
|
64
|
+
* - call-site default applied (fixed) -> model "balanced-model"
|
|
65
|
+
* - silently downgraded to active -> model "active-model"
|
|
66
|
+
*/
|
|
67
|
+
function baseLlm(callSites: Record<string, unknown>): unknown {
|
|
68
|
+
return {
|
|
69
|
+
llm: {
|
|
70
|
+
default: { provider: "anthropic", model: "default-model" },
|
|
71
|
+
profiles: {
|
|
72
|
+
balanced: { provider: "anthropic", model: "balanced-model" },
|
|
73
|
+
speedy: { provider: "anthropic", model: "active-model" },
|
|
74
|
+
},
|
|
75
|
+
activeProfile: "speedy",
|
|
76
|
+
callSites,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("config recovery prunes call-site overrides emptied by a strip", () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
ensureTestDir();
|
|
84
|
+
if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH, { force: true });
|
|
85
|
+
delete process.env.IS_PLATFORM;
|
|
86
|
+
invalidateConfigCache();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
delete process.env.IS_PLATFORM;
|
|
91
|
+
if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH, { force: true });
|
|
92
|
+
invalidateConfigCache();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("an undefined call-site profile ref falls back to the shipped default, not the active profile", () => {
|
|
96
|
+
// The `.profile` ref is invalid (no such profile), so schema recovery
|
|
97
|
+
// strips it. Before the fix this left `callSites.memoryV3SelectL2 = {}`,
|
|
98
|
+
// which the resolver treats as a present override and so skips the shipped
|
|
99
|
+
// `{profile:"balanced"}` default — silently resolving to the active profile.
|
|
100
|
+
writeConfig(baseLlm({ memoryV3SelectL2: { profile: "ghost-profile" } }));
|
|
101
|
+
|
|
102
|
+
const config = loadConfig();
|
|
103
|
+
|
|
104
|
+
// The emptied call-site entry must be pruned entirely, not left as `{}`.
|
|
105
|
+
expect(config.llm.callSites?.memoryV3SelectL2).toBeUndefined();
|
|
106
|
+
|
|
107
|
+
// Resolution now lands on the shipped call-site default (balanced), not the
|
|
108
|
+
// active profile ("active-model"), which is what the bug produced.
|
|
109
|
+
const resolved = resolveCallSiteConfig("memoryV3SelectL2", config.llm);
|
|
110
|
+
expect(resolved.model).toBe("balanced-model");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a valid sibling call-site override survives while the invalid one is pruned", () => {
|
|
114
|
+
// memoryRouter -> balanced is valid and must be preserved; only the invalid
|
|
115
|
+
// memoryV3SelectL2 entry is pruned. Guards against over-pruning the parent.
|
|
116
|
+
writeConfig(
|
|
117
|
+
baseLlm({
|
|
118
|
+
memoryV3SelectL2: { profile: "missing" },
|
|
119
|
+
memoryRouter: { profile: "balanced" },
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
|
|
125
|
+
expect(config.llm.callSites?.memoryV3SelectL2).toBeUndefined();
|
|
126
|
+
expect(config.llm.callSites?.memoryRouter).toEqual({ profile: "balanced" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("a partial override keeping other fields is not pruned", () => {
|
|
130
|
+
// Stripping the invalid `.profile` leaves a non-empty `{temperature:0.5}`,
|
|
131
|
+
// a legitimate user override the resolver should keep (and which therefore
|
|
132
|
+
// still shadows the shipped default per existing either/or semantics).
|
|
133
|
+
writeConfig(
|
|
134
|
+
baseLlm({ memoryV3SelectL2: { profile: "missing", temperature: 0.5 } }),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
|
|
139
|
+
expect(config.llm.callSites?.memoryV3SelectL2).toEqual({
|
|
140
|
+
temperature: 0.5,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
"risk": "low",
|
|
9
9
|
"input_schema": {
|
|
10
10
|
"type": "object",
|
|
11
|
-
"properties": {
|
|
11
|
+
"properties": {
|
|
12
|
+
"target_client_id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
12
17
|
"required": []
|
|
13
18
|
},
|
|
14
19
|
"executor": "tools/computer-use-observe.ts",
|