@vellumai/assistant 0.4.49 → 0.4.51
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- 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__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- 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__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- 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-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__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- 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 +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- 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/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- 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 +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- 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-queries.ts +25 -83
- 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/invite-store.ts +19 -0
- 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 +2 -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/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -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/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -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 +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- 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/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- 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/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- 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__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- 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/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/runtime/routes/mcp-routes.ts +0 -20
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import { extractTarToDir } from "../skills/catalog-install.js";
|
|
7
|
+
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
function makeTarEntry(name: string, content: string): Buffer {
|
|
11
|
+
const header = Buffer.alloc(512, 0);
|
|
12
|
+
const nameBuffer = Buffer.from(name, "utf-8");
|
|
13
|
+
nameBuffer.copy(header, 0, 0, Math.min(nameBuffer.length, 100));
|
|
14
|
+
|
|
15
|
+
const mode = Buffer.from("0000644\0", "ascii");
|
|
16
|
+
mode.copy(header, 100);
|
|
17
|
+
Buffer.from("0000000\0", "ascii").copy(header, 108); // uid
|
|
18
|
+
Buffer.from("0000000\0", "ascii").copy(header, 116); // gid
|
|
19
|
+
|
|
20
|
+
const sizeOct = content.length.toString(8).padStart(11, "0") + "\0";
|
|
21
|
+
Buffer.from(sizeOct, "ascii").copy(header, 124);
|
|
22
|
+
|
|
23
|
+
Buffer.from("00000000000\0", "ascii").copy(header, 136); // mtime
|
|
24
|
+
Buffer.from(" ", "ascii").copy(header, 148); // checksum placeholder
|
|
25
|
+
header[156] = "0".charCodeAt(0);
|
|
26
|
+
Buffer.from("ustar\0", "ascii").copy(header, 257);
|
|
27
|
+
Buffer.from("00", "ascii").copy(header, 263);
|
|
28
|
+
|
|
29
|
+
let sum = 0;
|
|
30
|
+
for (let i = 0; i < 512; i += 1) sum += header[i] ?? 0;
|
|
31
|
+
const checksum = sum.toString(8).padStart(6, "0");
|
|
32
|
+
Buffer.from(`${checksum}\0 `, "ascii").copy(header, 148);
|
|
33
|
+
|
|
34
|
+
const data = Buffer.from(content, "utf-8");
|
|
35
|
+
const paddedSize = Math.ceil(data.length / 512) * 512;
|
|
36
|
+
const padded = Buffer.alloc(paddedSize, 0);
|
|
37
|
+
data.copy(padded);
|
|
38
|
+
|
|
39
|
+
return Buffer.concat([header, padded]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeTar(entries: Array<{ name: string; content: string }>): Buffer {
|
|
43
|
+
const body = entries.map((entry) => makeTarEntry(entry.name, entry.content));
|
|
44
|
+
return Buffer.concat([...body, Buffer.alloc(1024, 0)]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tempDir = join(
|
|
49
|
+
tmpdir(),
|
|
50
|
+
`skills-extract-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
51
|
+
);
|
|
52
|
+
mkdirSync(tempDir, { recursive: true });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("extractTarToDir", () => {
|
|
60
|
+
test("extracts valid files and detects SKILL.md", () => {
|
|
61
|
+
const tar = makeTar([
|
|
62
|
+
{ name: "SKILL.md", content: "# demo\n" },
|
|
63
|
+
{ name: "scripts/run.sh", content: "echo ok\n" },
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const foundSkillMd = extractTarToDir(tar, tempDir);
|
|
67
|
+
|
|
68
|
+
expect(foundSkillMd).toBe(true);
|
|
69
|
+
expect(readFileSync(join(tempDir, "SKILL.md"), "utf-8")).toBe("# demo\n");
|
|
70
|
+
expect(readFileSync(join(tempDir, "scripts", "run.sh"), "utf-8")).toBe(
|
|
71
|
+
"echo ok\n",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("rejects traversal and absolute archive paths", () => {
|
|
76
|
+
const tar = makeTar([
|
|
77
|
+
{ name: "SKILL.md", content: "# demo\n" },
|
|
78
|
+
{ name: "../../escape.txt", content: "nope\n" },
|
|
79
|
+
{ name: "..\\..\\win-escape.txt", content: "nope\n" },
|
|
80
|
+
{ name: "/absolute.txt", content: "nope\n" },
|
|
81
|
+
{ name: "C:/windows.txt", content: "nope\n" },
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const foundSkillMd = extractTarToDir(tar, tempDir);
|
|
85
|
+
|
|
86
|
+
expect(foundSkillMd).toBe(true);
|
|
87
|
+
expect(existsSync(join(tempDir, "escape.txt"))).toBe(false);
|
|
88
|
+
expect(existsSync(join(tempDir, "win-escape.txt"))).toBe(false);
|
|
89
|
+
expect(existsSync(join(tempDir, "absolute.txt"))).toBe(false);
|
|
90
|
+
expect(existsSync(join(tempDir, "windows.txt"))).toBe(false);
|
|
91
|
+
expect(readFileSync(join(tempDir, "SKILL.md"), "utf-8")).toBe("# demo\n");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -674,13 +674,13 @@ describe("ingress-dependent setup skills declare public-ingress", () => {
|
|
|
674
674
|
expect(includes).toContain("public-ingress");
|
|
675
675
|
});
|
|
676
676
|
|
|
677
|
-
test("slack-oauth-setup includes
|
|
677
|
+
test("slack-oauth-setup includes collaborative-oauth-flow", () => {
|
|
678
678
|
const includes = readSkillIncludes(
|
|
679
679
|
FIRST_PARTY_SKILLS_DIR,
|
|
680
680
|
"slack-oauth-setup",
|
|
681
681
|
);
|
|
682
682
|
expect(includes).toBeDefined();
|
|
683
|
-
expect(includes).toContain("
|
|
683
|
+
expect(includes).toContain("collaborative-oauth-flow");
|
|
684
684
|
});
|
|
685
685
|
});
|
|
686
686
|
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AuditResponse,
|
|
5
|
+
SkillAuditData,
|
|
6
|
+
SkillsShSearchResult,
|
|
7
|
+
} from "../skills/skillssh-registry.js";
|
|
8
|
+
import {
|
|
9
|
+
fetchSkillAudits,
|
|
10
|
+
fetchSkillFromGitHub,
|
|
11
|
+
formatAuditBadges,
|
|
12
|
+
providerDisplayName,
|
|
13
|
+
resolveSkillSource,
|
|
14
|
+
riskToDisplay,
|
|
15
|
+
searchSkillsRegistry,
|
|
16
|
+
validateSkillSlug,
|
|
17
|
+
} from "../skills/skillssh-registry.js";
|
|
18
|
+
|
|
19
|
+
// ─── Fetch mock helpers ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const originalFetch = globalThis.fetch;
|
|
22
|
+
|
|
23
|
+
let mockFetchImpl: (url: string | URL | Request) => Promise<Response>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
mockFetchImpl = () =>
|
|
27
|
+
Promise.resolve(new Response("not mocked", { status: 500 }));
|
|
28
|
+
globalThis.fetch = mock((input: string | URL | Request) =>
|
|
29
|
+
mockFetchImpl(typeof input === "string" ? input : input.toString()),
|
|
30
|
+
) as unknown as typeof fetch;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
globalThis.fetch = originalFetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── searchSkillsRegistry ────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe("searchSkillsRegistry", () => {
|
|
40
|
+
test("sends correct query parameters and returns results", async () => {
|
|
41
|
+
const mockResults: SkillsShSearchResult[] = [
|
|
42
|
+
{
|
|
43
|
+
id: "vercel-labs/agent-skills/vercel-react-best-practices",
|
|
44
|
+
skillId: "vercel-react-best-practices",
|
|
45
|
+
name: "Vercel React Best Practices",
|
|
46
|
+
installs: 1200,
|
|
47
|
+
source: "vercel-labs/agent-skills",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
52
|
+
const urlStr = url.toString();
|
|
53
|
+
expect(urlStr).toContain("skills.sh/api/search");
|
|
54
|
+
expect(urlStr).toContain("q=react");
|
|
55
|
+
expect(urlStr).toContain("limit=5");
|
|
56
|
+
return Promise.resolve(
|
|
57
|
+
new Response(JSON.stringify({ skills: mockResults }), {
|
|
58
|
+
status: 200,
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const results = await searchSkillsRegistry("react", 5);
|
|
65
|
+
expect(results).toEqual(mockResults);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("omits limit parameter when not provided", async () => {
|
|
69
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
70
|
+
const urlStr = url.toString();
|
|
71
|
+
expect(urlStr).not.toContain("limit=");
|
|
72
|
+
return Promise.resolve(
|
|
73
|
+
new Response(JSON.stringify({ skills: [] }), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const results = await searchSkillsRegistry("test");
|
|
81
|
+
expect(results).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("throws on non-OK response", async () => {
|
|
85
|
+
mockFetchImpl = () =>
|
|
86
|
+
Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
87
|
+
|
|
88
|
+
await expect(searchSkillsRegistry("bad-query")).rejects.toThrow(
|
|
89
|
+
"skills.sh search failed: HTTP 404",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── fetchSkillAudits ────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("fetchSkillAudits", () => {
|
|
97
|
+
test("sends correct parameters and returns audit data", async () => {
|
|
98
|
+
const mockAudits: AuditResponse = {
|
|
99
|
+
"vercel-react-best-practices": {
|
|
100
|
+
ath: {
|
|
101
|
+
risk: "safe",
|
|
102
|
+
alerts: 0,
|
|
103
|
+
score: 100,
|
|
104
|
+
analyzedAt: "2025-01-15T00:00:00Z",
|
|
105
|
+
},
|
|
106
|
+
socket: {
|
|
107
|
+
risk: "low",
|
|
108
|
+
alerts: 1,
|
|
109
|
+
score: 95,
|
|
110
|
+
analyzedAt: "2025-01-15T00:00:00Z",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
116
|
+
const urlStr = url.toString();
|
|
117
|
+
expect(urlStr).toContain("add-skill.vercel.sh/audit");
|
|
118
|
+
expect(urlStr).toContain("source=vercel-labs%2Fagent-skills");
|
|
119
|
+
expect(urlStr).toContain(
|
|
120
|
+
"skills=vercel-react-best-practices%2Canother-skill",
|
|
121
|
+
);
|
|
122
|
+
return Promise.resolve(
|
|
123
|
+
new Response(JSON.stringify(mockAudits), {
|
|
124
|
+
status: 200,
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const audits = await fetchSkillAudits("vercel-labs/agent-skills", [
|
|
131
|
+
"vercel-react-best-practices",
|
|
132
|
+
"another-skill",
|
|
133
|
+
]);
|
|
134
|
+
expect(audits).toEqual(mockAudits);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("returns empty object for empty slugs list", async () => {
|
|
138
|
+
const audits = await fetchSkillAudits("some/source", []);
|
|
139
|
+
expect(audits).toEqual({});
|
|
140
|
+
// fetch should not have been called
|
|
141
|
+
expect(globalThis.fetch).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("throws on non-OK response", async () => {
|
|
145
|
+
mockFetchImpl = () =>
|
|
146
|
+
Promise.resolve(new Response("Internal Server Error", { status: 500 }));
|
|
147
|
+
|
|
148
|
+
await expect(fetchSkillAudits("some/source", ["slug"])).rejects.toThrow(
|
|
149
|
+
"Audit fetch failed: HTTP 500",
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── Display helpers ─────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
describe("riskToDisplay", () => {
|
|
157
|
+
test("maps risk levels correctly", () => {
|
|
158
|
+
expect(riskToDisplay("safe")).toBe("PASS");
|
|
159
|
+
expect(riskToDisplay("low")).toBe("PASS");
|
|
160
|
+
expect(riskToDisplay("medium")).toBe("WARN");
|
|
161
|
+
expect(riskToDisplay("high")).toBe("FAIL");
|
|
162
|
+
expect(riskToDisplay("critical")).toBe("FAIL");
|
|
163
|
+
expect(riskToDisplay("unknown")).toBe("?");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("providerDisplayName", () => {
|
|
168
|
+
test("maps known providers", () => {
|
|
169
|
+
expect(providerDisplayName("ath")).toBe("ATH");
|
|
170
|
+
expect(providerDisplayName("socket")).toBe("Socket");
|
|
171
|
+
expect(providerDisplayName("snyk")).toBe("Snyk");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("returns raw name for unknown providers", () => {
|
|
175
|
+
expect(providerDisplayName("custom-auditor")).toBe("custom-auditor");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("formatAuditBadges", () => {
|
|
180
|
+
test("formats multiple providers as badges", () => {
|
|
181
|
+
const auditData: SkillAuditData = {
|
|
182
|
+
ath: { risk: "safe", analyzedAt: "2025-01-15T00:00:00Z" },
|
|
183
|
+
socket: { risk: "safe", analyzedAt: "2025-01-15T00:00:00Z" },
|
|
184
|
+
snyk: { risk: "medium", analyzedAt: "2025-01-15T00:00:00Z" },
|
|
185
|
+
};
|
|
186
|
+
expect(formatAuditBadges(auditData)).toBe(
|
|
187
|
+
"Security: [ATH:PASS] [Socket:PASS] [Snyk:WARN]",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("returns fallback message when no providers present", () => {
|
|
192
|
+
expect(formatAuditBadges({})).toBe("Security: no audit data");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("handles single provider", () => {
|
|
196
|
+
const auditData: SkillAuditData = {
|
|
197
|
+
ath: { risk: "critical", analyzedAt: "2025-01-15T00:00:00Z" },
|
|
198
|
+
};
|
|
199
|
+
expect(formatAuditBadges(auditData)).toBe("Security: [ATH:FAIL]");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ─── resolveSkillSource ─────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
describe("resolveSkillSource", () => {
|
|
206
|
+
test("parses owner/repo@skill-name format", () => {
|
|
207
|
+
const result = resolveSkillSource("vercel-labs/skills@find-skills");
|
|
208
|
+
expect(result).toEqual({
|
|
209
|
+
owner: "vercel-labs",
|
|
210
|
+
repo: "skills",
|
|
211
|
+
skillSlug: "find-skills",
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("parses owner/repo/skill-name format", () => {
|
|
216
|
+
const result = resolveSkillSource("vercel-labs/skills/find-skills");
|
|
217
|
+
expect(result).toEqual({
|
|
218
|
+
owner: "vercel-labs",
|
|
219
|
+
repo: "skills",
|
|
220
|
+
skillSlug: "find-skills",
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("parses full GitHub URL with main branch", () => {
|
|
225
|
+
const result = resolveSkillSource(
|
|
226
|
+
"https://github.com/vercel-labs/skills/tree/main/skills/find-skills",
|
|
227
|
+
);
|
|
228
|
+
expect(result).toEqual({
|
|
229
|
+
owner: "vercel-labs",
|
|
230
|
+
repo: "skills",
|
|
231
|
+
skillSlug: "find-skills",
|
|
232
|
+
ref: "main",
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("parses full GitHub URL with non-main branch", () => {
|
|
237
|
+
const result = resolveSkillSource(
|
|
238
|
+
"https://github.com/some-org/repo/tree/develop/skills/my-skill",
|
|
239
|
+
);
|
|
240
|
+
expect(result).toEqual({
|
|
241
|
+
owner: "some-org",
|
|
242
|
+
repo: "repo",
|
|
243
|
+
skillSlug: "my-skill",
|
|
244
|
+
ref: "develop",
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("parses GitHub URL with trailing slash", () => {
|
|
249
|
+
const result = resolveSkillSource(
|
|
250
|
+
"https://github.com/owner/repo/tree/main/skills/skill-name/",
|
|
251
|
+
);
|
|
252
|
+
expect(result).toEqual({
|
|
253
|
+
owner: "owner",
|
|
254
|
+
repo: "repo",
|
|
255
|
+
skillSlug: "skill-name",
|
|
256
|
+
ref: "main",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("throws on bare skill name (no owner/repo)", () => {
|
|
261
|
+
expect(() => resolveSkillSource("find-skills")).toThrow(
|
|
262
|
+
'Invalid skill source "find-skills"',
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("throws on empty string", () => {
|
|
267
|
+
expect(() => resolveSkillSource("")).toThrow('Invalid skill source ""');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("throws on owner-only format", () => {
|
|
271
|
+
expect(() => resolveSkillSource("vercel-labs")).toThrow(
|
|
272
|
+
'Invalid skill source "vercel-labs"',
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("throws on owner/repo without skill", () => {
|
|
277
|
+
expect(() => resolveSkillSource("vercel-labs/skills")).toThrow(
|
|
278
|
+
'Invalid skill source "vercel-labs/skills"',
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("rejects path traversal in @ format slug", () => {
|
|
283
|
+
expect(() => resolveSkillSource("owner/repo@../../malicious")).toThrow(
|
|
284
|
+
'Invalid skill source "owner/repo@../../malicious"',
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("rejects uppercase slug in @ format", () => {
|
|
289
|
+
expect(() => resolveSkillSource("owner/repo@BadSlug")).toThrow(
|
|
290
|
+
'Invalid skill source "owner/repo@BadSlug"',
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ─── validateSkillSlug ──────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
describe("validateSkillSlug", () => {
|
|
298
|
+
test("accepts valid slugs", () => {
|
|
299
|
+
expect(() => validateSkillSlug("my-skill")).not.toThrow();
|
|
300
|
+
expect(() => validateSkillSlug("skill123")).not.toThrow();
|
|
301
|
+
expect(() => validateSkillSlug("my.skill")).not.toThrow();
|
|
302
|
+
expect(() => validateSkillSlug("my_skill")).not.toThrow();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("rejects path traversal characters", () => {
|
|
306
|
+
expect(() => validateSkillSlug("../../malicious")).toThrow(
|
|
307
|
+
"path traversal",
|
|
308
|
+
);
|
|
309
|
+
expect(() => validateSkillSlug("foo/bar")).toThrow("path traversal");
|
|
310
|
+
expect(() => validateSkillSlug("foo\\bar")).toThrow("path traversal");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("rejects slugs starting with special chars", () => {
|
|
314
|
+
expect(() => validateSkillSlug(".hidden")).toThrow();
|
|
315
|
+
expect(() => validateSkillSlug("-dash")).toThrow();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("rejects empty input", () => {
|
|
319
|
+
expect(() => validateSkillSlug("")).toThrow("Skill slug is required");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ─── fetchSkillFromGitHub ───────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
describe("fetchSkillFromGitHub", () => {
|
|
326
|
+
test("fetches from conventional skills/<slug>/ path", async () => {
|
|
327
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
328
|
+
const urlStr = url.toString();
|
|
329
|
+
// Probe request for skills/my-skill
|
|
330
|
+
if (urlStr.includes("/contents/skills/my-skill")) {
|
|
331
|
+
return Promise.resolve(
|
|
332
|
+
new Response(
|
|
333
|
+
JSON.stringify([
|
|
334
|
+
{
|
|
335
|
+
name: "SKILL.md",
|
|
336
|
+
type: "file",
|
|
337
|
+
download_url: "https://raw.example.com/SKILL.md",
|
|
338
|
+
},
|
|
339
|
+
]),
|
|
340
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
// File download
|
|
345
|
+
if (urlStr.includes("raw.example.com/SKILL.md")) {
|
|
346
|
+
return Promise.resolve(new Response("# My Skill", { status: 200 }));
|
|
347
|
+
}
|
|
348
|
+
return Promise.resolve(new Response("not found", { status: 404 }));
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const files = await fetchSkillFromGitHub("owner", "repo", "my-skill");
|
|
352
|
+
expect(files["SKILL.md"]).toBe("# My Skill");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("falls back to tree search when skills/<slug>/ returns 404", async () => {
|
|
356
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
357
|
+
const urlStr = url.toString();
|
|
358
|
+
// Probe for conventional path returns 404
|
|
359
|
+
if (urlStr.includes("/contents/skills/csv")) {
|
|
360
|
+
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
361
|
+
}
|
|
362
|
+
// Tree search returns the skill at a non-standard path
|
|
363
|
+
if (urlStr.includes("/git/trees/")) {
|
|
364
|
+
return Promise.resolve(
|
|
365
|
+
new Response(
|
|
366
|
+
JSON.stringify({
|
|
367
|
+
tree: [
|
|
368
|
+
{ path: "examples/skills/csv/SKILL.md", type: "blob" },
|
|
369
|
+
{ path: "examples/skills/csv/scripts/filter.sh", type: "blob" },
|
|
370
|
+
{ path: "README.md", type: "blob" },
|
|
371
|
+
],
|
|
372
|
+
}),
|
|
373
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
// Subdirectory listing (must precede parent path check — both use
|
|
378
|
+
// .includes() and the parent path is a prefix of this one)
|
|
379
|
+
if (urlStr.includes("/contents/examples/skills/csv/scripts")) {
|
|
380
|
+
return Promise.resolve(
|
|
381
|
+
new Response(
|
|
382
|
+
JSON.stringify([
|
|
383
|
+
{
|
|
384
|
+
name: "filter.sh",
|
|
385
|
+
type: "file",
|
|
386
|
+
download_url: "https://raw.example.com/filter.sh",
|
|
387
|
+
},
|
|
388
|
+
]),
|
|
389
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
// Contents API for the discovered path
|
|
394
|
+
if (urlStr.includes("/contents/examples/skills/csv")) {
|
|
395
|
+
return Promise.resolve(
|
|
396
|
+
new Response(
|
|
397
|
+
JSON.stringify([
|
|
398
|
+
{
|
|
399
|
+
name: "SKILL.md",
|
|
400
|
+
type: "file",
|
|
401
|
+
download_url: "https://raw.example.com/SKILL.md",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "scripts",
|
|
405
|
+
type: "dir",
|
|
406
|
+
download_url: null,
|
|
407
|
+
},
|
|
408
|
+
]),
|
|
409
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
// File downloads
|
|
414
|
+
if (urlStr.includes("raw.example.com/SKILL.md")) {
|
|
415
|
+
return Promise.resolve(new Response("# CSV Skill", { status: 200 }));
|
|
416
|
+
}
|
|
417
|
+
if (urlStr.includes("raw.example.com/filter.sh")) {
|
|
418
|
+
return Promise.resolve(
|
|
419
|
+
new Response("#!/bin/bash\necho filter", { status: 200 }),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return Promise.resolve(new Response("not found", { status: 404 }));
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const files = await fetchSkillFromGitHub("vercel-labs", "bash-tool", "csv");
|
|
426
|
+
expect(files["SKILL.md"]).toBe("# CSV Skill");
|
|
427
|
+
expect(files["scripts/filter.sh"]).toBe("#!/bin/bash\necho filter");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("throws when skill not found in tree either", async () => {
|
|
431
|
+
mockFetchImpl = (url: string | URL | Request) => {
|
|
432
|
+
const urlStr = url.toString();
|
|
433
|
+
if (urlStr.includes("/contents/skills/missing")) {
|
|
434
|
+
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
435
|
+
}
|
|
436
|
+
if (urlStr.includes("/git/trees/")) {
|
|
437
|
+
return Promise.resolve(
|
|
438
|
+
new Response(
|
|
439
|
+
JSON.stringify({ tree: [{ path: "README.md", type: "blob" }] }),
|
|
440
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return Promise.resolve(new Response("not found", { status: 404 }));
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
await expect(
|
|
448
|
+
fetchSkillFromGitHub("owner", "repo", "missing"),
|
|
449
|
+
).rejects.toThrow("Searched skills/missing/ and the full repo tree");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
@@ -101,6 +101,8 @@ mock.module("../security/secure-keys.js", () => {
|
|
|
101
101
|
};
|
|
102
102
|
return {
|
|
103
103
|
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
104
|
+
getSecureKeyAsync: async (account: string) =>
|
|
105
|
+
secureKeyStore[account] ?? undefined,
|
|
104
106
|
setSecureKey: syncSet,
|
|
105
107
|
deleteSecureKey: syncDelete,
|
|
106
108
|
setSecureKeyAsync: async (account: string, value: string) =>
|
|
@@ -230,15 +232,15 @@ describe("Slack channel config handler", () => {
|
|
|
230
232
|
globalThis.fetch = originalFetch;
|
|
231
233
|
});
|
|
232
234
|
|
|
233
|
-
test("GET returns correct shape when not configured", () => {
|
|
234
|
-
const result = getSlackChannelConfig();
|
|
235
|
+
test("GET returns correct shape when not configured", async () => {
|
|
236
|
+
const result = await getSlackChannelConfig();
|
|
235
237
|
expect(result.success).toBe(true);
|
|
236
238
|
expect(result.hasBotToken).toBe(false);
|
|
237
239
|
expect(result.hasAppToken).toBe(false);
|
|
238
240
|
expect(result.connected).toBe(false);
|
|
239
241
|
});
|
|
240
242
|
|
|
241
|
-
test("GET returns connected: true when oauth_connection is active and both keys exist", () => {
|
|
243
|
+
test("GET returns connected: true when oauth_connection is active and both keys exist", async () => {
|
|
242
244
|
oauthConnectionStore["slack_channel"] = {
|
|
243
245
|
id: "conn-slack",
|
|
244
246
|
status: "active",
|
|
@@ -246,14 +248,14 @@ describe("Slack channel config handler", () => {
|
|
|
246
248
|
secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
|
|
247
249
|
secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
|
|
248
250
|
|
|
249
|
-
const result = getSlackChannelConfig();
|
|
251
|
+
const result = await getSlackChannelConfig();
|
|
250
252
|
expect(result.success).toBe(true);
|
|
251
253
|
expect(result.hasBotToken).toBe(true);
|
|
252
254
|
expect(result.hasAppToken).toBe(true);
|
|
253
255
|
expect(result.connected).toBe(true);
|
|
254
256
|
});
|
|
255
257
|
|
|
256
|
-
test("GET reports per-field token presence independently of connection row", () => {
|
|
258
|
+
test("GET reports per-field token presence independently of connection row", async () => {
|
|
257
259
|
// Only bot_token in keychain, no app_token, but connection row exists
|
|
258
260
|
oauthConnectionStore["slack_channel"] = {
|
|
259
261
|
id: "conn-slack",
|
|
@@ -261,7 +263,7 @@ describe("Slack channel config handler", () => {
|
|
|
261
263
|
};
|
|
262
264
|
secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
|
|
263
265
|
|
|
264
|
-
const result = getSlackChannelConfig();
|
|
266
|
+
const result = await getSlackChannelConfig();
|
|
265
267
|
expect(result.success).toBe(true);
|
|
266
268
|
expect(result.hasBotToken).toBe(true);
|
|
267
269
|
expect(result.hasAppToken).toBe(false);
|
|
@@ -269,7 +271,7 @@ describe("Slack channel config handler", () => {
|
|
|
269
271
|
expect(result.connected).toBe(false);
|
|
270
272
|
});
|
|
271
273
|
|
|
272
|
-
test("GET returns metadata from config when available", () => {
|
|
274
|
+
test("GET returns metadata from config when available", async () => {
|
|
273
275
|
oauthConnectionStore["slack_channel"] = {
|
|
274
276
|
id: "conn-slack",
|
|
275
277
|
status: "active",
|
|
@@ -285,7 +287,7 @@ describe("Slack channel config handler", () => {
|
|
|
285
287
|
},
|
|
286
288
|
};
|
|
287
289
|
|
|
288
|
-
const result = getSlackChannelConfig();
|
|
290
|
+
const result = await getSlackChannelConfig();
|
|
289
291
|
expect(result.teamId).toBe("T123");
|
|
290
292
|
expect(result.teamName).toBe("TestTeam");
|
|
291
293
|
expect(result.botUserId).toBe("U_BOT");
|
|
@@ -777,6 +777,7 @@ describe("Trust Store", () => {
|
|
|
777
777
|
"computer_use_click",
|
|
778
778
|
"computer_use_drag",
|
|
779
779
|
"computer_use_key",
|
|
780
|
+
"computer_use_observe",
|
|
780
781
|
"computer_use_open_app",
|
|
781
782
|
"computer_use_run_applescript",
|
|
782
783
|
"computer_use_scroll",
|
|
@@ -900,6 +901,20 @@ describe("Trust Store", () => {
|
|
|
900
901
|
);
|
|
901
902
|
});
|
|
902
903
|
|
|
904
|
+
test("findHighestPriorityRule matches default ask for computer_use_observe", () => {
|
|
905
|
+
const match = findHighestPriorityRule(
|
|
906
|
+
"computer_use_observe",
|
|
907
|
+
["computer_use_observe:"],
|
|
908
|
+
"/tmp",
|
|
909
|
+
);
|
|
910
|
+
expect(match).not.toBeNull();
|
|
911
|
+
expect(match!.id).toBe("default:ask-computer_use_observe-global");
|
|
912
|
+
expect(match!.decision).toBe("ask");
|
|
913
|
+
expect(match!.priority).toBe(
|
|
914
|
+
DEFAULT_PRIORITY_BY_ID.get("default:ask-computer_use_observe-global")!,
|
|
915
|
+
);
|
|
916
|
+
});
|
|
917
|
+
|
|
903
918
|
test("bootstrap delete rule matches only when workingDir is the workspace dir", () => {
|
|
904
919
|
const workspaceDir = join(testDir, "workspace");
|
|
905
920
|
// Should match when workingDir is the workspace directory — the bootstrap
|