@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.
Files changed (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -10,7 +10,7 @@ import {
10
10
  writeFileSync,
11
11
  } from "node:fs";
12
12
  import { homedir } from "node:os";
13
- import { dirname, join } from "node:path";
13
+ import { dirname, join, posix, resolve, sep } from "node:path";
14
14
  import { gunzipSync } from "node:zlib";
15
15
 
16
16
  import { getLogger } from "../util/logger.js";
@@ -160,16 +160,35 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
160
160
 
161
161
  // Skip directories and empty names
162
162
  if (name && typeFlag !== 53 /* '5' */) {
163
- // Prevent path traversal
164
- const normalizedName = name.replace(/^\.\//, "");
165
- if (!normalizedName.startsWith("..") && !normalizedName.includes("/..")) {
166
- const destPath = join(destDir, normalizedName);
163
+ // Prevent path traversal and absolute path writes
164
+ const normalizedName = name.replace(/\\/g, "/").replace(/^\.\/+/, "");
165
+ const normalizedPath = posix.normalize(normalizedName);
166
+ const hasWindowsDrivePrefix = /^[a-zA-Z]:\//.test(normalizedPath);
167
+ const isTraversal =
168
+ normalizedPath === ".." || normalizedPath.startsWith("../");
169
+
170
+ if (
171
+ normalizedPath &&
172
+ normalizedPath !== "." &&
173
+ !normalizedPath.startsWith("/") &&
174
+ !hasWindowsDrivePrefix &&
175
+ !isTraversal
176
+ ) {
177
+ const destRoot = resolve(destDir);
178
+ const destPath = resolve(destRoot, normalizedPath);
179
+ const insideDestination =
180
+ destPath === destRoot || destPath.startsWith(destRoot + sep);
181
+ if (!insideDestination) {
182
+ offset += Math.ceil(size / 512) * 512;
183
+ continue;
184
+ }
185
+
167
186
  mkdirSync(dirname(destPath), { recursive: true });
168
187
  writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
169
188
 
170
189
  if (
171
- normalizedName === "SKILL.md" ||
172
- normalizedName.endsWith("/SKILL.md")
190
+ normalizedPath === "SKILL.md" ||
191
+ normalizedPath.endsWith("/SKILL.md")
173
192
  ) {
174
193
  foundSkillMd = true;
175
194
  }
@@ -319,27 +338,63 @@ export async function installSkillLocally(
319
338
 
320
339
  // ─── Auto-install (for skill_load) ──────────────────────────────────────────
321
340
 
341
+ /**
342
+ * Resolve the catalog skill list, checking local (dev mode) first, then remote.
343
+ *
344
+ * In dev mode with a local catalog, returns local entries immediately to avoid
345
+ * unnecessary network latency. Pass `skillId` to trigger a deferred remote
346
+ * fetch only when the requested skill is not found locally — this preserves the
347
+ * ability to discover remote-only skills without penalising every call with a
348
+ * 10s timeout on flaky networks.
349
+ *
350
+ * Callers that install multiple skills in a loop should call this once and pass
351
+ * the result to `autoInstallFromCatalog` to avoid redundant network requests.
352
+ */
353
+ export async function resolveCatalog(
354
+ skillId?: string,
355
+ ): Promise<CatalogSkill[]> {
356
+ const repoSkillsDir = getRepoSkillsDir();
357
+ if (repoSkillsDir) {
358
+ const local = readLocalCatalog(repoSkillsDir);
359
+ if (local.length > 0) {
360
+ // If no specific skill requested, or it exists locally, skip remote fetch
361
+ if (!skillId || local.some((s) => s.id === skillId)) {
362
+ return local;
363
+ }
364
+ // Skill not found locally — merge with remote so remote-only skills
365
+ // can still be discovered. Local entries take precedence by id.
366
+ try {
367
+ const remote = await fetchCatalog();
368
+ const localIds = new Set(local.map((s) => s.id));
369
+ return [...local, ...remote.filter((s) => !localIds.has(s.id))];
370
+ } catch {
371
+ return local;
372
+ }
373
+ }
374
+ }
375
+
376
+ return fetchCatalog();
377
+ }
378
+
322
379
  /**
323
380
  * Attempt to find and install a skill from the first-party catalog.
324
381
  * Returns true if the skill was installed, false if not found in catalog.
325
382
  * Throws on install failures (network, filesystem, etc).
383
+ *
384
+ * When `catalog` is provided it is used directly, avoiding a redundant
385
+ * network fetch — pass a pre-resolved catalog when calling in a loop.
326
386
  */
327
387
  export async function autoInstallFromCatalog(
328
388
  skillId: string,
389
+ catalog?: CatalogSkill[],
329
390
  ): Promise<boolean> {
330
- // Check local catalog first (dev mode), then remote
331
- const repoSkillsDir = getRepoSkillsDir();
332
- let entry: CatalogSkill | undefined;
391
+ let skills: CatalogSkill[];
333
392
 
334
- if (repoSkillsDir) {
335
- const localCatalog = readLocalCatalog(repoSkillsDir);
336
- entry = localCatalog.find((s) => s.id === skillId);
337
- }
338
-
339
- if (!entry) {
393
+ if (catalog) {
394
+ skills = catalog;
395
+ } else {
340
396
  try {
341
- const remoteCatalog = await fetchCatalog();
342
- entry = remoteCatalog.find((s) => s.id === skillId);
397
+ skills = await resolveCatalog(skillId);
343
398
  } catch (err) {
344
399
  log.warn(
345
400
  { err, skillId },
@@ -349,6 +404,7 @@ export async function autoInstallFromCatalog(
349
404
  }
350
405
  }
351
406
 
407
+ const entry = skills.find((s) => s.id === skillId);
352
408
  if (!entry) {
353
409
  return false;
354
410
  }
@@ -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 {