@vellumai/assistant 0.4.48 → 0.4.50
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 +26 -35
- package/README.md +5 -26
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +2 -2
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +249 -2
- package/src/__tests__/approval-cascade.test.ts +796 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +15 -6
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +13 -20
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +152 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
- package/src/__tests__/credential-metadata-store.test.ts +64 -73
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +13 -7
- package/src/__tests__/credential-vault-unit.test.ts +284 -49
- package/src/__tests__/credential-vault.test.ts +150 -16
- package/src/__tests__/credentials-cli.test.ts +71 -0
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +791 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +2 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +32 -51
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +14 -46
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +941 -15
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +870 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -3
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +24 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills-uninstall.test.ts +1 -1
- package/src/__tests__/skills.test.ts +3 -3
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +67 -3
- package/src/__tests__/slack-share-routes.test.ts +17 -19
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +7 -13
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-routes.test.ts +0 -16
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +286 -0
- package/src/agent/loop.ts +104 -131
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +133 -6
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +52 -18
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +3 -8
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +18 -12
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +101 -15
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +284 -0
- package/src/cli/commands/oauth/connections.ts +633 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +256 -0
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +177 -339
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +6 -11
- package/src/cli/reference.ts +1 -3
- package/src/cli.ts +4 -10
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +23 -5
- package/src/config/bundled-skills/computer-use/tools/{computer-use-request-control.ts → computer-use-observe.ts} +1 -5
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/loader.ts +0 -6
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/skills.ts +21 -2
- package/src/config/types.ts +0 -4
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +53 -11
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +49 -46
- package/src/daemon/handlers/config-telegram.ts +32 -16
- package/src/daemon/handlers/sessions.ts +27 -36
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +430 -0
- package/src/daemon/lifecycle.ts +67 -71
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +1 -129
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +4 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +25 -21
- package/src/daemon/session-agent-loop-handlers.ts +40 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +43 -28
- package/src/daemon/session-tool-setup.ts +9 -10
- package/src/daemon/session.ts +150 -17
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/instrument.ts +61 -1
- package/src/logfire.ts +16 -5
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +62 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +67 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +4 -4
- package/src/messaging/providers/gmail/client.ts +82 -2
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/gmail/people-client.ts +10 -10
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
- package/src/messaging/providers/whatsapp/adapter.ts +11 -8
- package/src/messaging/registry.ts +2 -32
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +133 -25
- package/src/oauth/byo-connection.ts +22 -6
- package/src/oauth/connect-orchestrator.ts +113 -57
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +35 -11
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +582 -0
- package/src/oauth/platform-connection.test.ts +29 -0
- package/src/oauth/platform-connection.ts +6 -5
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +167 -0
- package/src/oauth/token-persistence.ts +81 -77
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +1 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +36 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +46 -42
- package/src/providers/anthropic/client.ts +59 -20
- package/src/providers/retry.ts +1 -27
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -6
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +10 -8
- package/src/runtime/http-types.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/conversation-routes.ts +81 -19
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -7
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +55 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/schedule/integration-status.ts +10 -9
- package/src/security/credential-key.ts +0 -156
- package/src/security/keychain-broker-client.ts +22 -10
- package/src/security/oauth2.ts +1 -1
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +137 -64
- package/src/skills/catalog-install.ts +414 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/computer-use/definitions.ts +36 -11
- package/src/tools/computer-use/registry.ts +5 -6
- package/src/tools/credentials/broker.ts +1 -2
- package/src/tools/credentials/metadata-store.ts +17 -121
- package/src/tools/credentials/vault.ts +92 -167
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/registry.ts +2 -7
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +85 -3
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/watcher/providers/google-calendar.ts +2 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/computer-use-session.ts +0 -1026
- package/src/daemon/ride-shotgun-handler.ts +0 -569
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/oauth/provider-base-urls.ts +0 -21
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/routes/mcp-routes.ts +0 -20
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
- package/src/runtime/telegram-streaming-delivery.ts +0 -393
- package/src/tools/computer-use/request-computer-control.ts +0 -56
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
14
|
+
import { gunzipSync } from "node:zlib";
|
|
15
|
+
|
|
16
|
+
import { getLogger } from "../util/logger.js";
|
|
17
|
+
import {
|
|
18
|
+
getWorkspaceConfigPath,
|
|
19
|
+
getWorkspaceSkillsDir,
|
|
20
|
+
readPlatformToken,
|
|
21
|
+
} from "../util/platform.js";
|
|
22
|
+
|
|
23
|
+
const log = getLogger("catalog-install");
|
|
24
|
+
|
|
25
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface CatalogSkill {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
emoji?: string;
|
|
32
|
+
includes?: string[];
|
|
33
|
+
version?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CatalogManifest {
|
|
37
|
+
version: number;
|
|
38
|
+
skills: CatalogSkill[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export function getSkillsIndexPath(): string {
|
|
44
|
+
return join(getWorkspaceSkillsDir(), "SKILLS.md");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the repo-level skills/ directory when running in dev mode.
|
|
49
|
+
* Returns the path if VELLUM_DEV is set and the directory exists, or undefined.
|
|
50
|
+
*/
|
|
51
|
+
export function getRepoSkillsDir(): string | undefined {
|
|
52
|
+
if (!process.env.VELLUM_DEV) return undefined;
|
|
53
|
+
|
|
54
|
+
// assistant/src/skills/catalog-install.ts -> ../../../skills/
|
|
55
|
+
const candidate = join(import.meta.dir, "..", "..", "..", "skills");
|
|
56
|
+
if (existsSync(join(candidate, "catalog.json"))) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Platform API ────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function getConfigPlatformUrl(): string | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const configPath = getWorkspaceConfigPath();
|
|
67
|
+
if (!existsSync(configPath)) return undefined;
|
|
68
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8")) as Record<
|
|
69
|
+
string,
|
|
70
|
+
unknown
|
|
71
|
+
>;
|
|
72
|
+
const platform = raw.platform as Record<string, unknown> | undefined;
|
|
73
|
+
const baseUrl = platform?.baseUrl;
|
|
74
|
+
if (typeof baseUrl === "string" && baseUrl.trim()) return baseUrl.trim();
|
|
75
|
+
} catch {
|
|
76
|
+
// ignore
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getPlatformUrl(): string {
|
|
82
|
+
return (
|
|
83
|
+
process.env.VELLUM_PLATFORM_URL ??
|
|
84
|
+
getConfigPlatformUrl() ??
|
|
85
|
+
"https://platform.vellum.ai"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildHeaders(): Record<string, string> {
|
|
90
|
+
const headers: Record<string, string> = {};
|
|
91
|
+
const token = readPlatformToken();
|
|
92
|
+
if (token) {
|
|
93
|
+
headers["X-Session-Token"] = token;
|
|
94
|
+
}
|
|
95
|
+
return headers;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Catalog operations ──────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export async function fetchCatalog(): Promise<CatalogSkill[]> {
|
|
101
|
+
const url = `${getPlatformUrl()}/v1/skills/`;
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
headers: buildHeaders(),
|
|
104
|
+
signal: AbortSignal.timeout(10000),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Platform API error ${response.status}: ${response.statusText}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const manifest = (await response.json()) as CatalogManifest;
|
|
114
|
+
if (!Array.isArray(manifest.skills)) {
|
|
115
|
+
throw new Error("Platform catalog has invalid skills array");
|
|
116
|
+
}
|
|
117
|
+
return manifest.skills;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
|
|
121
|
+
try {
|
|
122
|
+
const raw = readFileSync(join(repoSkillsDir, "catalog.json"), "utf-8");
|
|
123
|
+
const manifest = JSON.parse(raw) as CatalogManifest;
|
|
124
|
+
if (!Array.isArray(manifest.skills)) return [];
|
|
125
|
+
return manifest.skills;
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Tar extraction ──────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract all files from a tar archive (uncompressed) into a directory.
|
|
135
|
+
* Returns true if a SKILL.md was found in the archive.
|
|
136
|
+
*/
|
|
137
|
+
export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
|
|
138
|
+
let foundSkillMd = false;
|
|
139
|
+
let offset = 0;
|
|
140
|
+
while (offset + 512 <= tarBuffer.length) {
|
|
141
|
+
const header = tarBuffer.subarray(offset, offset + 512);
|
|
142
|
+
|
|
143
|
+
// End-of-archive (two consecutive zero blocks)
|
|
144
|
+
if (header.every((b) => b === 0)) break;
|
|
145
|
+
|
|
146
|
+
// Filename (bytes 0-99, null-terminated)
|
|
147
|
+
const nameEnd = header.indexOf(0, 0);
|
|
148
|
+
const name = header
|
|
149
|
+
.subarray(0, Math.min(nameEnd >= 0 ? nameEnd : 100, 100))
|
|
150
|
+
.toString("utf-8");
|
|
151
|
+
|
|
152
|
+
// File type (byte 156): '5' = directory, '0' or '\0' = regular file
|
|
153
|
+
const typeFlag = header[156];
|
|
154
|
+
|
|
155
|
+
// File size (bytes 124-135, octal)
|
|
156
|
+
const sizeStr = header.subarray(124, 136).toString("utf-8").trim();
|
|
157
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
158
|
+
|
|
159
|
+
offset += 512; // past header
|
|
160
|
+
|
|
161
|
+
// Skip directories and empty names
|
|
162
|
+
if (name && typeFlag !== 53 /* '5' */) {
|
|
163
|
+
// Prevent path traversal and absolute path writes
|
|
164
|
+
const normalizedName = name.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
165
|
+
const normalizedPath = posix.normalize(normalizedName);
|
|
166
|
+
const hasWindowsDrivePrefix = /^[a-zA-Z]:\//.test(normalizedPath);
|
|
167
|
+
const isTraversal =
|
|
168
|
+
normalizedPath === ".." || normalizedPath.startsWith("../");
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
normalizedPath &&
|
|
172
|
+
normalizedPath !== "." &&
|
|
173
|
+
!normalizedPath.startsWith("/") &&
|
|
174
|
+
!hasWindowsDrivePrefix &&
|
|
175
|
+
!isTraversal
|
|
176
|
+
) {
|
|
177
|
+
const destRoot = resolve(destDir);
|
|
178
|
+
const destPath = resolve(destRoot, normalizedPath);
|
|
179
|
+
const insideDestination =
|
|
180
|
+
destPath === destRoot || destPath.startsWith(destRoot + sep);
|
|
181
|
+
if (!insideDestination) {
|
|
182
|
+
offset += Math.ceil(size / 512) * 512;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
187
|
+
writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
normalizedPath === "SKILL.md" ||
|
|
191
|
+
normalizedPath.endsWith("/SKILL.md")
|
|
192
|
+
) {
|
|
193
|
+
foundSkillMd = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip to next header (data padded to 512 bytes)
|
|
199
|
+
offset += Math.ceil(size / 512) * 512;
|
|
200
|
+
}
|
|
201
|
+
return foundSkillMd;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function fetchAndExtractSkill(
|
|
205
|
+
skillId: string,
|
|
206
|
+
destDir: string,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
209
|
+
const response = await fetch(url, {
|
|
210
|
+
headers: buildHeaders(),
|
|
211
|
+
signal: AbortSignal.timeout(15000),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Failed to fetch skill "${skillId}": HTTP ${response.status}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const gzipBuffer = Buffer.from(await response.arrayBuffer());
|
|
221
|
+
const tarBuffer = gunzipSync(gzipBuffer);
|
|
222
|
+
const foundSkillMd = extractTarToDir(tarBuffer, destDir);
|
|
223
|
+
|
|
224
|
+
if (!foundSkillMd) {
|
|
225
|
+
throw new Error(`SKILL.md not found in archive for "${skillId}"`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── SKILLS.md index management ──────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
function atomicWriteFile(filePath: string, content: string): void {
|
|
232
|
+
const dir = dirname(filePath);
|
|
233
|
+
mkdirSync(dir, { recursive: true });
|
|
234
|
+
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
235
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
236
|
+
renameSync(tmpPath, filePath);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function upsertSkillsIndex(id: string): void {
|
|
240
|
+
const indexPath = getSkillsIndexPath();
|
|
241
|
+
let lines: string[] = [];
|
|
242
|
+
if (existsSync(indexPath)) {
|
|
243
|
+
lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
247
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
248
|
+
if (lines.some((line) => pattern.test(line))) return;
|
|
249
|
+
|
|
250
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
251
|
+
nonEmpty.push(`- ${id}`);
|
|
252
|
+
const content = nonEmpty.join("\n");
|
|
253
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function removeSkillsIndexEntry(id: string): void {
|
|
257
|
+
const indexPath = getSkillsIndexPath();
|
|
258
|
+
if (!existsSync(indexPath)) return;
|
|
259
|
+
|
|
260
|
+
const lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
261
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
262
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
263
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
264
|
+
|
|
265
|
+
// If nothing changed, skip the write
|
|
266
|
+
if (filtered.length === lines.length) return;
|
|
267
|
+
|
|
268
|
+
const content = filtered.join("\n");
|
|
269
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── Install / uninstall ─────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export function uninstallSkillLocally(skillId: string): void {
|
|
275
|
+
const skillDir = join(getWorkspaceSkillsDir(), skillId);
|
|
276
|
+
|
|
277
|
+
if (!existsSync(skillDir)) {
|
|
278
|
+
throw new Error(`Skill "${skillId}" is not installed.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
282
|
+
removeSkillsIndexEntry(skillId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function installSkillLocally(
|
|
286
|
+
skillId: string,
|
|
287
|
+
catalogEntry: CatalogSkill,
|
|
288
|
+
overwrite: boolean,
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const skillDir = join(getWorkspaceSkillsDir(), skillId);
|
|
291
|
+
const skillFilePath = join(skillDir, "SKILL.md");
|
|
292
|
+
|
|
293
|
+
if (existsSync(skillFilePath) && !overwrite) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
mkdirSync(skillDir, { recursive: true });
|
|
300
|
+
|
|
301
|
+
// In dev mode, install from the local repo skills directory if available
|
|
302
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
303
|
+
const repoSkillSource = repoSkillsDir
|
|
304
|
+
? join(repoSkillsDir, skillId)
|
|
305
|
+
: undefined;
|
|
306
|
+
|
|
307
|
+
if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
|
|
308
|
+
cpSync(repoSkillSource, skillDir, { recursive: true });
|
|
309
|
+
} else {
|
|
310
|
+
await fetchAndExtractSkill(skillId, skillDir);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Write version metadata
|
|
314
|
+
if (catalogEntry.version) {
|
|
315
|
+
const meta = {
|
|
316
|
+
version: catalogEntry.version,
|
|
317
|
+
installedAt: new Date().toISOString(),
|
|
318
|
+
};
|
|
319
|
+
atomicWriteFile(
|
|
320
|
+
join(skillDir, "version.json"),
|
|
321
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Install npm dependencies if the skill has a package.json
|
|
326
|
+
if (existsSync(join(skillDir, "package.json"))) {
|
|
327
|
+
const bunPath = `${homedir()}/.bun/bin`;
|
|
328
|
+
execSync("bun install", {
|
|
329
|
+
cwd: skillDir,
|
|
330
|
+
stdio: "inherit",
|
|
331
|
+
env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Register in SKILLS.md only after all steps succeed
|
|
336
|
+
upsertSkillsIndex(skillId);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── Auto-install (for skill_load) ──────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Resolve the catalog skill list, checking local (dev mode) first, then remote.
|
|
343
|
+
*
|
|
344
|
+
* In dev mode with a local catalog, returns local entries immediately to avoid
|
|
345
|
+
* unnecessary network latency. Pass `skillId` to trigger a deferred remote
|
|
346
|
+
* fetch only when the requested skill is not found locally — this preserves the
|
|
347
|
+
* ability to discover remote-only skills without penalising every call with a
|
|
348
|
+
* 10s timeout on flaky networks.
|
|
349
|
+
*
|
|
350
|
+
* Callers that install multiple skills in a loop should call this once and pass
|
|
351
|
+
* the result to `autoInstallFromCatalog` to avoid redundant network requests.
|
|
352
|
+
*/
|
|
353
|
+
export async function resolveCatalog(
|
|
354
|
+
skillId?: string,
|
|
355
|
+
): Promise<CatalogSkill[]> {
|
|
356
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
357
|
+
if (repoSkillsDir) {
|
|
358
|
+
const local = readLocalCatalog(repoSkillsDir);
|
|
359
|
+
if (local.length > 0) {
|
|
360
|
+
// If no specific skill requested, or it exists locally, skip remote fetch
|
|
361
|
+
if (!skillId || local.some((s) => s.id === skillId)) {
|
|
362
|
+
return local;
|
|
363
|
+
}
|
|
364
|
+
// Skill not found locally — merge with remote so remote-only skills
|
|
365
|
+
// can still be discovered. Local entries take precedence by id.
|
|
366
|
+
try {
|
|
367
|
+
const remote = await fetchCatalog();
|
|
368
|
+
const localIds = new Set(local.map((s) => s.id));
|
|
369
|
+
return [...local, ...remote.filter((s) => !localIds.has(s.id))];
|
|
370
|
+
} catch {
|
|
371
|
+
return local;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return fetchCatalog();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Attempt to find and install a skill from the first-party catalog.
|
|
381
|
+
* Returns true if the skill was installed, false if not found in catalog.
|
|
382
|
+
* Throws on install failures (network, filesystem, etc).
|
|
383
|
+
*
|
|
384
|
+
* When `catalog` is provided it is used directly, avoiding a redundant
|
|
385
|
+
* network fetch — pass a pre-resolved catalog when calling in a loop.
|
|
386
|
+
*/
|
|
387
|
+
export async function autoInstallFromCatalog(
|
|
388
|
+
skillId: string,
|
|
389
|
+
catalog?: CatalogSkill[],
|
|
390
|
+
): Promise<boolean> {
|
|
391
|
+
let skills: CatalogSkill[];
|
|
392
|
+
|
|
393
|
+
if (catalog) {
|
|
394
|
+
skills = catalog;
|
|
395
|
+
} else {
|
|
396
|
+
try {
|
|
397
|
+
skills = await resolveCatalog(skillId);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
log.warn(
|
|
400
|
+
{ err, skillId },
|
|
401
|
+
"Failed to fetch remote catalog for auto-install",
|
|
402
|
+
);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const entry = skills.find((s) => s.id === skillId);
|
|
408
|
+
if (!entry) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await installSkillLocally(skillId, entry, false);
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
@@ -151,3 +151,35 @@ export function traverseIncludes(
|
|
|
151
151
|
dfs(rootId);
|
|
152
152
|
return { visited };
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Collect all missing skill IDs reachable from the root's include graph.
|
|
157
|
+
* DFS traversal that tracks visited nodes to prevent infinite loops on cycles.
|
|
158
|
+
* The root itself is never reported as missing (it's already loaded by the caller).
|
|
159
|
+
*/
|
|
160
|
+
export function collectAllMissing(
|
|
161
|
+
rootId: string,
|
|
162
|
+
catalogIndex: Map<string, SkillSummary>,
|
|
163
|
+
): Set<string> {
|
|
164
|
+
const missing = new Set<string>();
|
|
165
|
+
const visited = new Set<string>();
|
|
166
|
+
|
|
167
|
+
function dfs(id: string): void {
|
|
168
|
+
if (visited.has(id)) return;
|
|
169
|
+
visited.add(id);
|
|
170
|
+
|
|
171
|
+
const skill = catalogIndex.get(id);
|
|
172
|
+
if (!skill?.includes) return;
|
|
173
|
+
|
|
174
|
+
for (const childId of skill.includes) {
|
|
175
|
+
if (!catalogIndex.has(childId)) {
|
|
176
|
+
missing.add(childId);
|
|
177
|
+
} else if (!visited.has(childId)) {
|
|
178
|
+
dfs(childId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
dfs(rootId);
|
|
184
|
+
return missing;
|
|
185
|
+
}
|