@vellumai/assistant 0.9.0 → 0.10.0-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 +18 -34
- package/bun.lock +7 -8
- package/docs/activation-funnel-telemetry.md +28 -22
- package/docs/architecture/security.md +29 -28
- package/docs/stt-provider-onboarding.md +3 -5
- package/docs/workflows-testing.md +13 -44
- package/docs/workflows.md +3 -5
- package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
- package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
- package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
- package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
- package/openapi.yaml +976 -63
- package/package.json +2 -1
- package/scripts/sync-llm-catalog.ts +6 -15
- package/scripts/sync-web-search-catalog.ts +3 -11
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
- package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/avatar-identity-sync.test.ts +2 -27
- package/src/__tests__/btw-routes.test.ts +6 -8
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/checker.test.ts +0 -3
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +268 -27
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/config-watcher.test.ts +0 -18
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-broker.test.ts +449 -1
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-execution-tools.test.ts +0 -1
- package/src/__tests__/credential-prompt-route.test.ts +4 -4
- package/src/__tests__/credential-routes.test.ts +360 -0
- package/src/__tests__/credential-security-invariants.test.ts +4 -13
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +152 -1
- package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -7
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/identity-routes.test.ts +0 -189
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +4 -7
- package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
- package/src/__tests__/llm-catalog-parity.test.ts +30 -23
- package/src/__tests__/llm-resolver.test.ts +70 -24
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
- package/src/__tests__/onboarding-persona-write.test.ts +1 -1
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +49 -14
- package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/registry.test.ts +2 -7
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
- package/src/__tests__/schedule-routes.test.ts +0 -30
- package/src/__tests__/schedule-tools.test.ts +2 -18
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +51 -1
- package/src/__tests__/skill-runtime-path.test.ts +2 -3
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +266 -0
- package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
- package/src/__tests__/token-manager.test.ts +519 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/tool-executor.test.ts +0 -79
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +8 -10
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +2 -3
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
- package/src/__tests__/workspace-greetings.test.ts +152 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +49 -29
- package/src/api/README.md +6 -6
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +28 -4
- package/src/api/responses/home.ts +26 -4
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +183 -80
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-access-wait.ts +1 -1
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/calls/voice-session-bridge.ts +2 -2
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +197 -4
- package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
- package/src/cli/lib/diff-plugin.ts +346 -0
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/install-from-github.ts +105 -17
- package/src/cli/lib/merge-plugin-tree.ts +328 -0
- package/src/cli/lib/plugin-fingerprint.ts +14 -0
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +298 -10
- package/src/cli/program.ts +2 -6
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/subagent/SKILL.md +4 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
- package/src/config/bundled-skills/workflows/SKILL.md +14 -8
- package/src/config/bundled-tool-registry.ts +2 -7
- package/src/config/call-site-defaults.ts +15 -2
- package/src/config/feature-flag-registry.json +46 -31
- package/src/config/inference-profile-validation.ts +26 -0
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/loader.ts +4 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/profile-order.ts +28 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/elevenlabs.ts +0 -1
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +109 -44
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +89 -106
- package/src/contacts/contacts-write.ts +5 -22
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +88 -54
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/credential-execution/process-manager.ts +55 -14
- package/src/credential-execution/prompted-credential.ts +2 -3
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/config-watcher.ts +0 -4
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +114 -22
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-slash.ts +2 -23
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +27 -14
- package/src/daemon/conversation.ts +66 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +15 -16
- package/src/daemon/handlers/config-slack-channel.ts +22 -3
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -27
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/conversations.ts +3 -3
- package/src/daemon/message-types/sync.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/orphan-reaper.test.ts +0 -19
- package/src/daemon/orphan-reaper.ts +2 -24
- package/src/daemon/server.ts +0 -10
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/home/relationship-state.ts +2 -4
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/bookmark-crud.ts +1 -2
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +17 -17
- package/src/memory/embedding-backend.ts +38 -1
- package/src/memory/embedding-billing-breaker.ts +96 -0
- package/src/memory/jobs-store.ts +25 -13
- package/src/memory/jobs-worker.ts +54 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-constants.ts +4 -4
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +28 -227
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/run-migrations.ts +41 -0
- package/src/memory/migrations/validate-migration-state.ts +1 -1
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -5
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/adapter.ts +1 -1
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/shared.ts +29 -0
- package/src/notifications/adapters/slack.ts +58 -103
- package/src/notifications/adapters/telegram.ts +2 -20
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +16 -3
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +7 -16
- package/src/notifications/notification-utils.ts +19 -20
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -121
- package/src/oauth/AGENTS.md +5 -24
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/constants.ts +1 -1
- package/src/plugin-api/index.ts +33 -1
- package/src/plugin-api/model-profiles.ts +33 -0
- package/src/plugin-api/types.ts +50 -2
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +60 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
- package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +14 -4
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/AGENTS.md +0 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +59 -63
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/btw-sidechain.ts +3 -6
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +22 -45
- package/src/runtime/channel-invite-transports/telegram.ts +4 -4
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +3 -3
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +9 -25
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
- package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/btw-routes.ts +1 -27
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +39 -14
- package/src/runtime/routes/credential-routes.ts +40 -16
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/identity-routes.ts +1 -296
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +202 -3
- package/src/runtime/routes/schedule-routes.ts +0 -22
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +229 -44
- package/src/runtime/routes/workflow-routes.ts +131 -29
- package/src/runtime/routes/workspace-greetings.ts +55 -0
- package/src/runtime/sync/resource-sync-events.ts +1 -11
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/inference-profile.ts +2 -14
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +17 -4
- package/src/subagent/types.ts +6 -0
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/AGENTS.md +3 -3
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +30 -19
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search-error.ts +1 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +4 -3
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +7 -12
- package/src/tools/schedule/update.ts +4 -11
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/side-effects.ts +2 -17
- package/src/tools/skills/execute.ts +33 -0
- package/src/tools/subagent/spawn.ts +61 -12
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +18 -13
- package/src/tools/tool-manifest.ts +0 -2
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +64 -3
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.test.ts +8 -18
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +10 -3
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/capabilities.ts +2 -3
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -28
- package/src/workflows/run-manager.ts +66 -24
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
- package/src/__tests__/credential-security-e2e.test.ts +0 -362
- package/src/__tests__/credential-vault-unit.test.ts +0 -1528
- package/src/__tests__/credential-vault.test.ts +0 -1706
- package/src/__tests__/identity-intro-cache.test.ts +0 -315
- package/src/__tests__/secret-onetime-send.test.ts +0 -182
- package/src/cli/commands/__tests__/task.test.ts +0 -914
- package/src/cli/commands/task.ts +0 -771
- package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
- package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
- package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
- package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
- package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
- package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
- package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
- package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
- package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
- package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
- package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
- package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
- package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
- package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
- package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
- package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
- package/src/memory/preloaded-apps.ts +0 -116
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
- package/src/runtime/routes/identity-intro-cache.ts +0 -172
- package/src/tools/credentials/vault.ts +0 -712
|
@@ -36,6 +36,10 @@ import {
|
|
|
36
36
|
fetchMarketplaceEntries,
|
|
37
37
|
type MarketplaceEntry,
|
|
38
38
|
} from "./plugin-marketplace.js";
|
|
39
|
+
import {
|
|
40
|
+
detectPluginSurfaces,
|
|
41
|
+
type PluginSurfaces,
|
|
42
|
+
} from "./plugin-surfaces.js";
|
|
39
43
|
|
|
40
44
|
/** Full commit SHA (40 hex SHA-1 or 64 hex SHA-256). */
|
|
41
45
|
const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
@@ -131,6 +135,12 @@ export interface PluginInspection {
|
|
|
131
135
|
readonly remote: PluginRemoteInfo | null;
|
|
132
136
|
/** Marketplace fetch error message, when the catalog could not be read. */
|
|
133
137
|
readonly remoteError: string | null;
|
|
138
|
+
/**
|
|
139
|
+
* Surfaces the installed copy contributes (skills, hooks, tools), read from
|
|
140
|
+
* its on-disk tree. `null` when the plugin is not installed — there is no
|
|
141
|
+
* tree to inspect, and the marketplace metadata does not enumerate surfaces.
|
|
142
|
+
*/
|
|
143
|
+
readonly surfaces: PluginSurfaces | null;
|
|
134
144
|
}
|
|
135
145
|
|
|
136
146
|
/** Neither an installed copy nor a marketplace entry claims the name. */
|
|
@@ -266,6 +276,7 @@ export async function inspectPlugin(
|
|
|
266
276
|
});
|
|
267
277
|
const installed = entry !== null;
|
|
268
278
|
const local = entry ? readLocal(entry, readInstallMeta(entry.target)) : null;
|
|
279
|
+
const surfaces = entry ? detectPluginSurfaces(entry.target) : null;
|
|
269
280
|
|
|
270
281
|
let remote: PluginRemoteInfo | null = null;
|
|
271
282
|
let remoteError: string | null = null;
|
|
@@ -294,7 +305,7 @@ export async function inspectPlugin(
|
|
|
294
305
|
}
|
|
295
306
|
|
|
296
307
|
const status = classify(installed, local, remote, remoteError);
|
|
297
|
-
return { name, installed, status, local, remote, remoteError };
|
|
308
|
+
return { name, installed, status, local, remote, remoteError, surfaces };
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
function classify(
|
|
@@ -221,7 +221,7 @@ function isTransientUpstreamStatus(res: Response): boolean {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/** Resolved GitHub coordinates a plugin name is fetched from. */
|
|
224
|
-
interface PluginFetchSource {
|
|
224
|
+
export interface PluginFetchSource {
|
|
225
225
|
readonly owner: string;
|
|
226
226
|
readonly repo: string;
|
|
227
227
|
/** Repo-relative directory holding the plugin root; `""` = repo root. */
|
|
@@ -376,22 +376,13 @@ export async function installPlugin(
|
|
|
376
376
|
let commit: string | null = null;
|
|
377
377
|
let committedAt: string | null = null;
|
|
378
378
|
try {
|
|
379
|
-
const
|
|
380
|
-
source,
|
|
381
|
-
|
|
382
|
-
deps.runGit ?? defaultGitRunner,
|
|
379
|
+
const materialized = await materializePluginTree(
|
|
380
|
+
{ source, name, stubRef: marketplaceRef, destDir: stagingDir },
|
|
381
|
+
deps,
|
|
383
382
|
);
|
|
384
|
-
fileCount =
|
|
385
|
-
commit =
|
|
386
|
-
committedAt =
|
|
387
|
-
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude
|
|
388
|
-
// Code plugin) that the Vellum loader can't run as-is. When we curate an
|
|
389
|
-
// adapter stub for it, overlay the stub and run its transform so the
|
|
390
|
-
// materialized tree is a valid Vellum plugin. Raw clones (no stub) are
|
|
391
|
-
// left untouched.
|
|
392
|
-
if (fileCount > 0) {
|
|
393
|
-
await applyAdapterStub(name, marketplaceRef, stagingDir, deps);
|
|
394
|
-
}
|
|
383
|
+
fileCount = materialized.fileCount;
|
|
384
|
+
commit = materialized.commit;
|
|
385
|
+
committedAt = materialized.committedAt;
|
|
395
386
|
} catch (err) {
|
|
396
387
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
397
388
|
throw err;
|
|
@@ -402,6 +393,52 @@ export async function installPlugin(
|
|
|
402
393
|
throw new PluginNotFoundError(name, ref, sourceLabel(source));
|
|
403
394
|
}
|
|
404
395
|
|
|
396
|
+
finalizeStagedInstall(stagingDir, {
|
|
397
|
+
name,
|
|
398
|
+
source,
|
|
399
|
+
ref,
|
|
400
|
+
commit,
|
|
401
|
+
committedAt,
|
|
402
|
+
pluginsDir,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return { name, target, fileCount, ref, commit, committedAt };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Inputs for {@link finalizeStagedInstall}. */
|
|
409
|
+
export interface FinalizeStagedInstallParams {
|
|
410
|
+
readonly name: string;
|
|
411
|
+
/** Source coordinates recorded in the provenance sidecar. */
|
|
412
|
+
readonly source: PluginFetchSource;
|
|
413
|
+
/** Ref recorded in the sidecar (the resolved commit SHA for marketplace installs). */
|
|
414
|
+
readonly ref: string;
|
|
415
|
+
readonly commit: string | null;
|
|
416
|
+
/** ISO-8601 committer timestamp of {@link FinalizeStagedInstallParams.commit} (UTC); null when unknown. */
|
|
417
|
+
readonly committedAt: string | null;
|
|
418
|
+
/** Served plugins directory; the staging dir is swapped into `<pluginsDir>/<name>`. */
|
|
419
|
+
readonly pluginsDir: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Fingerprint a fully-populated `stagingDir`, write its provenance sidecar, and
|
|
424
|
+
* atomically swap it into `<pluginsDir>/<name>`. Returns the final install path
|
|
425
|
+
* and the fingerprint that was recorded.
|
|
426
|
+
*
|
|
427
|
+
* Shared by {@link installPlugin} (fresh materialization) and the merge-based
|
|
428
|
+
* `plugins upgrade --strategy` path so both record identical provenance and use
|
|
429
|
+
* the same atomic rm+rename swap.
|
|
430
|
+
*/
|
|
431
|
+
export function finalizeStagedInstall(
|
|
432
|
+
stagingDir: string,
|
|
433
|
+
{
|
|
434
|
+
name,
|
|
435
|
+
source,
|
|
436
|
+
ref,
|
|
437
|
+
commit,
|
|
438
|
+
committedAt,
|
|
439
|
+
pluginsDir,
|
|
440
|
+
}: FinalizeStagedInstallParams,
|
|
441
|
+
): { target: string; fingerprint: Fingerprint } {
|
|
405
442
|
// Hash the materialized tree before the sidecar is written (so the sidecar
|
|
406
443
|
// never hashes itself) — the baseline `plugins inspect` uses to detect later
|
|
407
444
|
// local edits. The per-file fingerprint answers "which files changed"; the
|
|
@@ -428,13 +465,14 @@ export async function installPlugin(
|
|
|
428
465
|
// rm and the rename — and at that point the staging dir is fully populated.
|
|
429
466
|
// Ensure the served `plugins/` directory exists: staging now lives outside
|
|
430
467
|
// it, so the target's parent is no longer created as a side effect.
|
|
468
|
+
const target = join(pluginsDir, name);
|
|
431
469
|
mkdirSync(pluginsDir, { recursive: true });
|
|
432
470
|
if (existsSync(target)) {
|
|
433
471
|
rmSync(target, { recursive: true, force: true });
|
|
434
472
|
}
|
|
435
473
|
renameSync(stagingDir, target);
|
|
436
474
|
|
|
437
|
-
return {
|
|
475
|
+
return { target, fingerprint };
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
/** Cap on any single git invocation; a shallow fetch is well under this. */
|
|
@@ -523,6 +561,56 @@ export interface InstallMeta {
|
|
|
523
561
|
readonly fingerprint: Fingerprint | null;
|
|
524
562
|
}
|
|
525
563
|
|
|
564
|
+
/** Outcome of materializing an external plugin tree into a directory. */
|
|
565
|
+
export interface MaterializedTree {
|
|
566
|
+
/** Number of regular files written into the destination. */
|
|
567
|
+
readonly fileCount: number;
|
|
568
|
+
/** Commit SHA the source was cloned at; `null` when it could not be read. */
|
|
569
|
+
readonly commit: string | null;
|
|
570
|
+
/** ISO-8601 committer timestamp of {@link MaterializedTree.commit}; `null` when unread. */
|
|
571
|
+
readonly committedAt: string | null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Produce the exact plugin tree an install stages, into `destDir`: clone the
|
|
576
|
+
* source at `source.ref`, then overlay the curated adapter stub (when one
|
|
577
|
+
* exists) so a foreign-ecosystem clone is translated into Vellum shape.
|
|
578
|
+
*
|
|
579
|
+
* `installPlugin` calls this and then fingerprints, records provenance, and
|
|
580
|
+
* swaps the result into the workspace. `diffPlugin` (see {@link ./diff-plugin})
|
|
581
|
+
* calls it with the *recorded install commit* to reconstruct the install-time
|
|
582
|
+
* baseline. Routing both through one path guarantees a re-materialized commit
|
|
583
|
+
* is byte-identical to what the original install produced, so an install-time
|
|
584
|
+
* adapter transform never reads as local drift when diffing.
|
|
585
|
+
*/
|
|
586
|
+
export async function materializePluginTree(
|
|
587
|
+
opts: {
|
|
588
|
+
/** Source coordinates; `source.ref` selects the commit to clone. */
|
|
589
|
+
readonly source: PluginFetchSource;
|
|
590
|
+
/** Install name, used to locate the curated adapter stub. */
|
|
591
|
+
readonly name: string;
|
|
592
|
+
/** Ref the curated adapter stub is fetched at (the canonical repo ref). */
|
|
593
|
+
readonly stubRef: string;
|
|
594
|
+
/** Directory the tree is written into. */
|
|
595
|
+
readonly destDir: string;
|
|
596
|
+
},
|
|
597
|
+
deps: InstallPluginDeps,
|
|
598
|
+
): Promise<MaterializedTree> {
|
|
599
|
+
const cloned = await copyExternalViaGit(
|
|
600
|
+
opts.source,
|
|
601
|
+
opts.destDir,
|
|
602
|
+
deps.runGit ?? defaultGitRunner,
|
|
603
|
+
);
|
|
604
|
+
// An external clone is often a foreign-ecosystem plugin (e.g. a Claude Code
|
|
605
|
+
// plugin) that the Vellum loader can't run as-is. When we curate an adapter
|
|
606
|
+
// stub for it, overlay the stub and run its transform so the materialized
|
|
607
|
+
// tree is a valid Vellum plugin. Raw clones (no stub) are left untouched.
|
|
608
|
+
if (cloned.fileCount > 0) {
|
|
609
|
+
await applyAdapterStub(opts.name, opts.stubRef, opts.destDir, deps);
|
|
610
|
+
}
|
|
611
|
+
return cloned;
|
|
612
|
+
}
|
|
613
|
+
|
|
526
614
|
/**
|
|
527
615
|
* Materialize an external plugin by shallow-cloning its repo at the pinned ref.
|
|
528
616
|
*
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge of a plugin tree, used by `plugins upgrade --strategy` to
|
|
3
|
+
* carry local edits forward across an upgrade instead of discarding them.
|
|
4
|
+
*
|
|
5
|
+
* An upgrade has three inputs, exactly like a git merge:
|
|
6
|
+
* - **base** — the tree the plugin was installed at (the recorded commit,
|
|
7
|
+
* re-materialized through the install pipeline; see {@link ./diff-plugin}).
|
|
8
|
+
* - **ours** — the current on-disk install, carrying any local edits.
|
|
9
|
+
* - **theirs** — the marketplace's current pin, the tree being upgraded to.
|
|
10
|
+
*
|
|
11
|
+
* Per file the merge is delegated to `git merge-file`, the same line-level
|
|
12
|
+
* three-way algorithm git itself uses: a hunk edited on only one side is taken
|
|
13
|
+
* from that side, so non-conflicting edits from *both* sides survive. Only a
|
|
14
|
+
* hunk edited differently on both sides is a true conflict, resolved per the
|
|
15
|
+
* caller's strategy:
|
|
16
|
+
* - `ours` keeps the local hunk (`git merge-file --ours`),
|
|
17
|
+
* - `theirs` keeps the pinned hunk (`git merge-file --theirs`),
|
|
18
|
+
* - `assistant` writes standard git conflict markers into the file (no resolve
|
|
19
|
+
* flag) and reports the path, leaving the conflict for the assistant to
|
|
20
|
+
* resolve — exactly the mid-merge working-tree UX.
|
|
21
|
+
*
|
|
22
|
+
* File-level add/delete divergence (a file added, removed, or modified on only
|
|
23
|
+
* one side) is resolved here before any line merge, since `git merge-file`
|
|
24
|
+
* operates on three existing blobs.
|
|
25
|
+
*
|
|
26
|
+
* Binary files cannot be line-merged, so a binary file that diverged on both
|
|
27
|
+
* sides is resolved whole-file by the strategy (`ours`/`theirs`); under
|
|
28
|
+
* `assistant` the local copy is kept and the path is reported as a binary
|
|
29
|
+
* conflict, since markers cannot be written into binary content.
|
|
30
|
+
*
|
|
31
|
+
* The `overwrite` strategy never reaches here — it discards local edits and is
|
|
32
|
+
* a plain re-install at the pin.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { execFile } from "node:child_process";
|
|
36
|
+
import {
|
|
37
|
+
mkdirSync,
|
|
38
|
+
mkdtempSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
rmSync,
|
|
41
|
+
writeFileSync,
|
|
42
|
+
} from "node:fs";
|
|
43
|
+
import { tmpdir } from "node:os";
|
|
44
|
+
import { dirname, join } from "node:path";
|
|
45
|
+
import { promisify } from "node:util";
|
|
46
|
+
|
|
47
|
+
import { INSTALL_META_FILENAME } from "./install-from-github.js";
|
|
48
|
+
import { computeFingerprint } from "./plugin-fingerprint.js";
|
|
49
|
+
|
|
50
|
+
const execFileAsync = promisify(execFile);
|
|
51
|
+
|
|
52
|
+
/** Cap on a single `git merge-file`; a per-file line merge is near-instant. */
|
|
53
|
+
const MERGE_TIMEOUT_MS = 30_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How hunks edited differently on both sides are reconciled. `ours`/`theirs`
|
|
57
|
+
* auto-resolve toward that side; `assistant` writes conflict markers and
|
|
58
|
+
* reports the conflict for later resolution. `overwrite` is not a merge
|
|
59
|
+
* strategy — the caller re-installs the pin wholesale instead of merging.
|
|
60
|
+
*/
|
|
61
|
+
export type PluginMergeStrategy = "ours" | "theirs" | "assistant";
|
|
62
|
+
|
|
63
|
+
/** Human-readable labels for the conflict markers `assistant` writes. */
|
|
64
|
+
export interface ConflictLabels {
|
|
65
|
+
readonly ours: string;
|
|
66
|
+
readonly base: string;
|
|
67
|
+
readonly theirs: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const DEFAULT_CONFLICT_LABELS: ConflictLabels = {
|
|
71
|
+
ours: "local edits",
|
|
72
|
+
base: "install baseline",
|
|
73
|
+
theirs: "upgrade pin",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Inputs for a three-way plugin-tree merge. */
|
|
77
|
+
export interface MergePluginTreeOptions {
|
|
78
|
+
/** Re-materialized install-commit tree (the merge base). */
|
|
79
|
+
readonly baseDir: string;
|
|
80
|
+
/** Current on-disk install, carrying local edits (`ours`). */
|
|
81
|
+
readonly oursDir: string;
|
|
82
|
+
/** Marketplace-pinned tree being upgraded to (`theirs`). */
|
|
83
|
+
readonly theirsDir: string;
|
|
84
|
+
/** Empty directory the merged tree is written into. */
|
|
85
|
+
readonly destDir: string;
|
|
86
|
+
/** How to resolve hunks edited differently on both sides. */
|
|
87
|
+
readonly strategy: PluginMergeStrategy;
|
|
88
|
+
/** Marker labels for the `assistant` strategy. Defaults are used when omitted. */
|
|
89
|
+
readonly conflictLabels?: ConflictLabels;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Outcome of a three-way plugin-tree merge. */
|
|
93
|
+
export interface PluginMergeResult {
|
|
94
|
+
/** Number of files written into the destination tree. */
|
|
95
|
+
readonly fileCount: number;
|
|
96
|
+
/**
|
|
97
|
+
* Paths (relative to the tree root) left for the assistant to resolve.
|
|
98
|
+
* Text files carry git conflict markers; modify/delete divergences keep the
|
|
99
|
+
* surviving content. Empty for `ours`/`theirs`, which auto-resolve.
|
|
100
|
+
*/
|
|
101
|
+
readonly conflicts: readonly string[];
|
|
102
|
+
/**
|
|
103
|
+
* Paths of binary files that diverged on both sides. The local copy is kept
|
|
104
|
+
* (markers cannot be written into binary content), so the assistant must
|
|
105
|
+
* choose a version rather than edit markers. Empty for `ours`/`theirs`.
|
|
106
|
+
*/
|
|
107
|
+
readonly binaryConflicts: readonly string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Result of merging a single file. */
|
|
111
|
+
interface MergedFile {
|
|
112
|
+
readonly content: Buffer;
|
|
113
|
+
/** Conflict markers were written into `content` (text, `assistant` only). */
|
|
114
|
+
readonly conflicted: boolean;
|
|
115
|
+
/** A binary file conflicted; `content` is the kept local copy (`assistant`). */
|
|
116
|
+
readonly binaryConflicted: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
|
|
120
|
+
function isBinary(buf: Buffer): boolean {
|
|
121
|
+
const len = Math.min(buf.length, 8000);
|
|
122
|
+
for (let i = 0; i < len; i++) {
|
|
123
|
+
if (buf[i] === 0) return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Write `content` to `destDir/rel`, creating parent directories as needed. */
|
|
129
|
+
function writeInto(destDir: string, rel: string, content: Buffer): void {
|
|
130
|
+
const abs = join(destDir, rel);
|
|
131
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
132
|
+
writeFileSync(abs, content);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Line-merge three blobs with `git merge-file`. Non-conflicting hunks from both
|
|
137
|
+
* sides are always kept. `ours`/`theirs` auto-resolve conflicting hunks toward
|
|
138
|
+
* that side (no markers); `assistant` writes git conflict markers and flags the
|
|
139
|
+
* file as conflicted.
|
|
140
|
+
*
|
|
141
|
+
* Binary input cannot be line-merged: a side that matches the base did not
|
|
142
|
+
* change, so the other side's edit is taken. A binary blob changed on *both*
|
|
143
|
+
* sides is a true conflict — resolved whole-file by `ours`/`theirs`, or kept as
|
|
144
|
+
* the local copy and flagged under `assistant` (no markers possible).
|
|
145
|
+
*/
|
|
146
|
+
async function threeWayMergeFile(
|
|
147
|
+
ours: Buffer,
|
|
148
|
+
base: Buffer,
|
|
149
|
+
theirs: Buffer,
|
|
150
|
+
strategy: PluginMergeStrategy,
|
|
151
|
+
labels: ConflictLabels,
|
|
152
|
+
): Promise<MergedFile> {
|
|
153
|
+
if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
|
|
154
|
+
if (ours.equals(base))
|
|
155
|
+
return { content: theirs, conflicted: false, binaryConflicted: false };
|
|
156
|
+
if (theirs.equals(base))
|
|
157
|
+
return { content: ours, conflicted: false, binaryConflicted: false };
|
|
158
|
+
if (strategy === "assistant") {
|
|
159
|
+
return { content: ours, conflicted: false, binaryConflicted: true };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: strategy === "ours" ? ours : theirs,
|
|
163
|
+
conflicted: false,
|
|
164
|
+
binaryConflicted: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const scratch = mkdtempSync(join(tmpdir(), "plugin-merge-file-"));
|
|
169
|
+
try {
|
|
170
|
+
const oursPath = join(scratch, "ours");
|
|
171
|
+
const basePath = join(scratch, "base");
|
|
172
|
+
const theirsPath = join(scratch, "theirs");
|
|
173
|
+
writeFileSync(oursPath, ours);
|
|
174
|
+
writeFileSync(basePath, base);
|
|
175
|
+
writeFileSync(theirsPath, theirs);
|
|
176
|
+
|
|
177
|
+
// `-p` prints the merged result to stdout instead of editing `ours` in
|
|
178
|
+
// place. `--ours`/`--theirs` auto-resolve conflicting hunks toward that
|
|
179
|
+
// side; with neither (the `assistant` strategy) git writes conflict
|
|
180
|
+
// markers, labelled via `-L` so the assistant can tell the sides apart.
|
|
181
|
+
const args = ["merge-file", "-p"];
|
|
182
|
+
if (strategy === "ours" || strategy === "theirs") {
|
|
183
|
+
args.push(`--${strategy}`);
|
|
184
|
+
} else {
|
|
185
|
+
args.push("-L", labels.ours, "-L", labels.base, "-L", labels.theirs);
|
|
186
|
+
}
|
|
187
|
+
args.push(oursPath, basePath, theirsPath);
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
190
|
+
cwd: scratch,
|
|
191
|
+
encoding: "buffer",
|
|
192
|
+
timeout: MERGE_TIMEOUT_MS,
|
|
193
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
194
|
+
});
|
|
195
|
+
return { content: stdout, conflicted: false, binaryConflicted: false };
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// `git merge-file` exits with the (positive) conflict count when markers
|
|
198
|
+
// were left, still printing the full merged bytes to stdout — that is the
|
|
199
|
+
// only rejection we may install. A killed process (timeout) or a
|
|
200
|
+
// `maxBuffer` overflow also rejects here, but with truncated/partial
|
|
201
|
+
// stdout we must NOT install it, or a large conflicted file would be
|
|
202
|
+
// silently corrupted; surface those as errors. A resolving flag never
|
|
203
|
+
// leaves conflicts, so any non-zero exit there is likewise a real
|
|
204
|
+
// failure.
|
|
205
|
+
const e = err as {
|
|
206
|
+
stdout?: Buffer;
|
|
207
|
+
code?: unknown;
|
|
208
|
+
killed?: boolean;
|
|
209
|
+
signal?: string | null;
|
|
210
|
+
};
|
|
211
|
+
const isConflictExit =
|
|
212
|
+
strategy === "assistant" &&
|
|
213
|
+
e.killed !== true &&
|
|
214
|
+
e.signal == null &&
|
|
215
|
+
typeof e.code === "number" &&
|
|
216
|
+
e.code > 0;
|
|
217
|
+
if (isConflictExit && Buffer.isBuffer(e.stdout)) {
|
|
218
|
+
return { content: e.stdout, conflicted: true, binaryConflicted: false };
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
} finally {
|
|
223
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Three-way merge `oursDir`/`theirsDir` against `baseDir` into `destDir` per
|
|
229
|
+
* `strategy`. The provenance sidecar is excluded on every side — it is
|
|
230
|
+
* rewritten by the caller after the swap and must not be carried through the
|
|
231
|
+
* merge.
|
|
232
|
+
*
|
|
233
|
+
* Returns the file count plus, for the `assistant` strategy, the paths left for
|
|
234
|
+
* the assistant to resolve (text files with conflict markers and modify/delete
|
|
235
|
+
* divergences) and the binary files that conflicted.
|
|
236
|
+
*/
|
|
237
|
+
export async function mergePluginTree({
|
|
238
|
+
baseDir,
|
|
239
|
+
oursDir,
|
|
240
|
+
theirsDir,
|
|
241
|
+
destDir,
|
|
242
|
+
strategy,
|
|
243
|
+
conflictLabels,
|
|
244
|
+
}: MergePluginTreeOptions): Promise<PluginMergeResult> {
|
|
245
|
+
const labels = conflictLabels ?? DEFAULT_CONFLICT_LABELS;
|
|
246
|
+
const exclude = [INSTALL_META_FILENAME];
|
|
247
|
+
const base = computeFingerprint(baseDir, exclude).files;
|
|
248
|
+
const ours = computeFingerprint(oursDir, exclude).files;
|
|
249
|
+
const theirs = computeFingerprint(theirsDir, exclude).files;
|
|
250
|
+
|
|
251
|
+
const paths = new Set([
|
|
252
|
+
...Object.keys(base),
|
|
253
|
+
...Object.keys(ours),
|
|
254
|
+
...Object.keys(theirs),
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const readBase = (rel: string) => readFileSync(join(baseDir, rel));
|
|
258
|
+
const readOurs = (rel: string) => readFileSync(join(oursDir, rel));
|
|
259
|
+
const readTheirs = (rel: string) => readFileSync(join(theirsDir, rel));
|
|
260
|
+
|
|
261
|
+
let fileCount = 0;
|
|
262
|
+
const conflicts: string[] = [];
|
|
263
|
+
const binaryConflicts: string[] = [];
|
|
264
|
+
const keep = (rel: string, content: Buffer): void => {
|
|
265
|
+
writeInto(destDir, rel, content);
|
|
266
|
+
fileCount++;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
for (const rel of paths) {
|
|
270
|
+
const b = base[rel];
|
|
271
|
+
const o = ours[rel];
|
|
272
|
+
const t = theirs[rel];
|
|
273
|
+
|
|
274
|
+
if (o !== undefined && t !== undefined) {
|
|
275
|
+
// Present on both sides: identical needs no merge; otherwise line-merge
|
|
276
|
+
// (an empty base when the file was added on both sides).
|
|
277
|
+
if (o === t) {
|
|
278
|
+
keep(rel, readOurs(rel));
|
|
279
|
+
} else {
|
|
280
|
+
const merged = await threeWayMergeFile(
|
|
281
|
+
readOurs(rel),
|
|
282
|
+
b !== undefined ? readBase(rel) : Buffer.alloc(0),
|
|
283
|
+
readTheirs(rel),
|
|
284
|
+
strategy,
|
|
285
|
+
labels,
|
|
286
|
+
);
|
|
287
|
+
keep(rel, merged.content);
|
|
288
|
+
if (merged.conflicted) conflicts.push(rel);
|
|
289
|
+
if (merged.binaryConflicted) binaryConflicts.push(rel);
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (o !== undefined) {
|
|
295
|
+
// Present only locally. A local-only addition (no base) always survives.
|
|
296
|
+
// A file deleted upstream is a delete: drop it when unchanged locally.
|
|
297
|
+
// On a modify/delete conflict `ours` keeps the edit and `theirs` honors
|
|
298
|
+
// the deletion; `assistant` keeps the edit and flags it (a whole-file
|
|
299
|
+
// conflict markers can't express).
|
|
300
|
+
if (b === undefined || (b !== o && strategy !== "theirs")) {
|
|
301
|
+
keep(rel, readOurs(rel));
|
|
302
|
+
if (b !== undefined && b !== o && strategy === "assistant") {
|
|
303
|
+
conflicts.push(rel);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (t !== undefined) {
|
|
310
|
+
// Present only at the pin. A remote-only addition (no base) always lands.
|
|
311
|
+
// A file deleted locally is a delete: keep upstream's removal when the
|
|
312
|
+
// pin left it unchanged. On a delete/modify conflict `theirs` keeps the
|
|
313
|
+
// pin's edit and `ours` honors the local deletion; `assistant` keeps the
|
|
314
|
+
// pin's edit and flags it.
|
|
315
|
+
if (b === undefined || (b !== t && strategy !== "ours")) {
|
|
316
|
+
keep(rel, readTheirs(rel));
|
|
317
|
+
if (b !== undefined && b !== t && strategy === "assistant") {
|
|
318
|
+
conflicts.push(rel);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Present only in the base: removed on both sides, so it stays removed.
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { fileCount, conflicts, binaryConflicts };
|
|
328
|
+
}
|
|
@@ -120,6 +120,20 @@ export function compareFingerprint(
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Whether two fingerprints cover the same files with identical digests. Used to
|
|
125
|
+
* confirm a re-materialized tree faithfully reproduces a recorded baseline
|
|
126
|
+
* before it is trusted as a merge base.
|
|
127
|
+
*/
|
|
128
|
+
export function fingerprintsEqual(a: Fingerprint, b: Fingerprint): boolean {
|
|
129
|
+
const aKeys = Object.keys(a.files);
|
|
130
|
+
if (aKeys.length !== Object.keys(b.files).length) return false;
|
|
131
|
+
for (const key of aKeys) {
|
|
132
|
+
if (a.files[key] !== b.files[key]) return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
123
137
|
/**
|
|
124
138
|
* Parse a fingerprint from already-decoded JSON. Lenient by design — any shape
|
|
125
139
|
* problem yields `null` so an older or hand-edited sidecar simply reports "no
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect the surfaces an installed plugin contributes to the running assistant,
|
|
3
|
+
* read directly from its on-disk tree.
|
|
4
|
+
*
|
|
5
|
+
* The host discovers a plugin's contributions from fixed directory conventions:
|
|
6
|
+
*
|
|
7
|
+
* - `hooks/<name>.{ts,js}` → a lifecycle hook keyed by the file basename
|
|
8
|
+
* (see the external plugin loader's `loadHooks`).
|
|
9
|
+
* - `tools/<name>.{ts,js}` → a tool, also keyed by the file basename
|
|
10
|
+
* (see the external plugin loader's tool walk).
|
|
11
|
+
* - `skills/<id>/SKILL.md` → a skill owned by the plugin (see the skills
|
|
12
|
+
* catalog's `discoverPluginResidentSkills`).
|
|
13
|
+
*
|
|
14
|
+
* This module re-derives those same sets so `plugins inspect` can report exactly
|
|
15
|
+
* what a plugin contributes. Detection is intentionally a self-contained walk of
|
|
16
|
+
* the install tree — `cli/lib` does not reach into the daemon-internal loader or
|
|
17
|
+
* skills catalog — but it mirrors their conventions so inspect agrees with what
|
|
18
|
+
* the runtime actually loads.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
/** The surfaces an installed plugin contributes, each sorted and de-duplicated. */
|
|
25
|
+
export interface PluginSurfaces {
|
|
26
|
+
/** Skill ids shipped at `skills/<id>/SKILL.md`. */
|
|
27
|
+
readonly skills: readonly string[];
|
|
28
|
+
/** Lifecycle hook names from `hooks/<name>.{ts,js}` (e.g. `pre-model-call`). */
|
|
29
|
+
readonly hooks: readonly string[];
|
|
30
|
+
/**
|
|
31
|
+
* Registered tool names from `tools/<name>.{ts,js}`. The loader derives a
|
|
32
|
+
* tool's name from its filename via {@link deriveToolName} (e.g.
|
|
33
|
+
* `create-issue.ts` registers as `create_issue`), so the derived form is
|
|
34
|
+
* reported rather than the raw basename. A tool module that overrides its own
|
|
35
|
+
* name via an exported `name` is not reflected here: that would require
|
|
36
|
+
* importing and executing untrusted plugin code, which inspection avoids.
|
|
37
|
+
*/
|
|
38
|
+
readonly tools: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Derive a tool's registered name from its file basename, mirroring the
|
|
43
|
+
* external plugin loader's `deriveToolName`: non-alphanumeric runs collapse to
|
|
44
|
+
* `_`, leading/trailing `_` are trimmed, and an empty result falls back to
|
|
45
|
+
* `tool`. Keeps the inspected tool name aligned with the callable tool name.
|
|
46
|
+
*/
|
|
47
|
+
function deriveToolName(basename: string): string {
|
|
48
|
+
return (
|
|
49
|
+
basename.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "tool"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* List the basenames of every `.ts`/`.js` module directly under `dir`,
|
|
55
|
+
* preferring `.js` over `.ts` for the same basename (compiled-binary semantics)
|
|
56
|
+
* and skipping `.d.ts` declaration files. Returns names sorted for a
|
|
57
|
+
* deterministic listing. A missing or non-directory path yields `[]`.
|
|
58
|
+
*
|
|
59
|
+
* Mirrors the external plugin loader's `listSurfaceDir`, the gate it uses to
|
|
60
|
+
* turn a `hooks/`/`tools/` directory into loadable surfaces.
|
|
61
|
+
*/
|
|
62
|
+
function listModuleBasenames(dir: string): string[] {
|
|
63
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
|
|
64
|
+
const bases = new Set<string>();
|
|
65
|
+
for (const entry of readdirSync(dir)) {
|
|
66
|
+
if (entry.endsWith(".d.ts")) continue;
|
|
67
|
+
if (!entry.endsWith(".ts") && !entry.endsWith(".js")) continue;
|
|
68
|
+
bases.add(entry.slice(0, -3));
|
|
69
|
+
}
|
|
70
|
+
return [...bases].sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List the skill ids a plugin ships: each subdirectory of `skills/` that
|
|
75
|
+
* contains a `SKILL.md`. Mirrors the skills catalog's plugin-resident skill
|
|
76
|
+
* discovery so inspect reports the same set the runtime would surface.
|
|
77
|
+
*/
|
|
78
|
+
function listSkillIds(skillsDir: string): string[] {
|
|
79
|
+
if (!existsSync(skillsDir) || !statSync(skillsDir).isDirectory()) return [];
|
|
80
|
+
const ids: string[] = [];
|
|
81
|
+
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
83
|
+
if (existsSync(join(skillsDir, entry.name, "SKILL.md"))) {
|
|
84
|
+
ids.push(entry.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return ids.sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Detect the {@link PluginSurfaces} an installed plugin contributes by walking
|
|
92
|
+
* its install tree at `pluginDir`. Surface types with no contributions come
|
|
93
|
+
* back as empty arrays; callers omit empty types from the rendered output.
|
|
94
|
+
*/
|
|
95
|
+
export function detectPluginSurfaces(pluginDir: string): PluginSurfaces {
|
|
96
|
+
const toolNames = listModuleBasenames(join(pluginDir, "tools")).map(
|
|
97
|
+
deriveToolName,
|
|
98
|
+
);
|
|
99
|
+
return {
|
|
100
|
+
skills: listSkillIds(join(pluginDir, "skills")),
|
|
101
|
+
hooks: listModuleBasenames(join(pluginDir, "hooks")),
|
|
102
|
+
tools: [...new Set(toolNames)].sort(),
|
|
103
|
+
};
|
|
104
|
+
}
|