@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -1,10 +1,11 @@
1
1
  import { execSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import {
4
3
  cpSync,
5
4
  existsSync,
6
5
  mkdirSync,
6
+ mkdtempSync,
7
7
  readFileSync,
8
+ realpathSync,
8
9
  renameSync,
9
10
  rmSync,
10
11
  writeFileSync,
@@ -14,6 +15,7 @@ import { dirname, join, posix, resolve, sep } from "node:path";
14
15
  import { gunzipSync } from "node:zlib";
15
16
 
16
17
  import { getPlatformBaseUrl } from "../config/env.js";
18
+ import { loadSkillCatalog } from "../config/skills.js";
17
19
  import { deleteSkillCapabilityNode } from "../memory/graph/capability-seed.js";
18
20
  import { getLogger } from "../util/logger.js";
19
21
  import { getWorkspaceSkillsDir } from "../util/platform.js";
@@ -32,6 +34,7 @@ export interface CatalogSkill {
32
34
  version?: string;
33
35
  updatedAt?: string;
34
36
  metadata?: {
37
+ emoji?: string;
35
38
  vellum?: {
36
39
  "display-name"?: string;
37
40
  "activation-hints"?: string[];
@@ -46,12 +49,6 @@ export interface CatalogManifest {
46
49
  skills: CatalogSkill[];
47
50
  }
48
51
 
49
- // ─── Path helpers ────────────────────────────────────────────────────────────
50
-
51
- function getSkillsIndexPath(): string {
52
- return join(getWorkspaceSkillsDir(), "SKILLS.md");
53
- }
54
-
55
52
  /**
56
53
  * Resolve the directory containing a `catalog.json` and first-party skill
57
54
  * sources — either bundled next to a compiled binary (e.g. `Vellum.app`) or
@@ -130,9 +127,63 @@ export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
130
127
 
131
128
  // ─── Tar extraction ──────────────────────────────────────────────────────────
132
129
 
130
+ interface SafeSkillInstallPath {
131
+ normalizedPath: string;
132
+ destPath: string;
133
+ }
134
+
135
+ function safeResolveSkillInstallPath(
136
+ destRoot: string,
137
+ relativePath: string,
138
+ ): SafeSkillInstallPath | null {
139
+ const normalizedName = relativePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
140
+ const normalizedPath = posix.normalize(normalizedName);
141
+ const hasWindowsDrivePrefix = /^[a-zA-Z]:\//.test(normalizedPath);
142
+ const isTraversal =
143
+ normalizedPath === ".." || normalizedPath.startsWith("../");
144
+
145
+ if (
146
+ !normalizedPath ||
147
+ normalizedPath === "." ||
148
+ normalizedPath.startsWith("/") ||
149
+ hasWindowsDrivePrefix ||
150
+ isTraversal
151
+ ) {
152
+ return null;
153
+ }
154
+
155
+ const resolvedDestRoot = resolve(destRoot);
156
+ const destPath = resolve(resolvedDestRoot, normalizedPath);
157
+ const insideDestination =
158
+ destPath === resolvedDestRoot ||
159
+ destPath.startsWith(resolvedDestRoot + sep);
160
+ if (!insideDestination) return null;
161
+
162
+ return { normalizedPath, destPath };
163
+ }
164
+
165
+ export function writeSkillFilesToDir(
166
+ files: Record<string, string | Buffer>,
167
+ destDir: string,
168
+ ): boolean {
169
+ let foundSkillMd = false;
170
+ for (const [relativePath, content] of Object.entries(files)) {
171
+ const resolved = safeResolveSkillInstallPath(destDir, relativePath);
172
+ if (!resolved) continue;
173
+
174
+ mkdirSync(dirname(resolved.destPath), { recursive: true });
175
+ writeFileSync(resolved.destPath, content);
176
+
177
+ if (resolved.normalizedPath === "SKILL.md") {
178
+ foundSkillMd = true;
179
+ }
180
+ }
181
+ return foundSkillMd;
182
+ }
183
+
133
184
  /**
134
185
  * Extract all files from a tar archive (uncompressed) into a directory.
135
- * Returns true if a SKILL.md was found in the archive.
186
+ * Returns true if a top-level SKILL.md was found in the archive.
136
187
  */
137
188
  export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
138
189
  let foundSkillMd = false;
@@ -160,38 +211,15 @@ export function extractTarToDir(tarBuffer: Buffer, destDir: string): boolean {
160
211
 
161
212
  // Skip directories and empty names
162
213
  if (name && typeFlag !== 53 /* '5' */) {
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
-
186
- mkdirSync(dirname(destPath), { recursive: true });
187
- writeFileSync(destPath, tarBuffer.subarray(offset, offset + size));
188
-
189
- if (
190
- normalizedPath === "SKILL.md" ||
191
- normalizedPath.endsWith("/SKILL.md")
192
- ) {
193
- foundSkillMd = true;
194
- }
214
+ const resolved = safeResolveSkillInstallPath(destDir, name);
215
+ if (resolved) {
216
+ mkdirSync(dirname(resolved.destPath), { recursive: true });
217
+ writeFileSync(
218
+ resolved.destPath,
219
+ tarBuffer.subarray(offset, offset + size),
220
+ );
221
+
222
+ if (resolved.normalizedPath === "SKILL.md") foundSkillMd = true;
195
223
  }
196
224
  }
197
225
 
@@ -226,61 +254,147 @@ async function fetchAndExtractSkill(
226
254
  }
227
255
  }
228
256
 
229
- // ─── SKILLS.md index management ──────────────────────────────────────────────
257
+ // ─── Install / uninstall ─────────────────────────────────────────────────────
230
258
 
231
- function atomicWriteFile(filePath: string, content: string): void {
232
- const dir = dirname(filePath);
233
- mkdirSync(dir, { recursive: true });
234
- const tmpPath = join(dir, `.tmp-${randomUUID()}`);
235
- writeFileSync(tmpPath, content, "utf-8");
236
- renameSync(tmpPath, filePath);
237
- }
259
+ export function uninstallSkillLocally(skillId: string): void {
260
+ const skillDir = join(getWorkspaceSkillsDir(), skillId);
238
261
 
239
- export function upsertSkillsIndex(id: string): void {
240
- const indexPath = getSkillsIndexPath();
241
- let lines: string[] = [];
242
- if (existsSync(indexPath)) {
243
- lines = readFileSync(indexPath, "utf-8").split("\n");
262
+ if (!existsSync(skillDir)) {
263
+ throw new Error(`Skill "${skillId}" is not installed.`);
244
264
  }
245
265
 
246
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
247
- const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
248
- if (lines.some((line) => pattern.test(line))) return;
266
+ rmSync(skillDir, { recursive: true, force: true });
267
+ deleteSkillCapabilityNode(skillId);
268
+ }
249
269
 
250
- const nonEmpty = lines.filter((l) => l.trim());
251
- nonEmpty.push(`- ${id}`);
252
- const content = nonEmpty.join("\n");
253
- atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
270
+ function assertInstalledSkillDiscoverable(
271
+ skillId: string,
272
+ skillDir = join(getWorkspaceSkillsDir(), skillId),
273
+ ): void {
274
+ const skillFilePath = join(skillDir, "SKILL.md");
275
+ if (!existsSync(skillFilePath)) {
276
+ throw new Error(
277
+ `Installed skill "${skillId}" is missing SKILL.md at the skill root`,
278
+ );
279
+ }
280
+
281
+ const discovered = loadSkillCatalog().some((skill) => {
282
+ if (skill.id !== skillId) return false;
283
+ try {
284
+ return realpathSync(skill.directoryPath) === realpathSync(skillDir);
285
+ } catch {
286
+ return skill.directoryPath === skillDir;
287
+ }
288
+ });
289
+ if (!discovered) {
290
+ throw new Error(
291
+ `Installed skill "${skillId}" was not discovered by the skill catalog`,
292
+ );
293
+ }
254
294
  }
255
295
 
256
- function removeSkillsIndexEntry(id: string): void {
257
- const indexPath = getSkillsIndexPath();
258
- if (!existsSync(indexPath)) return;
296
+ function getInstallStagingRoot(): string {
297
+ return join(getWorkspaceSkillsDir(), ".install-staging");
298
+ }
259
299
 
260
- const lines = readFileSync(indexPath, "utf-8").split("\n");
261
- const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
262
- const pattern = new RegExp(`^[-*]\\s+(?:\`)?${escaped}(?:\`)?\\s*$`);
263
- const filtered = lines.filter((line) => !pattern.test(line));
300
+ export function createSkillInstallStagingDir(): string {
301
+ const stagingRoot = getInstallStagingRoot();
302
+ mkdirSync(stagingRoot, { recursive: true });
303
+ return mkdtempSync(join(stagingRoot, "skill-"));
304
+ }
264
305
 
265
- // If nothing changed, skip the write
266
- if (filtered.length === lines.length) return;
306
+ function createSkillInstallBackupPath(): string {
307
+ const stagingRoot = getInstallStagingRoot();
308
+ mkdirSync(stagingRoot, { recursive: true });
309
+ return join(
310
+ stagingRoot,
311
+ `backup-${Date.now()}-${Math.random().toString(36).slice(2)}`,
312
+ );
313
+ }
267
314
 
268
- const content = filtered.join("\n");
269
- atomicWriteFile(indexPath, content.endsWith("\n") ? content : content + "\n");
315
+ function assertStagedSkillRoot(skillId: string, stagedDir: string): void {
316
+ if (!existsSync(join(stagedDir, "SKILL.md"))) {
317
+ throw new Error(
318
+ `Installed skill "${skillId}" is missing SKILL.md at the skill root`,
319
+ );
320
+ }
270
321
  }
271
322
 
272
- // ─── Install / uninstall ─────────────────────────────────────────────────────
323
+ export function installSkillDependenciesIfPresent(skillDir: string): void {
324
+ if (existsSync(join(skillDir, "package.json"))) {
325
+ const bunPath = `${homedir()}/.bun/bin`;
326
+ execSync("bun install", {
327
+ cwd: skillDir,
328
+ stdio: "inherit",
329
+ env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
330
+ });
331
+ }
332
+ }
273
333
 
274
- export function uninstallSkillLocally(skillId: string): void {
334
+ function restoreOrRemoveFailedSkillInstall(
335
+ skillId: string,
336
+ backupDir: string | null,
337
+ ): void {
275
338
  const skillDir = join(getWorkspaceSkillsDir(), skillId);
339
+ rmSync(skillDir, { recursive: true, force: true });
340
+ if (backupDir) {
341
+ renameSync(backupDir, skillDir);
342
+ }
343
+ }
276
344
 
277
- if (!existsSync(skillDir)) {
278
- throw new Error(`Skill "${skillId}" is not installed.`);
345
+ function discardSkillInstallBackup(backupDir: string | null): void {
346
+ if (backupDir) {
347
+ rmSync(backupDir, { recursive: true, force: true });
279
348
  }
349
+ }
280
350
 
281
- rmSync(skillDir, { recursive: true, force: true });
282
- removeSkillsIndexEntry(skillId);
283
- deleteSkillCapabilityNode(skillId);
351
+ function snapshotExistingSkillDir(skillId: string): string | null {
352
+ const skillDir = join(getWorkspaceSkillsDir(), skillId);
353
+ if (!existsSync(skillDir)) return null;
354
+
355
+ const backupDir = createSkillInstallBackupPath();
356
+ renameSync(skillDir, backupDir);
357
+ return backupDir;
358
+ }
359
+
360
+ export function commitStagedSkillInstall(
361
+ skillId: string,
362
+ stagedDir: string,
363
+ ): void {
364
+ assertStagedSkillRoot(skillId, stagedDir);
365
+
366
+ const skillDir = join(getWorkspaceSkillsDir(), skillId);
367
+ let backupDir: string | null = null;
368
+ let stagedMovedToFinal = false;
369
+
370
+ try {
371
+ backupDir = snapshotExistingSkillDir(skillId);
372
+ renameSync(stagedDir, skillDir);
373
+ stagedMovedToFinal = true;
374
+ assertInstalledSkillDiscoverable(skillId, skillDir);
375
+ discardSkillInstallBackup(backupDir);
376
+ } catch (err) {
377
+ const originalMessage = err instanceof Error ? err.message : String(err);
378
+ let restoreError: unknown;
379
+ if (backupDir || stagedMovedToFinal) {
380
+ try {
381
+ restoreOrRemoveFailedSkillInstall(skillId, backupDir);
382
+ } catch (restoreErr) {
383
+ restoreError = restoreErr;
384
+ }
385
+ }
386
+ rmSync(stagedDir, { recursive: true, force: true });
387
+ if (restoreError) {
388
+ const restoreMessage =
389
+ restoreError instanceof Error
390
+ ? restoreError.message
391
+ : String(restoreError);
392
+ throw new Error(
393
+ `${originalMessage}; failed to restore previous skill: ${restoreMessage}`,
394
+ );
395
+ }
396
+ throw err;
397
+ }
284
398
  }
285
399
 
286
400
  export async function installSkillLocally(
@@ -298,7 +412,7 @@ export async function installSkillLocally(
298
412
  );
299
413
  }
300
414
 
301
- mkdirSync(skillDir, { recursive: true });
415
+ const stagedDir = createSkillInstallStagingDir();
302
416
 
303
417
  // In dev mode, install from the local repo skills directory if available
304
418
  const repoSkillsDir = getRepoSkillsDir();
@@ -307,40 +421,37 @@ export async function installSkillLocally(
307
421
  : undefined;
308
422
 
309
423
  let installSource: "repo" | "platform";
310
- if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
311
- installSource = "repo";
312
- cpSync(repoSkillSource, skillDir, { recursive: true });
313
- } else {
314
- installSource = "platform";
315
- await fetchAndExtractSkill(skillId, skillDir);
316
- }
317
- log.info(
318
- { skillId, source: installSource },
319
- "Installed skill from %s",
320
- installSource,
321
- );
424
+ try {
425
+ if (repoSkillSource && existsSync(join(repoSkillSource, "SKILL.md"))) {
426
+ installSource = "repo";
427
+ cpSync(repoSkillSource, stagedDir, { recursive: true });
428
+ } else {
429
+ installSource = "platform";
430
+ await fetchAndExtractSkill(skillId, stagedDir);
431
+ }
322
432
 
323
- // Write install metadata
324
- writeInstallMeta(skillDir, {
325
- origin: "vellum",
326
- installedAt: new Date().toISOString(),
327
- ...(catalogEntry.version ? { version: catalogEntry.version } : {}),
328
- ...(contactId ? { installedBy: contactId } : {}),
329
- contentHash: computeSkillHash(skillDir) ?? undefined,
330
- });
433
+ assertStagedSkillRoot(skillId, stagedDir);
331
434
 
332
- // Post-install: install dependencies first, then index the skill.
333
- // Running bun install before upsertSkillsIndex ensures we don't index a
334
- // skill whose dependencies failed to install.
335
- if (existsSync(join(skillDir, "package.json"))) {
336
- const bunPath = `${homedir()}/.bun/bin`;
337
- execSync("bun install", {
338
- cwd: skillDir,
339
- stdio: "inherit",
340
- env: { ...process.env, PATH: `${bunPath}:${process.env.PATH}` },
435
+ writeInstallMeta(stagedDir, {
436
+ origin: "vellum",
437
+ installedAt: new Date().toISOString(),
438
+ ...(catalogEntry.version ? { version: catalogEntry.version } : {}),
439
+ ...(contactId ? { installedBy: contactId } : {}),
440
+ contentHash: computeSkillHash(stagedDir) ?? undefined,
341
441
  });
442
+
443
+ installSkillDependenciesIfPresent(stagedDir);
444
+ commitStagedSkillInstall(skillId, stagedDir);
445
+
446
+ log.info(
447
+ { skillId, source: installSource },
448
+ "Installed skill from %s",
449
+ installSource,
450
+ );
451
+ } catch (err) {
452
+ rmSync(stagedDir, { recursive: true, force: true });
453
+ throw err;
342
454
  }
343
- upsertSkillsIndex(skillId);
344
455
  }
345
456
 
346
457
  // ─── Auto-install (for skill_load) ──────────────────────────────────────────
@@ -379,7 +490,12 @@ export async function resolveCatalog(
379
490
  const localIds = new Set(local.map((s) => s.id));
380
491
  const merged = [...local, ...remote.filter((s) => !localIds.has(s.id))];
381
492
  log.info(
382
- { skillId, source: "merged", localCount: local.length, remoteCount: remote.length },
493
+ {
494
+ skillId,
495
+ source: "merged",
496
+ localCount: local.length,
497
+ remoteCount: remote.length,
498
+ },
383
499
  "Resolved skills catalog from local+remote merge",
384
500
  );
385
501
  return merged;
@@ -393,7 +509,10 @@ export async function resolveCatalog(
393
509
  }
394
510
  }
395
511
 
396
- log.info({ skillId, source: "remote" }, "Resolved skills catalog from platform API");
512
+ log.info(
513
+ { skillId, source: "remote" },
514
+ "Resolved skills catalog from platform API",
515
+ );
397
516
  return fetchCatalog();
398
517
  }
399
518
 
@@ -430,16 +549,14 @@ export async function autoInstallFromCatalog(
430
549
  return false;
431
550
  }
432
551
 
433
- // If the skill already exists on disk (stale index), re-index it instead
434
- // of attempting a fresh install that would fail.
552
+ // If the skill already exists on disk, reuse it instead of attempting a
553
+ // fresh install that would fail.
435
554
  const skillDir = join(getWorkspaceSkillsDir(), skillId);
436
555
  if (existsSync(join(skillDir, "SKILL.md"))) {
437
- log.info({ skillId, source: "disk-reindex" }, "Skill already on disk, re-indexing");
438
- upsertSkillsIndex(skillId);
556
+ log.info({ skillId, source: "disk" }, "Skill already on disk");
439
557
  return true;
440
558
  }
441
559
 
442
- // installSkillLocally handles dependency installation and SKILLS.md indexing.
443
560
  await installSkillLocally(skillId, entry, false);
444
561
 
445
562
  return true;
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import { getLogger } from "../util/logger.js";
@@ -133,12 +133,17 @@ export function verifyAndRecordSkillHash(slug: string): void {
133
133
  }
134
134
  }
135
135
 
136
- interface ClawhubInstallResult {
137
- success: boolean;
138
- skillName?: string;
139
- version?: string;
140
- error?: string;
141
- }
136
+ type ClawhubInstallResult =
137
+ | {
138
+ success: true;
139
+ skillId: string;
140
+ skillDir: string;
141
+ version?: string;
142
+ }
143
+ | {
144
+ success: false;
145
+ error: string;
146
+ };
142
147
 
143
148
  interface ClawhubSearchResultItem {
144
149
  name: string;
@@ -219,9 +224,50 @@ async function runClawhub(
219
224
  return { stdout, stderr, exitCode };
220
225
  }
221
226
 
227
+ function getSkillNameFromSlug(slug: string): string {
228
+ return slug.includes("/") ? slug.split("/").pop()! : slug;
229
+ }
230
+
231
+ function getClawhubSkillsDir(projectRoot?: string): string {
232
+ return projectRoot ? join(projectRoot, "skills") : getManagedSkillsDir();
233
+ }
234
+
235
+ function hasRootSkillFile(skillDir: string): boolean {
236
+ return existsSync(join(skillDir, "SKILL.md"));
237
+ }
238
+
239
+ function findInstalledClawhubSkillDir(
240
+ slug: string,
241
+ projectRoot?: string,
242
+ ): { skillId: string; skillDir: string } | null {
243
+ const skillsDir = getClawhubSkillsDir(projectRoot);
244
+ const expectedSkillName = getSkillNameFromSlug(slug);
245
+ const candidates = [
246
+ join(skillsDir, slug),
247
+ join(skillsDir, expectedSkillName),
248
+ ];
249
+
250
+ for (const candidate of candidates) {
251
+ if (hasRootSkillFile(candidate)) {
252
+ return { skillId: expectedSkillName, skillDir: candidate };
253
+ }
254
+ }
255
+
256
+ if (!projectRoot || !existsSync(skillsDir)) return null;
257
+
258
+ const stagedSkills = readdirSync(skillsDir, { withFileTypes: true }).filter(
259
+ (entry) =>
260
+ entry.isDirectory() && hasRootSkillFile(join(skillsDir, entry.name)),
261
+ );
262
+ if (stagedSkills.length !== 1) return null;
263
+
264
+ const skillId = stagedSkills[0]!.name;
265
+ return { skillId, skillDir: join(skillsDir, skillId) };
266
+ }
267
+
222
268
  export async function clawhubInstall(
223
269
  slug: string,
224
- opts?: { version?: string; contactId?: string },
270
+ opts?: { version?: string; contactId?: string; projectRoot?: string },
225
271
  ): Promise<ClawhubInstallResult> {
226
272
  if (!validateSlug(slug)) {
227
273
  return { success: false, error: `Invalid skill slug: ${slug}` };
@@ -231,26 +277,37 @@ export async function clawhubInstall(
231
277
  const args = ["install", installSlug, "--force"]; // non-interactive
232
278
 
233
279
  try {
234
- const result = await runClawhub(args);
280
+ const result = await runClawhub(args, { cwd: opts?.projectRoot });
235
281
  if (result.exitCode !== 0) {
236
282
  const error =
237
283
  result.stderr.trim() || result.stdout.trim() || "Unknown error";
238
284
  return { success: false, error };
239
285
  }
240
286
 
287
+ const installed = findInstalledClawhubSkillDir(slug, opts?.projectRoot);
288
+ if (!installed) {
289
+ return {
290
+ success: false,
291
+ error: `Installed skill "${slug}" is missing SKILL.md at the skill root`,
292
+ };
293
+ }
294
+
241
295
  // Write install-meta.json for the installed skill.
242
296
  // contentHash is included here, so there's no need to call
243
297
  // verifyAndRecordSkillHash() — it would just rewrite the same data.
244
- const skillDir = join(getManagedSkillsDir(), slug);
245
- writeInstallMeta(skillDir, {
298
+ writeInstallMeta(installed.skillDir, {
246
299
  origin: "clawhub",
247
300
  slug,
248
301
  installedAt: new Date().toISOString(),
249
302
  ...(opts?.contactId ? { installedBy: opts.contactId } : {}),
250
- contentHash: computeSkillHash(skillDir) ?? undefined,
303
+ contentHash: computeSkillHash(installed.skillDir) ?? undefined,
251
304
  });
252
305
 
253
- return { success: true, skillName: slug };
306
+ return {
307
+ success: true,
308
+ skillId: installed.skillId,
309
+ skillDir: installed.skillDir,
310
+ };
254
311
  } catch (err) {
255
312
  const message = err instanceof Error ? err.message : String(err);
256
313
  return { success: false, error: message };