@vellumai/assistant 0.4.49 → 0.4.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -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__/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__/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-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- 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__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- 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 +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- 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 +46 -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__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- 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__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- 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 +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- 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 +4 -10
- 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-tool-registry.ts +2 -5
- 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/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- 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 +31 -3
- 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 -0
- package/src/daemon/server.ts +12 -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-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -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 +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +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/index.ts +7 -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/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/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- 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/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- 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 +1 -3
- 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/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- 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,503 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { getWorkspaceSkillsDir } from "../util/platform.js";
|
|
7
|
+
import { upsertSkillsIndex } from "./catalog-install.js";
|
|
8
|
+
|
|
9
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface SkillsShSearchResult {
|
|
12
|
+
id: string; // e.g. "vercel-labs/agent-skills/vercel-react-best-practices"
|
|
13
|
+
skillId: string; // e.g. "vercel-react-best-practices"
|
|
14
|
+
name: string;
|
|
15
|
+
installs: number;
|
|
16
|
+
source: string; // e.g. "vercel-labs/agent-skills"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RiskLevel =
|
|
20
|
+
| "safe"
|
|
21
|
+
| "low"
|
|
22
|
+
| "medium"
|
|
23
|
+
| "high"
|
|
24
|
+
| "critical"
|
|
25
|
+
| "unknown";
|
|
26
|
+
|
|
27
|
+
export interface PartnerAudit {
|
|
28
|
+
risk: RiskLevel;
|
|
29
|
+
alerts?: number;
|
|
30
|
+
score?: number;
|
|
31
|
+
analyzedAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Map from audit provider name (e.g. "ath", "socket", "snyk") to audit data */
|
|
35
|
+
export type SkillAuditData = Record<string, PartnerAudit>;
|
|
36
|
+
|
|
37
|
+
/** Map from skill slug to per-provider audit data */
|
|
38
|
+
export type AuditResponse = Record<string, SkillAuditData>;
|
|
39
|
+
|
|
40
|
+
export interface ResolvedSkillSource {
|
|
41
|
+
owner: string;
|
|
42
|
+
repo: string;
|
|
43
|
+
skillSlug: string;
|
|
44
|
+
ref?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Map of relative file paths to their string contents */
|
|
48
|
+
export type SkillFiles = Record<string, string>;
|
|
49
|
+
|
|
50
|
+
// ─── Display helpers ─────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const RISK_DISPLAY: Record<RiskLevel, string> = {
|
|
53
|
+
safe: "PASS",
|
|
54
|
+
low: "PASS",
|
|
55
|
+
medium: "WARN",
|
|
56
|
+
high: "FAIL",
|
|
57
|
+
critical: "FAIL",
|
|
58
|
+
unknown: "?",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
62
|
+
ath: "ATH",
|
|
63
|
+
socket: "Socket",
|
|
64
|
+
snyk: "Snyk",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function riskToDisplay(risk: RiskLevel): string {
|
|
68
|
+
return RISK_DISPLAY[risk] ?? "?";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function providerDisplayName(provider: string): string {
|
|
72
|
+
return PROVIDER_DISPLAY[provider] ?? provider;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function formatAuditBadges(auditData: SkillAuditData): string {
|
|
76
|
+
const providers = Object.keys(auditData);
|
|
77
|
+
if (providers.length === 0) return "Security: no audit data";
|
|
78
|
+
|
|
79
|
+
const badges = providers.map((provider) => {
|
|
80
|
+
const audit = auditData[provider]!;
|
|
81
|
+
const display = riskToDisplay(audit.risk);
|
|
82
|
+
const name = providerDisplayName(provider);
|
|
83
|
+
return `[${name}:${display}]`;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return `Security: ${badges.join(" ")}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── API clients ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export async function searchSkillsRegistry(
|
|
92
|
+
query: string,
|
|
93
|
+
limit?: number,
|
|
94
|
+
): Promise<SkillsShSearchResult[]> {
|
|
95
|
+
const params = new URLSearchParams({ q: query });
|
|
96
|
+
if (limit != null) {
|
|
97
|
+
params.set("limit", String(limit));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const url = `https://skills.sh/api/search?${params.toString()}`;
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
signal: AbortSignal.timeout(10_000),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`skills.sh search failed: HTTP ${response.status} ${response.statusText}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = (await response.json()) as { skills: SkillsShSearchResult[] };
|
|
112
|
+
return data.skills ?? [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function fetchSkillAudits(
|
|
116
|
+
source: string,
|
|
117
|
+
skillSlugs: string[],
|
|
118
|
+
): Promise<AuditResponse> {
|
|
119
|
+
if (skillSlugs.length === 0) return {};
|
|
120
|
+
|
|
121
|
+
const params = new URLSearchParams({
|
|
122
|
+
source,
|
|
123
|
+
skills: skillSlugs.join(","),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const url = `https://add-skill.vercel.sh/audit?${params.toString()}`;
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
signal: AbortSignal.timeout(10_000),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Audit fetch failed: HTTP ${response.status} ${response.statusText}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (await response.json()) as AuditResponse;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Source resolution ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parse a skill source string into owner, repo, and skill slug.
|
|
144
|
+
*
|
|
145
|
+
* Supported formats:
|
|
146
|
+
* - `owner/repo@skill-name`
|
|
147
|
+
* - `owner/repo/skill-name`
|
|
148
|
+
* - `https://github.com/owner/repo/tree/<branch>/skills/skill-name`
|
|
149
|
+
*/
|
|
150
|
+
export function resolveSkillSource(source: string): ResolvedSkillSource {
|
|
151
|
+
// Full GitHub URL — capture the branch for ref passthrough
|
|
152
|
+
// Branch capture uses non-greedy `.+?` to handle branch names with slashes (e.g. feature/new-flow)
|
|
153
|
+
const urlMatch = source.match(
|
|
154
|
+
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/(.+?)\/skills\/([a-z0-9][a-z0-9._-]*)\/?$/,
|
|
155
|
+
);
|
|
156
|
+
if (urlMatch) {
|
|
157
|
+
return {
|
|
158
|
+
owner: urlMatch[1]!,
|
|
159
|
+
repo: urlMatch[2]!,
|
|
160
|
+
skillSlug: urlMatch[4]!,
|
|
161
|
+
ref: urlMatch[3]!,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// owner/repo@skill-name — restrict slug to safe characters
|
|
166
|
+
const atMatch = source.match(/^([^/]+)\/([^/@]+)@([a-z0-9][a-z0-9._-]*)$/);
|
|
167
|
+
if (atMatch) {
|
|
168
|
+
return { owner: atMatch[1]!, repo: atMatch[2]!, skillSlug: atMatch[3]! };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// owner/repo/skill-name (exactly 3 segments) — restrict slug to safe characters
|
|
172
|
+
const slashMatch = source.match(/^([^/]+)\/([^/]+)\/([a-z0-9][a-z0-9._-]*)$/);
|
|
173
|
+
if (slashMatch) {
|
|
174
|
+
return {
|
|
175
|
+
owner: slashMatch[1]!,
|
|
176
|
+
repo: slashMatch[2]!,
|
|
177
|
+
skillSlug: slashMatch[3]!,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Invalid skill source "${source}". Expected one of:\n` +
|
|
183
|
+
` owner/repo@skill-name\n` +
|
|
184
|
+
` owner/repo/skill-name\n` +
|
|
185
|
+
` https://github.com/owner/repo/tree/<branch>/skills/skill-name`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── GitHub fetch ───────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
interface GitHubContentsEntry {
|
|
192
|
+
name: string;
|
|
193
|
+
type: "file" | "dir";
|
|
194
|
+
download_url: string | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Build common headers for GitHub API requests (User-Agent + optional auth). */
|
|
198
|
+
function githubHeaders(): Record<string, string> {
|
|
199
|
+
const headers: Record<string, string> = {
|
|
200
|
+
Accept: "application/vnd.github.v3+json",
|
|
201
|
+
"User-Agent": "vellum-assistant",
|
|
202
|
+
};
|
|
203
|
+
const token = process.env.GITHUB_TOKEN;
|
|
204
|
+
if (token) {
|
|
205
|
+
headers["Authorization"] = `token ${token}`;
|
|
206
|
+
}
|
|
207
|
+
return headers;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface GitHubTreeEntry {
|
|
211
|
+
path: string;
|
|
212
|
+
type: "blob" | "tree";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Search the repo tree for a directory containing `<slug>/SKILL.md`.
|
|
217
|
+
* Returns the directory path (e.g. "examples/skills-tool/skills/csv") or null.
|
|
218
|
+
*/
|
|
219
|
+
async function findSkillDirInTree(
|
|
220
|
+
owner: string,
|
|
221
|
+
repo: string,
|
|
222
|
+
skillSlug: string,
|
|
223
|
+
ref: string,
|
|
224
|
+
headers: Record<string, string>,
|
|
225
|
+
): Promise<string | null> {
|
|
226
|
+
const treeUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
|
227
|
+
const response = await fetch(treeUrl, {
|
|
228
|
+
headers,
|
|
229
|
+
signal: AbortSignal.timeout(15_000),
|
|
230
|
+
});
|
|
231
|
+
if (!response.ok) return null;
|
|
232
|
+
|
|
233
|
+
const data = (await response.json()) as { tree: GitHubTreeEntry[] };
|
|
234
|
+
const suffix = `${skillSlug}/SKILL.md`;
|
|
235
|
+
const match = data.tree.find(
|
|
236
|
+
(entry) =>
|
|
237
|
+
entry.type === "blob" &&
|
|
238
|
+
(entry.path === suffix || entry.path.endsWith(`/${suffix}`)),
|
|
239
|
+
);
|
|
240
|
+
if (!match) return null;
|
|
241
|
+
|
|
242
|
+
// Return the directory containing SKILL.md (strip the trailing /SKILL.md)
|
|
243
|
+
return match.path.slice(0, -"/SKILL.md".length);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fetch SKILL.md and supporting files from a GitHub-hosted skills directory.
|
|
248
|
+
*
|
|
249
|
+
* First tries the conventional `skills/<slug>/` path. If that returns a 404,
|
|
250
|
+
* falls back to searching the full repo tree for `<slug>/SKILL.md` at any
|
|
251
|
+
* depth (handles repos like `vercel-labs/bash-tool` where skills live at
|
|
252
|
+
* non-standard paths like `examples/skills-tool/skills/csv/`).
|
|
253
|
+
*
|
|
254
|
+
* Uses the GitHub Contents API for directory listing and file downloads.
|
|
255
|
+
* Recursively fetches subdirectories (e.g. scripts/, references/).
|
|
256
|
+
*/
|
|
257
|
+
export async function fetchSkillFromGitHub(
|
|
258
|
+
owner: string,
|
|
259
|
+
repo: string,
|
|
260
|
+
skillSlug: string,
|
|
261
|
+
ref?: string,
|
|
262
|
+
): Promise<SkillFiles> {
|
|
263
|
+
const headers = githubHeaders();
|
|
264
|
+
|
|
265
|
+
async function fetchDir(
|
|
266
|
+
subpath: string,
|
|
267
|
+
prefix: string,
|
|
268
|
+
): Promise<SkillFiles> {
|
|
269
|
+
let apiUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${subpath}`;
|
|
270
|
+
if (ref) {
|
|
271
|
+
apiUrl += `?ref=${encodeURIComponent(ref)}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const response = await fetch(apiUrl, {
|
|
275
|
+
headers,
|
|
276
|
+
signal: AbortSignal.timeout(15_000),
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`GitHub API error: HTTP ${response.status} ${response.statusText}`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const entries = (await response.json()) as GitHubContentsEntry[];
|
|
286
|
+
if (!Array.isArray(entries)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Expected a directory listing for ${subpath}/ but got a single file`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const files: SkillFiles = {};
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
295
|
+
|
|
296
|
+
if (entry.type === "dir") {
|
|
297
|
+
// Recursively fetch subdirectory contents
|
|
298
|
+
const subFiles = await fetchDir(
|
|
299
|
+
`${subpath}/${entry.name}`,
|
|
300
|
+
relativePath,
|
|
301
|
+
);
|
|
302
|
+
Object.assign(files, subFiles);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (entry.type !== "file" || !entry.download_url) continue;
|
|
307
|
+
const fileResponse = await fetch(entry.download_url, {
|
|
308
|
+
headers,
|
|
309
|
+
signal: AbortSignal.timeout(10_000),
|
|
310
|
+
});
|
|
311
|
+
if (!fileResponse.ok) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Failed to download ${relativePath}: HTTP ${fileResponse.status}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
files[relativePath] = await fileResponse.text();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return files;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Try the conventional skills/<slug>/ path first
|
|
323
|
+
const conventionalPath = `skills/${encodeURIComponent(skillSlug)}`;
|
|
324
|
+
let skillDirPath = conventionalPath;
|
|
325
|
+
|
|
326
|
+
const probeUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${conventionalPath}${ref ? `?ref=${encodeURIComponent(ref)}` : ""}`;
|
|
327
|
+
const probeResponse = await fetch(probeUrl, {
|
|
328
|
+
headers,
|
|
329
|
+
signal: AbortSignal.timeout(15_000),
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (probeResponse.status === 404) {
|
|
333
|
+
// Fall back to searching the repo tree for <slug>/SKILL.md at any path
|
|
334
|
+
const treeRef = ref ?? "HEAD";
|
|
335
|
+
const foundPath = await findSkillDirInTree(
|
|
336
|
+
owner,
|
|
337
|
+
repo,
|
|
338
|
+
skillSlug,
|
|
339
|
+
treeRef,
|
|
340
|
+
headers,
|
|
341
|
+
);
|
|
342
|
+
if (!foundPath) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Skill "${skillSlug}" not found in ${owner}/${repo}. ` +
|
|
345
|
+
`Searched skills/${skillSlug}/ and the full repo tree.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
skillDirPath = foundPath;
|
|
349
|
+
} else if (!probeResponse.ok) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`GitHub API error: HTTP ${probeResponse.status} ${probeResponse.statusText}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If we already have the probe response for the conventional path and it was
|
|
356
|
+
// successful, we can use it directly instead of re-fetching.
|
|
357
|
+
let files: SkillFiles;
|
|
358
|
+
if (skillDirPath === conventionalPath && probeResponse.ok) {
|
|
359
|
+
const entries = (await probeResponse.json()) as GitHubContentsEntry[];
|
|
360
|
+
if (!Array.isArray(entries)) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Expected a directory listing for ${conventionalPath}/ but got a single file`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
// Fetch the directory contents from the already-parsed probe response
|
|
366
|
+
const result: SkillFiles = {};
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
if (entry.type === "dir") {
|
|
369
|
+
const subFiles = await fetchDir(
|
|
370
|
+
`${conventionalPath}/${entry.name}`,
|
|
371
|
+
entry.name,
|
|
372
|
+
);
|
|
373
|
+
Object.assign(result, subFiles);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (entry.type !== "file" || !entry.download_url) continue;
|
|
377
|
+
const fileResponse = await fetch(entry.download_url, {
|
|
378
|
+
headers,
|
|
379
|
+
signal: AbortSignal.timeout(10_000),
|
|
380
|
+
});
|
|
381
|
+
if (!fileResponse.ok) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Failed to download ${entry.name}: HTTP ${fileResponse.status}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
result[entry.name] = await fileResponse.text();
|
|
387
|
+
}
|
|
388
|
+
files = result;
|
|
389
|
+
} else {
|
|
390
|
+
files = await fetchDir(skillDirPath, "");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (!files["SKILL.md"]) {
|
|
394
|
+
throw new Error(`SKILL.md not found in ${owner}/${repo}/${skillDirPath}/`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return files;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── External skill installation ────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
// ─── Slug validation ────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
const VALID_SKILL_SLUG = /^[a-z0-9][a-z0-9._-]*$/;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Validate that a skill slug is safe for use in filesystem paths.
|
|
408
|
+
* Follows the same pattern as `validateManagedSkillId` in managed-store.ts.
|
|
409
|
+
*/
|
|
410
|
+
export function validateSkillSlug(slug: string): void {
|
|
411
|
+
if (!slug || typeof slug !== "string") {
|
|
412
|
+
throw new Error("Skill slug is required");
|
|
413
|
+
}
|
|
414
|
+
if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`Invalid skill slug "${slug}": must not contain path traversal characters`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
if (!VALID_SKILL_SLUG.test(slug)) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Invalid skill slug "${slug}": must start with a lowercase letter or digit and contain only lowercase letters, digits, dots, hyphens, and underscores`,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Install a community skill from a GitHub-hosted skills.sh registry repo.
|
|
428
|
+
*
|
|
429
|
+
* 1. Validates the skill slug for path safety
|
|
430
|
+
* 2. Fetches all files from `skills/<skillSlug>/` in the source repo
|
|
431
|
+
* 3. Writes them to `<workspace>/skills/<skillSlug>/` with path traversal protection
|
|
432
|
+
* 4. Writes `version.json` with origin metadata
|
|
433
|
+
* 5. Runs `bun install` if a `package.json` is present
|
|
434
|
+
* 6. Registers the skill in SKILLS.md only after all steps succeed
|
|
435
|
+
*/
|
|
436
|
+
export async function installExternalSkill(
|
|
437
|
+
owner: string,
|
|
438
|
+
repo: string,
|
|
439
|
+
skillSlug: string,
|
|
440
|
+
overwrite: boolean,
|
|
441
|
+
ref?: string,
|
|
442
|
+
): Promise<void> {
|
|
443
|
+
// Validate slug before using in filesystem paths
|
|
444
|
+
validateSkillSlug(skillSlug);
|
|
445
|
+
|
|
446
|
+
const skillDir = join(getWorkspaceSkillsDir(), skillSlug);
|
|
447
|
+
const skillFilePath = join(skillDir, "SKILL.md");
|
|
448
|
+
|
|
449
|
+
if (existsSync(skillFilePath) && !overwrite) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Skill "${skillSlug}" is already installed. Use --overwrite to replace it.`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const files = await fetchSkillFromGitHub(owner, repo, skillSlug, ref);
|
|
456
|
+
|
|
457
|
+
// Clear existing directory on overwrite to remove stale files
|
|
458
|
+
if (overwrite && existsSync(skillDir)) {
|
|
459
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
mkdirSync(skillDir, { recursive: true });
|
|
462
|
+
|
|
463
|
+
// Write files with path traversal protection (follows extractTarToDir pattern)
|
|
464
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
465
|
+
const normalized = filename.replace(/\\/g, "/").replace(/^\.\/+/g, "");
|
|
466
|
+
if (!normalized || normalized.includes("..") || normalized.startsWith("/"))
|
|
467
|
+
continue;
|
|
468
|
+
const destPath = resolve(skillDir, normalized);
|
|
469
|
+
if (
|
|
470
|
+
!destPath.startsWith(resolve(skillDir) + sep) &&
|
|
471
|
+
destPath !== resolve(skillDir)
|
|
472
|
+
)
|
|
473
|
+
continue;
|
|
474
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
475
|
+
writeFileSync(destPath, content, "utf-8");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Write origin metadata
|
|
479
|
+
const meta = {
|
|
480
|
+
origin: "skills.sh",
|
|
481
|
+
source: `${owner}/${repo}`,
|
|
482
|
+
skillSlug,
|
|
483
|
+
installedAt: new Date().toISOString(),
|
|
484
|
+
};
|
|
485
|
+
writeFileSync(
|
|
486
|
+
join(skillDir, "version.json"),
|
|
487
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
488
|
+
"utf-8",
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Install npm dependencies if the skill ships a package.json
|
|
492
|
+
if (existsSync(join(skillDir, "package.json"))) {
|
|
493
|
+
const bunPath = `${homedir()}/.bun/bin`;
|
|
494
|
+
execSync("bun install", {
|
|
495
|
+
cwd: skillDir,
|
|
496
|
+
stdio: "inherit",
|
|
497
|
+
env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Register in SKILLS.md only after files are written and deps installed
|
|
502
|
+
upsertSkillsIndex(skillSlug);
|
|
503
|
+
}
|
|
@@ -22,8 +22,12 @@ import {
|
|
|
22
22
|
messageAttachments,
|
|
23
23
|
messages,
|
|
24
24
|
} from "../../memory/schema.js";
|
|
25
|
-
import { escapeLikeWildcards } from "../../memory/search/lexical.js";
|
|
26
25
|
import { RiskLevel } from "../../permissions/types.js";
|
|
26
|
+
|
|
27
|
+
/** Escape SQL LIKE wildcard characters so a literal substring match is used. */
|
|
28
|
+
function escapeLikeWildcards(s: string): string {
|
|
29
|
+
return s.replace(/%/g, "").replace(/_/g, "");
|
|
30
|
+
}
|
|
27
31
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
28
32
|
import { registerTool } from "../registry.js";
|
|
29
33
|
import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
|
|
@@ -499,13 +499,3 @@ export const allComputerUseTools: Tool[] = [
|
|
|
499
499
|
computerUseDoneTool,
|
|
500
500
|
computerUseRespondTool,
|
|
501
501
|
];
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* Tools safe for the legacy fallback path (no skill projection).
|
|
505
|
-
*
|
|
506
|
-
* Excludes `computer_use_observe` because the macOS client doesn't handle it
|
|
507
|
-
* in the legacy code path — it falls back to `.done` which skips sending an
|
|
508
|
-
* observation, causing the daemon to block on `pendingObservation` until timeout.
|
|
509
|
-
*/
|
|
510
|
-
export const legacyFallbackComputerUseTools: Tool[] =
|
|
511
|
-
allComputerUseTools.filter((t) => t.name !== "computer_use_observe");
|
|
@@ -11,7 +11,7 @@ import { registerTool } from "../registry.js";
|
|
|
11
11
|
import { allComputerUseTools } from "./definitions.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Register the
|
|
14
|
+
* Register the 11 `computer_use_*` action proxy tools.
|
|
15
15
|
* After cutover these are provided by the bundled computer-use skill instead.
|
|
16
16
|
*/
|
|
17
17
|
export function registerComputerUseActionTools(): void {
|
|
@@ -747,9 +747,7 @@ class CredentialStoreTool implements Tool {
|
|
|
747
747
|
if (dbApp) {
|
|
748
748
|
if (!clientId) clientId = dbApp.clientId;
|
|
749
749
|
if (!clientSecret) {
|
|
750
|
-
clientSecret = getSecureKey(
|
|
751
|
-
`oauth_app/${dbApp.id}/client_secret`,
|
|
752
|
-
);
|
|
750
|
+
clientSecret = getSecureKey(dbApp.clientSecretCredentialPath);
|
|
753
751
|
}
|
|
754
752
|
}
|
|
755
753
|
}
|
|
@@ -3,7 +3,7 @@ import type { ToolDefinition } from "../../providers/types.js";
|
|
|
3
3
|
export const memoryRecallDefinition: ToolDefinition = {
|
|
4
4
|
name: "memory_recall",
|
|
5
5
|
description:
|
|
6
|
-
"
|
|
6
|
+
"Hybrid search across memory (semantic and recency) for specific information. Use this when you need to recall details about past conversations, decisions, preferences, project context, or any prior knowledge. Returns formatted memory context with item IDs for use with memory_manage.",
|
|
7
7
|
input_schema: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
@@ -11,10 +11,6 @@ export const memoryRecallDefinition: ToolDefinition = {
|
|
|
11
11
|
type: "string",
|
|
12
12
|
description: "The search query — be specific and descriptive",
|
|
13
13
|
},
|
|
14
|
-
max_results: {
|
|
15
|
-
type: "number",
|
|
16
|
-
description: "Maximum number of memory items to return (default: 10)",
|
|
17
|
-
},
|
|
18
14
|
scope: {
|
|
19
15
|
type: "string",
|
|
20
16
|
enum: ["default", "conversation"],
|
|
@@ -44,17 +40,12 @@ const memoryManageProperties = {
|
|
|
44
40
|
kind: {
|
|
45
41
|
type: "string" as const,
|
|
46
42
|
enum: [
|
|
43
|
+
"identity",
|
|
47
44
|
"preference",
|
|
48
|
-
"
|
|
45
|
+
"project",
|
|
49
46
|
"decision",
|
|
50
|
-
"
|
|
51
|
-
"relationship",
|
|
47
|
+
"constraint",
|
|
52
48
|
"event",
|
|
53
|
-
"opinion",
|
|
54
|
-
"instruction",
|
|
55
|
-
"style",
|
|
56
|
-
"playbook",
|
|
57
|
-
"learning",
|
|
58
49
|
],
|
|
59
50
|
description: "Category of the memory item (required for save)",
|
|
60
51
|
},
|