@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
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Tests for {@link uninstallPlugin}.
3
+ *
4
+ * Each test materializes a temp workspace plugins directory and points
5
+ * `uninstallPlugin` at it via the `workspacePluginsDir` option.
6
+ */
7
+
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ readdirSync,
13
+ rmSync,
14
+ symlinkSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
20
+
21
+ import { InvalidPluginNameError } from "../install-from-github.js";
22
+ import {
23
+ PluginNotInstalledError,
24
+ uninstallPlugin,
25
+ } from "../uninstall-plugin.js";
26
+
27
+ let pluginsDir: string;
28
+
29
+ beforeEach(() => {
30
+ pluginsDir = mkdtempSync(join(tmpdir(), "plugins-uninstall-"));
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(pluginsDir, { recursive: true, force: true });
35
+ });
36
+
37
+ function writePlugin(name: string): string {
38
+ const target = join(pluginsDir, name);
39
+ mkdirSync(join(target, "hooks"), { recursive: true });
40
+ writeFileSync(
41
+ join(target, "package.json"),
42
+ JSON.stringify({ name, version: "0.0.1" }),
43
+ );
44
+ writeFileSync(
45
+ join(target, "hooks", "init.ts"),
46
+ "export async function init() {}\n",
47
+ );
48
+ return target;
49
+ }
50
+
51
+ describe("uninstallPlugin", () => {
52
+ test("removes the install target recursively", () => {
53
+ const target = writePlugin("simple-memory");
54
+ expect(existsSync(target)).toBe(true);
55
+
56
+ const result = uninstallPlugin({
57
+ name: "simple-memory",
58
+ workspacePluginsDir: pluginsDir,
59
+ });
60
+
61
+ expect(result).toEqual({ name: "simple-memory", target });
62
+ expect(existsSync(target)).toBe(false);
63
+ expect(readdirSync(pluginsDir)).toEqual([]);
64
+ });
65
+
66
+ test("throws PluginNotInstalledError when no directory exists", () => {
67
+ expect(() =>
68
+ uninstallPlugin({
69
+ name: "ghost",
70
+ workspacePluginsDir: pluginsDir,
71
+ }),
72
+ ).toThrow(PluginNotInstalledError);
73
+ });
74
+
75
+ test("throws PluginNotInstalledError when the target is a regular file", () => {
76
+ writeFileSync(join(pluginsDir, "trap"), "not a plugin");
77
+
78
+ expect(() =>
79
+ uninstallPlugin({
80
+ name: "trap",
81
+ workspacePluginsDir: pluginsDir,
82
+ }),
83
+ ).toThrow(PluginNotInstalledError);
84
+ });
85
+
86
+ test("removes a symlinked plugin without touching the link target", () => {
87
+ const real = mkdtempSync(join(tmpdir(), "real-plugin-"));
88
+ try {
89
+ writeFileSync(
90
+ join(real, "package.json"),
91
+ JSON.stringify({ name: "linked", version: "0.0.1" }),
92
+ );
93
+ symlinkSync(real, join(pluginsDir, "linked"));
94
+
95
+ uninstallPlugin({
96
+ name: "linked",
97
+ workspacePluginsDir: pluginsDir,
98
+ });
99
+
100
+ expect(existsSync(join(pluginsDir, "linked"))).toBe(false);
101
+ // Real directory and its files remain — rm only removed the symlink.
102
+ expect(existsSync(real)).toBe(true);
103
+ expect(existsSync(join(real, "package.json"))).toBe(true);
104
+ } finally {
105
+ rmSync(real, { recursive: true, force: true });
106
+ }
107
+ });
108
+
109
+ test.each([
110
+ ["../escape"],
111
+ ["/abs/path"],
112
+ [".hidden"],
113
+ ["Name-WithCaps"],
114
+ ["space name"],
115
+ [""],
116
+ ])("rejects invalid plugin name %p before touching the filesystem", (bad) => {
117
+ // Salt the plugins dir with siblings to prove we don't blow them away.
118
+ writePlugin("real-plugin");
119
+ expect(() =>
120
+ uninstallPlugin({ name: bad, workspacePluginsDir: pluginsDir }),
121
+ ).toThrow(InvalidPluginNameError);
122
+ expect(existsSync(join(pluginsDir, "real-plugin"))).toBe(true);
123
+ });
124
+ });
@@ -0,0 +1,106 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ import {
6
+ detectUnknownCommand,
7
+ findClosestCommand,
8
+ formatUnknownCommandMessage,
9
+ knownCommandNames,
10
+ } from "../unknown-command.js";
11
+
12
+ function buildToyProgram(): Command {
13
+ const program = new Command();
14
+ program.name("assistant").description("toy");
15
+ program.command("status").description("show status");
16
+ program.command("config").description("manage config");
17
+ program.command("pending").alias("ls").description("inspect pending");
18
+ return program;
19
+ }
20
+
21
+ describe("knownCommandNames", () => {
22
+ it("collects command names and aliases", () => {
23
+ const names = knownCommandNames(buildToyProgram());
24
+ expect([...names].sort()).toEqual(["config", "ls", "pending", "status"]);
25
+ });
26
+ });
27
+
28
+ describe("findClosestCommand", () => {
29
+ it("returns the closest match within 40% distance", () => {
30
+ expect(findClosestCommand("confg", ["config", "status"])).toBe("config");
31
+ });
32
+
33
+ it("returns undefined when nothing is close enough", () => {
34
+ expect(findClosestCommand("xyzzy", ["config", "status"])).toBeUndefined();
35
+ });
36
+
37
+ it("is case-insensitive for comparison", () => {
38
+ expect(findClosestCommand("STATUS", ["status"])).toBe("status");
39
+ });
40
+ });
41
+
42
+ describe("detectUnknownCommand", () => {
43
+ const program = buildToyProgram();
44
+
45
+ it("returns null when no positional is present (root --help / --version)", () => {
46
+ expect(detectUnknownCommand(program, ["--help"])).toBeNull();
47
+ expect(detectUnknownCommand(program, ["-V"])).toBeNull();
48
+ expect(detectUnknownCommand(program, [])).toBeNull();
49
+ });
50
+
51
+ it("returns null when the first positional is a known command", () => {
52
+ expect(detectUnknownCommand(program, ["status"])).toBeNull();
53
+ expect(detectUnknownCommand(program, ["status", "--json"])).toBeNull();
54
+ });
55
+
56
+ it("returns null when the first positional is a registered alias", () => {
57
+ expect(detectUnknownCommand(program, ["ls"])).toBeNull();
58
+ });
59
+
60
+ it("flags an unknown first positional even when --help follows", () => {
61
+ expect(detectUnknownCommand(program, ["invalid", "--help"])).toEqual({
62
+ token: "invalid",
63
+ });
64
+ });
65
+
66
+ it("flags an unknown first positional even when --help precedes it", () => {
67
+ expect(detectUnknownCommand(program, ["--help", "invalid"])).toEqual({
68
+ token: "invalid",
69
+ });
70
+ });
71
+
72
+ it("includes a suggestion when the unknown token is close to a known one", () => {
73
+ expect(detectUnknownCommand(program, ["confg", "--help"])).toEqual({
74
+ token: "confg",
75
+ suggestion: "config",
76
+ });
77
+ });
78
+
79
+ it("omits suggestion when nothing is close enough", () => {
80
+ expect(detectUnknownCommand(program, ["xyzzy"])).toEqual({
81
+ token: "xyzzy",
82
+ });
83
+ });
84
+ });
85
+
86
+ describe("formatUnknownCommandMessage", () => {
87
+ it("emits two lines when there is no suggestion", () => {
88
+ const msg = formatUnknownCommandMessage({ token: "invalid" });
89
+ expect(msg.split("\n")).toEqual([
90
+ "unknown command 'invalid'",
91
+ "Run 'assistant --help' to see a list of available commands.",
92
+ ]);
93
+ });
94
+
95
+ it("inserts the suggestion line between the header and footer", () => {
96
+ const msg = formatUnknownCommandMessage({
97
+ token: "confg",
98
+ suggestion: "config",
99
+ });
100
+ expect(msg.split("\n")).toEqual([
101
+ "unknown command 'confg'",
102
+ "(Did you mean 'config'?)",
103
+ "Run 'assistant --help' to see a list of available commands.",
104
+ ]);
105
+ });
106
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Minimal ANSI red wrapper for CLI error output. Respects `NO_COLOR`
3
+ * (https://no-color.org/) and skips coloring when stderr is not a TTY so
4
+ * piped/captured output stays clean.
5
+ */
6
+ export function red(text: string): string {
7
+ if (!process.stderr.isTTY) return text;
8
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
9
+ return text;
10
+ }
11
+ return `\x1b[31m${text}\x1b[0m`;
12
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Interactive y/N prompt for destructive CLI subcommands. Uses
3
+ * `readline.createInterface` so EOF (Ctrl+D) is reported via the
4
+ * `close` event rather than hanging the process indefinitely.
5
+ *
6
+ * Returns one of:
7
+ * - `"confirmed"` user answered y/yes
8
+ * - `"denied"` user answered anything else (incl. EOF)
9
+ * - `"non-interactive"` stdin is not a TTY and the prompt was
10
+ * refused before any read happened
11
+ *
12
+ * Callers decide what each outcome means for `process.exitCode`. The
13
+ * convention used by `assistant plugins uninstall` is:
14
+ * confirmed → proceed
15
+ * denied → print "cancelled.", exit 0
16
+ * non-interactive → exit 1 (script must pass `--force`)
17
+ */
18
+
19
+ import readline from "node:readline";
20
+
21
+ export type ConfirmResult = "confirmed" | "denied" | "non-interactive";
22
+
23
+ export interface ConfirmPromptOptions {
24
+ /** The line written to stdout before reading. Should end with a space. */
25
+ question: string;
26
+ /** Whether stdin is attached to a TTY. Inject for testability. */
27
+ isTTY: boolean;
28
+ /**
29
+ * Message written to stderr when stdin is not a TTY. The caller chooses
30
+ * the wording so it can name the subject (plugin, file, command, etc.).
31
+ */
32
+ refuseNonInteractiveMessage: string;
33
+ /** Stream to read the answer from. Defaults to `process.stdin`. */
34
+ stdin?: NodeJS.ReadableStream;
35
+ /** Stream to write the prompt to. Defaults to `process.stdout`. */
36
+ stdout?: NodeJS.WritableStream;
37
+ /** Stream for the non-interactive refusal. Defaults to `process.stderr`. */
38
+ stderr?: NodeJS.WritableStream;
39
+ }
40
+
41
+ /**
42
+ * Pattern matched against the trimmed user response to decide
43
+ * confirmation. Case-insensitive. Anything else (including the empty
44
+ * string surfaced on EOF) is treated as denial — never trust silence
45
+ * to mean yes on a destructive prompt.
46
+ */
47
+ const CONFIRM_PATTERN = /^(y|yes)$/i;
48
+
49
+ export async function confirmPrompt(
50
+ opts: ConfirmPromptOptions,
51
+ ): Promise<ConfirmResult> {
52
+ const stderr = opts.stderr ?? process.stderr;
53
+
54
+ if (!opts.isTTY) {
55
+ stderr.write(`${opts.refuseNonInteractiveMessage}\n`);
56
+ return "non-interactive";
57
+ }
58
+
59
+ const rl = readline.createInterface({
60
+ input: opts.stdin ?? process.stdin,
61
+ output: opts.stdout ?? process.stdout,
62
+ terminal: false,
63
+ });
64
+
65
+ const answer = await new Promise<string>((resolve) => {
66
+ let settled = false;
67
+ const settle = (value: string) => {
68
+ if (settled) return;
69
+ settled = true;
70
+ resolve(value);
71
+ };
72
+ rl.question(opts.question, (line) => settle(line));
73
+ rl.on("close", () => settle(""));
74
+ });
75
+
76
+ rl.close();
77
+
78
+ return CONFIRM_PATTERN.test(answer.trim()) ? "confirmed" : "denied";
79
+ }
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Install an external plugin by name from the canonical GitHub source.
3
+ *
4
+ * The plugin source convention is fixed at
5
+ * `vellum-ai/vellum-assistant/experimental/plugins/<name>/` on the configured
6
+ * git ref. The {@link installPlugin} entry point fetches the directory tree
7
+ * via the GitHub Contents API and materializes it into
8
+ * `<workspacePluginsDir>/<name>/` so the daemon discovers it on next start.
9
+ *
10
+ * Designed for direct programmatic use. The CLI command
11
+ * `assistant plugins install <name>` is a thin wrapper that supplies
12
+ * production deps (`globalThis.fetch`, the live workspace directory) and
13
+ * formats the result for the terminal; downstream callers may supply their
14
+ * own `fetch` (e.g. an authenticated client, a retry-decorated client, or
15
+ * a test fixture) and an override workspace directory.
16
+ */
17
+
18
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+
21
+ import { getWorkspacePluginsDir } from "../../util/platform.js";
22
+
23
+ const PLUGIN_SOURCE_OWNER = "vellum-ai";
24
+ const PLUGIN_SOURCE_REPO = "vellum-assistant";
25
+ const PLUGIN_SOURCE_PATH_PREFIX = "experimental/plugins";
26
+ /** Default git ref to fetch from when callers don't override. */
27
+ export const DEFAULT_PLUGIN_REF = "main";
28
+
29
+ /** Entry shape returned by the GitHub Contents API for a directory listing. */
30
+ interface GitHubContentEntry {
31
+ readonly name: string;
32
+ readonly path: string;
33
+ readonly type: "file" | "dir" | "symlink" | "submodule";
34
+ readonly size: number;
35
+ readonly download_url: string | null;
36
+ }
37
+
38
+ /**
39
+ * Minimal `fetch` shape used by this module.
40
+ *
41
+ * Narrower than `typeof fetch` because Bun's `fetch` carries a `preconnect`
42
+ * static that this module does not need — pinning to the wider type would
43
+ * force every caller to construct a fully-featured Bun fetch.
44
+ */
45
+ export type FetchLike = (
46
+ input: RequestInfo | URL,
47
+ init?: RequestInit,
48
+ ) => Promise<Response>;
49
+
50
+ /** Options that control which plugin to install and how. */
51
+ export interface InstallPluginOptions {
52
+ readonly name: string;
53
+ /** Overwrite an existing install in place. The previous content is
54
+ * preserved on disk until the fetch succeeds. */
55
+ readonly force?: boolean;
56
+ /** Git ref (branch, tag, SHA) to fetch from. Defaults to {@link DEFAULT_PLUGIN_REF}. */
57
+ readonly ref?: string;
58
+ }
59
+
60
+ /** Dependencies injected by the caller. */
61
+ export interface InstallPluginDeps {
62
+ /** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
63
+ readonly fetch: FetchLike;
64
+ /** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
65
+ readonly workspacePluginsDir?: string;
66
+ }
67
+
68
+ /** Successful install result. */
69
+ export interface InstallPluginResult {
70
+ readonly name: string;
71
+ /** Absolute path the plugin was materialized into. */
72
+ readonly target: string;
73
+ readonly fileCount: number;
74
+ readonly ref: string;
75
+ }
76
+
77
+ /** Plugin name failed sanitization. */
78
+ export class InvalidPluginNameError extends Error {
79
+ constructor(name: string) {
80
+ super(`Invalid plugin name "${name}". Names must match /^[a-z0-9][a-z0-9_-]*$/.`);
81
+ this.name = "InvalidPluginNameError";
82
+ }
83
+ }
84
+
85
+ /** A plugin with the same name is already installed and `--force` was not passed. */
86
+ export class PluginAlreadyInstalledError extends Error {
87
+ constructor(
88
+ readonly pluginName: string,
89
+ readonly target: string,
90
+ ) {
91
+ super(`Plugin "${pluginName}" is already installed at ${target}.`);
92
+ this.name = "PluginAlreadyInstalledError";
93
+ }
94
+ }
95
+
96
+ /** GitHub responded that the plugin directory does not exist at this ref. */
97
+ export class PluginNotFoundError extends Error {
98
+ constructor(
99
+ readonly pluginName: string,
100
+ readonly ref: string,
101
+ ) {
102
+ const sourcePath = `${PLUGIN_SOURCE_OWNER}/${PLUGIN_SOURCE_REPO}/${PLUGIN_SOURCE_PATH_PREFIX}/${pluginName}`;
103
+ super(`Plugin "${pluginName}" not found at ${sourcePath} (ref ${ref}).`);
104
+ this.name = "PluginNotFoundError";
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Reject plugin names that could escape the canonical source path or the
110
+ * install target. The source convention is a flat namespace under
111
+ * `experimental/plugins/`, so a legitimate name is a single path segment
112
+ * built from kebab-case alphanumerics.
113
+ *
114
+ * Exported so callers (e.g. the CLI input prompt) can validate up front
115
+ * before invoking {@link installPlugin}.
116
+ */
117
+ export function sanitizePluginName(name: string): string {
118
+ const trimmed = name.trim();
119
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(trimmed)) {
120
+ throw new InvalidPluginNameError(name);
121
+ }
122
+ return trimmed;
123
+ }
124
+
125
+ /**
126
+ * Reject path components that could escape the staging or install target via
127
+ * `path.join` resolution of `..`, or that contain platform path separators.
128
+ * Used to filter entries returned by the GitHub Contents API before they
129
+ * become filesystem paths.
130
+ */
131
+ function assertSafeFilename(label: string, candidate: string): void {
132
+ if (
133
+ candidate.length === 0 ||
134
+ candidate === "." ||
135
+ candidate === ".." ||
136
+ candidate.includes("/") ||
137
+ candidate.includes("\\") ||
138
+ // Reject any name containing a null byte (filesystem terminator) or that
139
+ // resolves to a parent-segment when split — paranoid layer in case
140
+ // GitHub ever serves a name like "foo/../bar".
141
+ candidate.includes("\0") ||
142
+ candidate.split(/[/\\]/).some((seg) => seg === "..")
143
+ ) {
144
+ throw new Error(`Unsafe ${label} from GitHub response: ${JSON.stringify(candidate)}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Materialize a plugin tree into the local workspace.
150
+ *
151
+ * Staging: the new tree is written into a sibling staging directory and only
152
+ * swapped into place once the fetch completes. A transient failure (5xx,
153
+ * mid-stream 404, network loss) therefore leaves the previously installed
154
+ * copy untouched even when the caller passed `force: true`.
155
+ */
156
+ export async function installPlugin(
157
+ opts: InstallPluginOptions,
158
+ deps: InstallPluginDeps,
159
+ ): Promise<InstallPluginResult> {
160
+ const name = sanitizePluginName(opts.name);
161
+ const ref = opts.ref ?? DEFAULT_PLUGIN_REF;
162
+ const force = opts.force ?? false;
163
+
164
+ const pluginsDir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
165
+ const target = join(pluginsDir, name);
166
+
167
+ if (existsSync(target) && !force) {
168
+ throw new PluginAlreadyInstalledError(name, target);
169
+ }
170
+
171
+ // Stage into a sibling temp dir so an in-progress install never destroys
172
+ // the currently installed version. `process.pid` keeps concurrent installs
173
+ // of the same plugin from clobbering each other's staging.
174
+ const stagingDir = `${target}.installing.${process.pid}`;
175
+ if (existsSync(stagingDir)) {
176
+ rmSync(stagingDir, { recursive: true, force: true });
177
+ }
178
+ mkdirSync(stagingDir, { recursive: true });
179
+
180
+ let fileCount: number;
181
+ try {
182
+ fileCount = await copyDir(
183
+ `${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
184
+ ref,
185
+ stagingDir,
186
+ deps.fetch,
187
+ );
188
+ } catch (err) {
189
+ rmSync(stagingDir, { recursive: true, force: true });
190
+ throw err;
191
+ }
192
+
193
+ if (fileCount === 0) {
194
+ rmSync(stagingDir, { recursive: true, force: true });
195
+ throw new PluginNotFoundError(name, ref);
196
+ }
197
+
198
+ // Atomic-ish swap: rmSync + renameSync. On POSIX the rename itself is
199
+ // atomic, so the only window where the target is absent is between the
200
+ // rm and the rename — and at that point the staging dir is fully populated.
201
+ if (existsSync(target)) {
202
+ rmSync(target, { recursive: true, force: true });
203
+ }
204
+ renameSync(stagingDir, target);
205
+
206
+ return { name, target, fileCount, ref };
207
+ }
208
+
209
+ async function copyDir(
210
+ apiPath: string,
211
+ ref: string,
212
+ destDir: string,
213
+ fetchFn: FetchLike,
214
+ ): Promise<number> {
215
+ const entries = await listDir(apiPath, ref, fetchFn);
216
+ if (entries === null) return 0;
217
+
218
+ let count = 0;
219
+ for (const entry of entries) {
220
+ assertSafeFilename("entry name", entry.name);
221
+ if (entry.type === "dir") {
222
+ const subDest = join(destDir, entry.name);
223
+ mkdirSync(subDest, { recursive: true });
224
+ count += await copyDir(entry.path, ref, subDest, fetchFn);
225
+ continue;
226
+ }
227
+ if (entry.type === "file") {
228
+ await copyFile(entry, destDir, fetchFn);
229
+ count++;
230
+ continue;
231
+ }
232
+ // Skip symlink + submodule deliberately. The daemon-side loader does not
233
+ // follow either, so reproducing them in the install target adds risk
234
+ // without value.
235
+ }
236
+ return count;
237
+ }
238
+
239
+ async function listDir(
240
+ apiPath: string,
241
+ ref: string,
242
+ fetchFn: FetchLike,
243
+ ): Promise<readonly GitHubContentEntry[] | null> {
244
+ const url =
245
+ `https://api.github.com/repos/${PLUGIN_SOURCE_OWNER}/${PLUGIN_SOURCE_REPO}` +
246
+ `/contents/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}?ref=${encodeURIComponent(ref)}`;
247
+
248
+ const res = await githubFetch(url, "application/vnd.github+json", fetchFn);
249
+ if (res.status === 404) return null;
250
+ if (!res.ok) {
251
+ throw new Error(
252
+ `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`,
253
+ );
254
+ }
255
+
256
+ const body = (await res.json()) as unknown;
257
+ if (!Array.isArray(body)) {
258
+ // A non-array body for a /contents/<dir> path means the path is a
259
+ // file, not a directory — i.e. the plugin name resolved to a single
260
+ // file rather than a plugin directory. Treat as not-a-plugin.
261
+ return null;
262
+ }
263
+ return body as readonly GitHubContentEntry[];
264
+ }
265
+
266
+ async function copyFile(
267
+ entry: GitHubContentEntry,
268
+ destDir: string,
269
+ fetchFn: FetchLike,
270
+ ): Promise<void> {
271
+ if (!entry.download_url) {
272
+ throw new Error(`GitHub contents entry has no download_url: ${entry.path}`);
273
+ }
274
+ const res = await githubFetch(entry.download_url, "application/octet-stream", fetchFn);
275
+ if (!res.ok) {
276
+ throw new Error(`Download failed for ${entry.path}: HTTP ${res.status}`);
277
+ }
278
+ const buf = Buffer.from(await res.arrayBuffer());
279
+ // entry.name was already validated by the caller; assert again as a
280
+ // belt-and-braces guard so copyFile is safe to call from future paths.
281
+ assertSafeFilename("file entry name", entry.name);
282
+ const dest = join(destDir, entry.name);
283
+ mkdirSync(dirname(dest), { recursive: true });
284
+ writeFileSync(dest, buf);
285
+ }
286
+
287
+ /**
288
+ * Wraps `fetchFn` with the headers we want to send to GitHub for every
289
+ * request. Honors `GITHUB_TOKEN` when present so users who hit the
290
+ * unauthenticated rate limit can opt into a higher cap.
291
+ */
292
+ async function githubFetch(
293
+ url: string,
294
+ accept: string,
295
+ fetchFn: FetchLike,
296
+ ): Promise<Response> {
297
+ const headers: Record<string, string> = {
298
+ Accept: accept,
299
+ "User-Agent": "vellum-assistant-cli",
300
+ };
301
+ const token = process.env.GITHUB_TOKEN?.trim();
302
+ if (token) headers.Authorization = `Bearer ${token}`;
303
+ return fetchFn(url, { headers });
304
+ }