@vellumai/assistant 0.4.46 → 0.4.49
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 +7 -7
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/architecture/security.md +5 -5
- 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 +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- 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-fill-credential.test.ts +5 -2
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +35 -25
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/cli.test.ts +23 -0
- 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-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-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 +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +239 -26
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +111 -7
- package/src/__tests__/credential-vault-unit.test.ts +287 -54
- package/src/__tests__/credential-vault.test.ts +406 -12
- package/src/__tests__/credentials-cli.test.ts +82 -6
- 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-enforcement.test.ts +4 -2
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -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-grant-minting.test.ts +35 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -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__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +38 -25
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- 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 +756 -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-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- 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-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- 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-uninstall.test.ts +3 -3
- package/src/__tests__/skills.test.ts +3 -12
- package/src/__tests__/slack-channel-config.test.ts +76 -11
- package/src/__tests__/slack-share-routes.test.ts +17 -14
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- 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 +1 -22
- 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-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -20
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +8 -10
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +4 -8
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -1
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +46 -15
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +110 -23
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/cli.ts +3 -2
- 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/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
- 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-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- 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/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +76 -56
- package/src/daemon/handlers/config-telegram.ts +53 -24
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +39 -63
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +20 -11
- package/src/daemon/session.ts +139 -16
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/providers/index.ts +2 -1
- 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 +15 -1
- package/src/logfire.ts +16 -5
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +26 -3
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/guardian.ts +1 -1
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +19 -13
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +283 -122
- package/src/messaging/providers/gmail/people-client.ts +32 -24
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
- package/src/messaging/providers/whatsapp/adapter.ts +17 -11
- package/src/messaging/registry.ts +2 -31
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +537 -0
- package/src/oauth/byo-connection.ts +128 -0
- package/src/oauth/connect-orchestrator.ts +139 -56
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +58 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +192 -0
- package/src/oauth/platform-connection.ts +111 -0
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +8 -4
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +70 -45
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -27
- package/src/runtime/AGENTS.md +17 -0
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +17 -10
- package/src/runtime/http-types.ts +2 -3
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -6
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +113 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -8
- package/src/security/credential-key.ts +14 -0
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +145 -43
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +5 -4
- package/src/tools/credentials/metadata-store.ts +22 -74
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +139 -151
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/registry.ts +2 -7
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +62 -8
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +0 -5
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- 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/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +94 -86
- package/src/watcher/providers/linear.ts +87 -93
- 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__/ride-shotgun-handler.test.ts +0 -452
- 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/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1020
- package/src/daemon/ride-shotgun-handler.ts +0 -567
- 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/telegram-streaming-delivery.test.ts +0 -597
- package/src/runtime/telegram-streaming-delivery.ts +0 -383
- package/src/tools/computer-use/request-computer-control.ts +0 -61
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Token manager for OAuth2 credentials.
|
|
3
3
|
*
|
|
4
|
-
* Reads refresh configuration (tokenUrl, clientId)
|
|
5
|
-
*
|
|
6
|
-
* refresh
|
|
4
|
+
* Reads refresh configuration (tokenUrl, clientId, authMethod) exclusively
|
|
5
|
+
* from the SQLite oauth-store (provider + app + connection rows). After a
|
|
6
|
+
* successful refresh, writes tokens to new-format secure key paths and
|
|
7
|
+
* updates the oauth_connection row.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
getApp,
|
|
12
|
+
getConnectionByProvider,
|
|
13
|
+
getProvider,
|
|
14
|
+
updateConnection,
|
|
15
|
+
} from "../oauth/oauth-store.js";
|
|
13
16
|
import { getLogger } from "../util/logger.js";
|
|
14
17
|
import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
|
|
15
18
|
import { getSecureKey, setSecureKeyAsync } from "./secure-keys.js";
|
|
@@ -106,11 +109,34 @@ function recordRefreshFailure(service: string): void {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
// ── Per-service refresh deduplication ─────────────────────────────────
|
|
113
|
+
// When multiple concurrent `withValidToken` calls detect an expired or
|
|
114
|
+
// 401-rejected token for the same service, only one actual refresh
|
|
115
|
+
// attempt is made. Other callers join the in-flight promise.
|
|
116
|
+
|
|
117
|
+
const inflightRefreshes = new Map<string, Promise<string>>();
|
|
118
|
+
|
|
119
|
+
function deduplicatedRefresh(service: string): Promise<string> {
|
|
120
|
+
const existing = inflightRefreshes.get(service);
|
|
121
|
+
if (existing) return existing;
|
|
122
|
+
|
|
123
|
+
const promise = doRefresh(service).finally(() => {
|
|
124
|
+
inflightRefreshes.delete(service);
|
|
125
|
+
});
|
|
126
|
+
inflightRefreshes.set(service, promise);
|
|
127
|
+
return promise;
|
|
128
|
+
}
|
|
129
|
+
|
|
109
130
|
/** @internal Test-only: reset all circuit breaker state */
|
|
110
131
|
export function _resetRefreshBreakers(): void {
|
|
111
132
|
refreshBreakers.clear();
|
|
112
133
|
}
|
|
113
134
|
|
|
135
|
+
/** @internal Test-only: reset in-flight refresh deduplication state */
|
|
136
|
+
export function _resetInflightRefreshes(): void {
|
|
137
|
+
inflightRefreshes.clear();
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
/** @internal Test-only: get breaker state for a service */
|
|
115
141
|
export function _getRefreshBreakerState(
|
|
116
142
|
service: string,
|
|
@@ -132,54 +158,113 @@ export class TokenExpiredError extends Error {
|
|
|
132
158
|
|
|
133
159
|
/**
|
|
134
160
|
* Check whether the access token for a service is expired or will expire
|
|
135
|
-
* within the buffer window, based on the `expiresAt` field in
|
|
161
|
+
* within the buffer window, based on the `expiresAt` field in the
|
|
162
|
+
* oauth_connection row.
|
|
136
163
|
*/
|
|
137
164
|
function isTokenExpired(service: string): boolean {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
try {
|
|
166
|
+
const conn = getConnectionByProvider(service);
|
|
167
|
+
if (!conn?.expiresAt) return false;
|
|
168
|
+
return Date.now() >= conn.expiresAt - EXPIRY_BUFFER_MS;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Refresh config resolution ─────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
/** Shared shape for resolved refresh configuration. */
|
|
177
|
+
interface RefreshConfig {
|
|
178
|
+
tokenUrl: string;
|
|
179
|
+
clientId: string;
|
|
180
|
+
/** OAuth client secret (optional — PKCE flows may omit it). */
|
|
181
|
+
secret?: string;
|
|
182
|
+
refreshToken?: string;
|
|
183
|
+
authMethod?: TokenEndpointAuthMethod;
|
|
184
|
+
connId: string;
|
|
141
185
|
}
|
|
142
186
|
|
|
143
187
|
/**
|
|
144
|
-
*
|
|
145
|
-
* refresh token and OAuth2 config stored in credential metadata.
|
|
188
|
+
* Resolve refresh configuration from the SQLite oauth-store.
|
|
146
189
|
*
|
|
147
|
-
*
|
|
148
|
-
* Throws `TokenExpiredError` if
|
|
190
|
+
* Looks up connection -> app -> provider to read tokenUrl, clientId, and
|
|
191
|
+
* authMethod. Throws `TokenExpiredError` if the connection is not found
|
|
192
|
+
* or incomplete.
|
|
149
193
|
*/
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
194
|
+
function resolveRefreshConfig(service: string): RefreshConfig {
|
|
195
|
+
const conn = getConnectionByProvider(service);
|
|
196
|
+
if (!conn) {
|
|
153
197
|
throw new TokenExpiredError(
|
|
154
198
|
service,
|
|
155
|
-
`No
|
|
199
|
+
`No OAuth connection found for "${service}". Re-authorization required.${recoveryHint(service)}`,
|
|
156
200
|
);
|
|
157
201
|
}
|
|
158
202
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
203
|
+
const app = getApp(conn.oauthAppId);
|
|
204
|
+
if (!app) {
|
|
205
|
+
throw new TokenExpiredError(
|
|
206
|
+
service,
|
|
207
|
+
`No OAuth app found for "${service}". Re-authorization required.${recoveryHint(service)}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const provider = getProvider(conn.providerKey);
|
|
212
|
+
if (!provider) {
|
|
213
|
+
throw new TokenExpiredError(
|
|
214
|
+
service,
|
|
215
|
+
`No OAuth provider found for "${service}". Re-authorization required.${recoveryHint(service)}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
162
218
|
|
|
219
|
+
const tokenUrl = provider.tokenUrl;
|
|
220
|
+
const clientId = app.clientId;
|
|
163
221
|
if (!tokenUrl || !clientId) {
|
|
164
|
-
// Legacy credentials created by the old integration flow don't store
|
|
165
|
-
// oauth2TokenUrl/oauth2ClientId in metadata. The client ID is user-specific
|
|
166
|
-
// (from their Google Cloud Console) and cannot be recovered, so the only
|
|
167
|
-
// path forward is re-authorization via the new oauth2_connect flow.
|
|
168
|
-
const isLegacy = service === "integration:gmail" && !tokenUrl && !clientId;
|
|
169
|
-
const hint = isLegacy
|
|
170
|
-
? ` This is a one-time migration: your old Gmail connection needs to be re-authorized. Ask me to "reconnect Gmail" to set it up again.`
|
|
171
|
-
: "";
|
|
172
222
|
throw new TokenExpiredError(
|
|
173
223
|
service,
|
|
174
|
-
`Missing OAuth2 refresh config for "${service}".${
|
|
224
|
+
`Missing OAuth2 refresh config for "${service}".${recoveryHint(service)}`,
|
|
175
225
|
);
|
|
176
226
|
}
|
|
177
227
|
|
|
178
|
-
const
|
|
179
|
-
|
|
228
|
+
const secret = getSecureKey(`oauth_app/${app.id}/client_secret`);
|
|
229
|
+
|
|
230
|
+
const refreshToken = getSecureKey(
|
|
231
|
+
`oauth_connection/${conn.id}/refresh_token`,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const authMethod = provider.tokenEndpointAuthMethod as
|
|
180
235
|
| TokenEndpointAuthMethod
|
|
181
236
|
| undefined;
|
|
182
|
-
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
connId: conn.id,
|
|
240
|
+
tokenUrl,
|
|
241
|
+
clientId,
|
|
242
|
+
secret,
|
|
243
|
+
refreshToken,
|
|
244
|
+
authMethod,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Attempt to refresh the OAuth2 access token for a service.
|
|
250
|
+
*
|
|
251
|
+
* Reads refresh config exclusively from the SQLite oauth-store (provider,
|
|
252
|
+
* app, connection).
|
|
253
|
+
*
|
|
254
|
+
* Returns the new access token on success.
|
|
255
|
+
* Throws `TokenExpiredError` if refresh is not possible.
|
|
256
|
+
*/
|
|
257
|
+
async function doRefresh(service: string): Promise<string> {
|
|
258
|
+
const refreshConfig = resolveRefreshConfig(service);
|
|
259
|
+
const { tokenUrl, clientId, secret, authMethod, connId, refreshToken } =
|
|
260
|
+
refreshConfig;
|
|
261
|
+
|
|
262
|
+
if (!refreshToken) {
|
|
263
|
+
throw new TokenExpiredError(
|
|
264
|
+
service,
|
|
265
|
+
`No refresh token available for "${service}". Re-authorization required.${recoveryHint(service)}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
183
268
|
|
|
184
269
|
if (isRefreshBreakerOpen(service)) {
|
|
185
270
|
const state = refreshBreakers.get(service)!;
|
|
@@ -196,10 +281,10 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
196
281
|
let result;
|
|
197
282
|
try {
|
|
198
283
|
result = await refreshOAuth2Token(
|
|
199
|
-
|
|
284
|
+
tokenUrl,
|
|
200
285
|
clientId,
|
|
201
286
|
refreshToken,
|
|
202
|
-
|
|
287
|
+
secret,
|
|
203
288
|
authMethod,
|
|
204
289
|
);
|
|
205
290
|
} catch (err) {
|
|
@@ -217,9 +302,10 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
217
302
|
throw err;
|
|
218
303
|
}
|
|
219
304
|
|
|
305
|
+
// ----- Store refreshed access_token -----
|
|
220
306
|
if (
|
|
221
307
|
!(await setSecureKeyAsync(
|
|
222
|
-
`
|
|
308
|
+
`oauth_connection/${connId}/access_token`,
|
|
223
309
|
result.accessToken,
|
|
224
310
|
))
|
|
225
311
|
) {
|
|
@@ -232,7 +318,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
232
318
|
if (result.refreshToken) {
|
|
233
319
|
if (
|
|
234
320
|
!(await setSecureKeyAsync(
|
|
235
|
-
`
|
|
321
|
+
`oauth_connection/${connId}/refresh_token`,
|
|
236
322
|
result.refreshToken,
|
|
237
323
|
))
|
|
238
324
|
) {
|
|
@@ -243,7 +329,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
243
329
|
}
|
|
244
330
|
}
|
|
245
331
|
|
|
246
|
-
// Update
|
|
332
|
+
// Update oauth_connection row with new expiry.
|
|
247
333
|
// Use null to explicitly clear a stale expiresAt when the provider omits
|
|
248
334
|
// expires_in (or returns 0), so isTokenExpired won't keep forcing refreshes.
|
|
249
335
|
const expiresAt =
|
|
@@ -251,7 +337,17 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
251
337
|
? Date.now() + result.expiresIn * 1000
|
|
252
338
|
: null;
|
|
253
339
|
|
|
254
|
-
|
|
340
|
+
try {
|
|
341
|
+
updateConnection(connId, {
|
|
342
|
+
expiresAt,
|
|
343
|
+
hasRefreshToken: !!result.refreshToken,
|
|
344
|
+
});
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log.warn(
|
|
347
|
+
{ err, service },
|
|
348
|
+
"Failed to update oauth_connection after refresh",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
255
351
|
|
|
256
352
|
recordRefreshSuccess(service);
|
|
257
353
|
log.info({ service }, "OAuth2 access token refreshed successfully");
|
|
@@ -265,12 +361,18 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
265
361
|
* 1. Retrieves the stored access token (throws if none exists).
|
|
266
362
|
* 2. If the token is expired or near-expiry, refreshes it before calling the callback.
|
|
267
363
|
* 3. If the callback throws with a 401 status, attempts one refresh-and-retry cycle.
|
|
364
|
+
*
|
|
365
|
+
* Retained only for BYO connection internals — prefer
|
|
366
|
+
* `resolveOAuthConnection(service).request()` for new code.
|
|
268
367
|
*/
|
|
269
368
|
export async function withValidToken<T>(
|
|
270
369
|
service: string,
|
|
271
370
|
callback: (token: string) => Promise<T>,
|
|
272
371
|
): Promise<T> {
|
|
273
|
-
|
|
372
|
+
const conn = getConnectionByProvider(service);
|
|
373
|
+
let token = conn
|
|
374
|
+
? getSecureKey(`oauth_connection/${conn.id}/access_token`)
|
|
375
|
+
: undefined;
|
|
274
376
|
if (!token) {
|
|
275
377
|
throw new TokenExpiredError(
|
|
276
378
|
service,
|
|
@@ -280,14 +382,14 @@ export async function withValidToken<T>(
|
|
|
280
382
|
|
|
281
383
|
// Proactively refresh if expired or about to expire.
|
|
282
384
|
if (isTokenExpired(service)) {
|
|
283
|
-
token = await
|
|
385
|
+
token = await deduplicatedRefresh(service);
|
|
284
386
|
}
|
|
285
387
|
|
|
286
388
|
try {
|
|
287
389
|
return await callback(token);
|
|
288
390
|
} catch (err: unknown) {
|
|
289
391
|
if (is401Error(err)) {
|
|
290
|
-
token = await
|
|
392
|
+
token = await deduplicatedRefresh(service);
|
|
291
393
|
return callback(token);
|
|
292
394
|
}
|
|
293
395
|
throw err;
|
|
@@ -0,0 +1,358 @@
|
|
|
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 } 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
|
|
164
|
+
const normalizedName = name.replace(/^\.\//, "");
|
|
165
|
+
if (!normalizedName.startsWith("..") && !normalizedName.includes("/..")) {
|
|
166
|
+
const destPath = join(destDir, normalizedName);
|
|
167
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
168
|
+
writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
|
|
169
|
+
|
|
170
|
+
if (
|
|
171
|
+
normalizedName === "SKILL.md" ||
|
|
172
|
+
normalizedName.endsWith("/SKILL.md")
|
|
173
|
+
) {
|
|
174
|
+
foundSkillMd = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Skip to next header (data padded to 512 bytes)
|
|
180
|
+
offset += Math.ceil(size / 512) * 512;
|
|
181
|
+
}
|
|
182
|
+
return foundSkillMd;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function fetchAndExtractSkill(
|
|
186
|
+
skillId: string,
|
|
187
|
+
destDir: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const url = `${getPlatformUrl()}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
headers: buildHeaders(),
|
|
192
|
+
signal: AbortSignal.timeout(15000),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Failed to fetch skill "${skillId}": HTTP ${response.status}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const gzipBuffer = Buffer.from(await response.arrayBuffer());
|
|
202
|
+
const tarBuffer = gunzipSync(gzipBuffer);
|
|
203
|
+
const foundSkillMd = extractTarToDir(tarBuffer, destDir);
|
|
204
|
+
|
|
205
|
+
if (!foundSkillMd) {
|
|
206
|
+
throw new Error(`SKILL.md not found in archive for "${skillId}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── SKILLS.md index management ──────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
function atomicWriteFile(filePath: string, content: string): void {
|
|
213
|
+
const dir = dirname(filePath);
|
|
214
|
+
mkdirSync(dir, { recursive: true });
|
|
215
|
+
const tmpPath = join(dir, `.tmp-${randomUUID()}`);
|
|
216
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
217
|
+
renameSync(tmpPath, filePath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function upsertSkillsIndex(id: string): void {
|
|
221
|
+
const indexPath = getSkillsIndexPath();
|
|
222
|
+
let lines: string[] = [];
|
|
223
|
+
if (existsSync(indexPath)) {
|
|
224
|
+
lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
228
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
229
|
+
if (lines.some((line) => pattern.test(line))) return;
|
|
230
|
+
|
|
231
|
+
const nonEmpty = lines.filter((l) => l.trim());
|
|
232
|
+
nonEmpty.push(`- ${id}`);
|
|
233
|
+
const content = nonEmpty.join("\n");
|
|
234
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function removeSkillsIndexEntry(id: string): void {
|
|
238
|
+
const indexPath = getSkillsIndexPath();
|
|
239
|
+
if (!existsSync(indexPath)) return;
|
|
240
|
+
|
|
241
|
+
const lines = readFileSync(indexPath, "utf-8").split("\n");
|
|
242
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
243
|
+
const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
|
|
244
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
245
|
+
|
|
246
|
+
// If nothing changed, skip the write
|
|
247
|
+
if (filtered.length === lines.length) return;
|
|
248
|
+
|
|
249
|
+
const content = filtered.join("\n");
|
|
250
|
+
atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Install / uninstall ─────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
export function uninstallSkillLocally(skillId: string): void {
|
|
256
|
+
const skillDir = join(getWorkspaceSkillsDir(), skillId);
|
|
257
|
+
|
|
258
|
+
if (!existsSync(skillDir)) {
|
|
259
|
+
throw new Error(`Skill "${skillId}" is not installed.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
263
|
+
removeSkillsIndexEntry(skillId);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function installSkillLocally(
|
|
267
|
+
skillId: string,
|
|
268
|
+
catalogEntry: CatalogSkill,
|
|
269
|
+
overwrite: boolean,
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
const skillDir = join(getWorkspaceSkillsDir(), skillId);
|
|
272
|
+
const skillFilePath = join(skillDir, "SKILL.md");
|
|
273
|
+
|
|
274
|
+
if (existsSync(skillFilePath) && !overwrite) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Skill "${skillId}" is already installed. Use --overwrite to replace it.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
mkdirSync(skillDir, { recursive: true });
|
|
281
|
+
|
|
282
|
+
// In dev mode, install from the local repo skills directory if available
|
|
283
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
284
|
+
const repoSkillSource = repoSkillsDir
|
|
285
|
+
? join(repoSkillsDir, skillId)
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
|
|
289
|
+
cpSync(repoSkillSource, skillDir, { recursive: true });
|
|
290
|
+
} else {
|
|
291
|
+
await fetchAndExtractSkill(skillId, skillDir);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Write version metadata
|
|
295
|
+
if (catalogEntry.version) {
|
|
296
|
+
const meta = {
|
|
297
|
+
version: catalogEntry.version,
|
|
298
|
+
installedAt: new Date().toISOString(),
|
|
299
|
+
};
|
|
300
|
+
atomicWriteFile(
|
|
301
|
+
join(skillDir, "version.json"),
|
|
302
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Install npm dependencies if the skill has a package.json
|
|
307
|
+
if (existsSync(join(skillDir, "package.json"))) {
|
|
308
|
+
const bunPath = `${homedir()}/.bun/bin`;
|
|
309
|
+
execSync("bun install", {
|
|
310
|
+
cwd: skillDir,
|
|
311
|
+
stdio: "inherit",
|
|
312
|
+
env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Register in SKILLS.md only after all steps succeed
|
|
317
|
+
upsertSkillsIndex(skillId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── Auto-install (for skill_load) ──────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Attempt to find and install a skill from the first-party catalog.
|
|
324
|
+
* Returns true if the skill was installed, false if not found in catalog.
|
|
325
|
+
* Throws on install failures (network, filesystem, etc).
|
|
326
|
+
*/
|
|
327
|
+
export async function autoInstallFromCatalog(
|
|
328
|
+
skillId: string,
|
|
329
|
+
): Promise<boolean> {
|
|
330
|
+
// Check local catalog first (dev mode), then remote
|
|
331
|
+
const repoSkillsDir = getRepoSkillsDir();
|
|
332
|
+
let entry: CatalogSkill | undefined;
|
|
333
|
+
|
|
334
|
+
if (repoSkillsDir) {
|
|
335
|
+
const localCatalog = readLocalCatalog(repoSkillsDir);
|
|
336
|
+
entry = localCatalog.find((s) => s.id === skillId);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!entry) {
|
|
340
|
+
try {
|
|
341
|
+
const remoteCatalog = await fetchCatalog();
|
|
342
|
+
entry = remoteCatalog.find((s) => s.id === skillId);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
log.warn(
|
|
345
|
+
{ err, skillId },
|
|
346
|
+
"Failed to fetch remote catalog for auto-install",
|
|
347
|
+
);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!entry) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await installSkillLocally(skillId, entry, false);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
@@ -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
|
+
}
|