@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -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 +642 -5
  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-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -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,303 @@
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. Unauthenticated — the canonical source is a public repo, so
290
+ * there is nothing for an `Authorization` header to do.
291
+ */
292
+ async function githubFetch(
293
+ url: string,
294
+ accept: string,
295
+ fetchFn: FetchLike,
296
+ ): Promise<Response> {
297
+ return fetchFn(url, {
298
+ headers: {
299
+ Accept: accept,
300
+ "User-Agent": "vellum-assistant-cli",
301
+ },
302
+ });
303
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Enumerate plugins materialized under `<workspaceDir>/plugins/`.
3
+ *
4
+ * The CLI command `assistant plugins list` is a thin wrapper. Downstream
5
+ * callers (the daemon's diagnostics surface, a future TUI, scripted
6
+ * audits) can call {@link listInstalledPlugins} directly without going
7
+ * through commander.
8
+ *
9
+ * Designed to be lenient: a malformed `package.json` is reported as an
10
+ * error on that one entry rather than failing the whole listing. The
11
+ * daemon makes the same call on boot via `external-plugin-loader.ts`
12
+ * and we want both surfaces to agree on what's present.
13
+ */
14
+
15
+ import {
16
+ existsSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ statSync,
20
+ } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ import { getWorkspacePluginsDir } from "../../util/platform.js";
24
+
25
+ /** Minimal manifest fields surfaced to the CLI. */
26
+ export interface PluginPackageMetadata {
27
+ readonly name?: string;
28
+ readonly version?: string;
29
+ readonly description?: string;
30
+ readonly peerDependencies?: Record<string, string>;
31
+ }
32
+
33
+ /** One installed plugin entry. */
34
+ export interface InstalledPluginInfo {
35
+ /** Directory name under `<workspaceDir>/plugins/`. */
36
+ readonly name: string;
37
+ /** Absolute path to the plugin directory. */
38
+ readonly target: string;
39
+ /** Parsed `package.json` content, when present and parseable. */
40
+ readonly packageJson: PluginPackageMetadata | null;
41
+ /**
42
+ * Non-fatal issues with this entry (missing `package.json`, malformed
43
+ * JSON, unexpected type, etc.). Empty when the entry parses cleanly.
44
+ */
45
+ readonly issues: readonly string[];
46
+ }
47
+
48
+ /** Options accepted by {@link listInstalledPlugins}. */
49
+ export interface ListInstalledPluginsOptions {
50
+ /** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
51
+ readonly workspacePluginsDir?: string;
52
+ }
53
+
54
+ /**
55
+ * Return one entry per directory under the workspace plugins directory,
56
+ * sorted alphabetically by name. Hidden entries (`.`-prefixed) and
57
+ * non-directory entries are skipped silently — the daemon's loader does
58
+ * the same. Returns `[]` if the plugins directory does not exist.
59
+ */
60
+ export function listInstalledPlugins(
61
+ opts: ListInstalledPluginsOptions = {},
62
+ ): InstalledPluginInfo[] {
63
+ const pluginsDir = opts.workspacePluginsDir ?? getWorkspacePluginsDir();
64
+ if (!existsSync(pluginsDir)) return [];
65
+
66
+ const entries = readdirSync(pluginsDir, { withFileTypes: true })
67
+ .filter((e) => !e.name.startsWith("."))
68
+ .filter((e) => {
69
+ if (e.isDirectory()) return true;
70
+ if (!e.isSymbolicLink()) return false;
71
+ // Resolve the symlink and only keep it if it points to a directory.
72
+ try {
73
+ return statSync(join(pluginsDir, e.name)).isDirectory();
74
+ } catch {
75
+ return false;
76
+ }
77
+ })
78
+ .map((e) => e.name)
79
+ .sort();
80
+
81
+ return entries.map((name) => readPluginEntry(pluginsDir, name));
82
+ }
83
+
84
+ function readPluginEntry(
85
+ pluginsDir: string,
86
+ name: string,
87
+ ): InstalledPluginInfo {
88
+ const target = join(pluginsDir, name);
89
+ const pkgJsonPath = join(target, "package.json");
90
+ const issues: string[] = [];
91
+
92
+ if (!existsSync(pkgJsonPath)) {
93
+ issues.push("missing package.json");
94
+ return { name, target, packageJson: null, issues };
95
+ }
96
+
97
+ let raw: string;
98
+ try {
99
+ raw = readFileSync(pkgJsonPath, "utf8");
100
+ } catch (err) {
101
+ issues.push(
102
+ `package.json unreadable: ${err instanceof Error ? err.message : String(err)}`,
103
+ );
104
+ return { name, target, packageJson: null, issues };
105
+ }
106
+
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(raw);
110
+ } catch (err) {
111
+ issues.push(
112
+ `package.json invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
113
+ );
114
+ return { name, target, packageJson: null, issues };
115
+ }
116
+
117
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
118
+ issues.push("package.json is not an object");
119
+ return { name, target, packageJson: null, issues };
120
+ }
121
+
122
+ const meta = parsed as Record<string, unknown>;
123
+ const packageJson: PluginPackageMetadata = {
124
+ name: typeof meta.name === "string" ? meta.name : undefined,
125
+ version: typeof meta.version === "string" ? meta.version : undefined,
126
+ description:
127
+ typeof meta.description === "string" ? meta.description : undefined,
128
+ peerDependencies:
129
+ typeof meta.peerDependencies === "object" &&
130
+ meta.peerDependencies !== null &&
131
+ !Array.isArray(meta.peerDependencies)
132
+ ? (meta.peerDependencies as Record<string, string>)
133
+ : undefined,
134
+ };
135
+
136
+ return { name, target, packageJson, issues };
137
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Search for plugin directories in the canonical GitHub source.
3
+ *
4
+ * Lists `vellum-ai/vellum-assistant/experimental/plugins/` at the configured
5
+ * git ref and filters the directory entries by case-insensitive ECMAScript
6
+ * regex. A plain query like `"memory"` matches anywhere in the name; anchors
7
+ * like `"^simple"` work without escaping.
8
+ *
9
+ * Designed for direct programmatic use. The CLI command
10
+ * `assistant plugins search <query>` is a thin wrapper that supplies
11
+ * production deps (`globalThis.fetch`) and formats the result for the
12
+ * terminal; downstream callers may supply their own `fetch` (e.g. a
13
+ * retry-decorated client, or a test fixture).
14
+ */
15
+
16
+ import type { FetchLike } from "./install-from-github.js";
17
+ import { DEFAULT_PLUGIN_REF } from "./install-from-github.js";
18
+
19
+ // Re-export the dep-injection type so callers can grab everything they need
20
+ // from one module rather than reaching into `install-from-github.js`.
21
+ export type { FetchLike } from "./install-from-github.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
+
27
+ /** Entry shape returned by the GitHub Contents API for a directory listing. */
28
+ interface GitHubContentEntry {
29
+ readonly name: string;
30
+ readonly path: string;
31
+ readonly type: "file" | "dir" | "symlink" | "submodule";
32
+ readonly size: number;
33
+ readonly download_url: string | null;
34
+ }
35
+
36
+ /** Options that control the search. */
37
+ export interface SearchPluginsOptions {
38
+ /**
39
+ * ECMAScript regex pattern. Matched case-insensitively against directory
40
+ * names. Empty string matches everything.
41
+ */
42
+ readonly query: string;
43
+ /** Git ref to list from. Defaults to {@link DEFAULT_PLUGIN_REF}. */
44
+ readonly ref?: string;
45
+ }
46
+
47
+ /** Dependencies injected by the caller. */
48
+ export interface SearchPluginsDeps {
49
+ /** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
50
+ readonly fetch: FetchLike;
51
+ }
52
+
53
+ /** One matching plugin directory. */
54
+ export interface PluginSearchMatch {
55
+ /** Directory name under `experimental/plugins/`. */
56
+ readonly name: string;
57
+ /** Path within the repo (e.g. `experimental/plugins/<name>`). */
58
+ readonly path: string;
59
+ }
60
+
61
+ /** Search result envelope. */
62
+ export interface SearchPluginsResult {
63
+ readonly query: string;
64
+ readonly ref: string;
65
+ readonly matches: readonly PluginSearchMatch[];
66
+ }
67
+
68
+ /** Caller passed a query that doesn't compile as an ECMAScript regex. */
69
+ export class InvalidSearchPatternError extends Error {
70
+ constructor(pattern: string, cause: unknown) {
71
+ const detail = cause instanceof Error ? cause.message : String(cause);
72
+ super(`Invalid regex pattern ${JSON.stringify(pattern)}: ${detail}`);
73
+ this.name = "InvalidSearchPatternError";
74
+ }
75
+ }
76
+
77
+ /**
78
+ * List directories under `experimental/plugins/` at {@link opts.ref} and
79
+ * filter by {@link opts.query}.
80
+ *
81
+ * Only `type === "dir"` entries are returned — `experimental/plugins/`
82
+ * follows a convention where each plugin lives in its own directory, so
83
+ * loose files at the prefix are not plugins.
84
+ */
85
+ export async function searchPlugins(
86
+ opts: SearchPluginsOptions,
87
+ deps: SearchPluginsDeps,
88
+ ): Promise<SearchPluginsResult> {
89
+ const ref = opts.ref ?? DEFAULT_PLUGIN_REF;
90
+
91
+ // Compile the matcher up front so an invalid regex fails before we hit
92
+ // the network — keeps "user typo" cheap to recover from.
93
+ const matcher = buildMatcher(opts.query);
94
+
95
+ const entries = await listDir(PLUGIN_SOURCE_PATH_PREFIX, ref, deps.fetch);
96
+
97
+ const matches: PluginSearchMatch[] = [];
98
+ for (const entry of entries) {
99
+ if (entry.type !== "dir") continue;
100
+ if (!matcher(entry.name)) continue;
101
+ matches.push({ name: entry.name, path: entry.path });
102
+ }
103
+ matches.sort((a, b) => a.name.localeCompare(b.name));
104
+
105
+ return { query: opts.query, ref, matches };
106
+ }
107
+
108
+ function buildMatcher(query: string): (name: string) => boolean {
109
+ let re: RegExp;
110
+ try {
111
+ re = new RegExp(query, "i");
112
+ } catch (err) {
113
+ throw new InvalidSearchPatternError(query, err);
114
+ }
115
+ return (name) => re.test(name);
116
+ }
117
+
118
+ async function listDir(
119
+ apiPath: string,
120
+ ref: string,
121
+ fetchFn: FetchLike,
122
+ ): Promise<readonly GitHubContentEntry[]> {
123
+ const url =
124
+ `https://api.github.com/repos/${PLUGIN_SOURCE_OWNER}/${PLUGIN_SOURCE_REPO}` +
125
+ `/contents/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}` +
126
+ `?ref=${encodeURIComponent(ref)}`;
127
+
128
+ const res = await githubFetch(url, fetchFn);
129
+ if (!res.ok) {
130
+ // Unlike `installPlugin`, where 404 on a specific plugin name is a
131
+ // legitimate "not found" outcome, 404 on the plugins prefix itself
132
+ // means the canonical source path is gone — surface it as an error
133
+ // rather than silently returning empty results.
134
+ throw new Error(
135
+ `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`,
136
+ );
137
+ }
138
+
139
+ const body = (await res.json()) as unknown;
140
+ if (!Array.isArray(body)) {
141
+ // A non-array body for a /contents/<dir> path means the path is a
142
+ // file, not a directory — treat the prefix as empty rather than crash.
143
+ return [];
144
+ }
145
+ return body as readonly GitHubContentEntry[];
146
+ }
147
+
148
+ /**
149
+ * Wraps `fetchFn` with the headers we want to send to GitHub for every
150
+ * request. Unauthenticated — the canonical source is a public repo, mirroring
151
+ * `installPlugin` which uses the same envelope.
152
+ */
153
+ async function githubFetch(
154
+ url: string,
155
+ fetchFn: FetchLike,
156
+ ): Promise<Response> {
157
+ return fetchFn(url, {
158
+ headers: {
159
+ Accept: "application/vnd.github+json",
160
+ "User-Agent": "vellum-assistant-cli",
161
+ },
162
+ });
163
+ }