@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.
Files changed (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. 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 12 `computer_use_*` action proxy tools.
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
- "Deep search across all memory sources (semantic, lexical, entity graph, 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.",
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
- "fact",
45
+ "project",
49
46
  "decision",
50
- "profile",
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
  },