@vellumai/assistant 0.8.8 → 0.8.9-staging.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 (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -34,15 +34,26 @@
34
34
 
35
35
  import { beforeEach, describe, expect, mock, test } from "bun:test";
36
36
 
37
- import { InvalidPluginNameError } from "../../../cli/lib/install-from-github.js";
37
+ import {
38
+ type InstallPluginOptions,
39
+ type InstallPluginResult,
40
+ InvalidPluginNameError,
41
+ PluginAlreadyInstalledError,
42
+ PluginNotFoundError,
43
+ PluginSourceUnavailableError,
44
+ } from "../../../cli/lib/install-from-github.js";
38
45
  import type { InstalledPluginInfo } from "../../../cli/lib/list-installed-plugins.js";
46
+ import {
47
+ type PluginDetails,
48
+ PluginDetailsNotFoundError,
49
+ type PluginDetailsOptions,
50
+ } from "../../../cli/lib/plugin-details.js";
39
51
  import type {
52
+ PluginCatalog,
40
53
  PluginSearchMatch,
41
54
  SearchPluginsDeps,
42
- SearchPluginsOptions,
43
- SearchPluginsResult,
44
55
  } from "../../../cli/lib/search-plugins.js";
45
- import { InvalidSearchPatternError } from "../../../cli/lib/search-plugins.js";
56
+ import { PluginCatalogUnavailableError } from "../../../cli/lib/search-plugins.js";
46
57
  import {
47
58
  PluginNotInstalledError,
48
59
  type UninstallPluginOptions,
@@ -57,22 +68,18 @@ mock.module("../../../cli/lib/list-installed-plugins.js", () => ({
57
68
  listInstalledPlugins: () => installedFixture,
58
69
  }));
59
70
 
60
- // Mock searchPlugins: `searchSpy` records every invocation; the implementation
61
- // returns whatever `searchResult` is set to (or throws `searchError` when set).
62
- const searchSpy = mock(
63
- async (
64
- _opts: SearchPluginsOptions,
65
- _deps: SearchPluginsDeps,
66
- ): Promise<SearchPluginsResult> => {
67
- throw new Error("searchSpy default impl not configured");
71
+ // Mock the catalog cache: `getCatalogSpy` records every invocation and
72
+ // returns the (unfiltered) catalog the route then filters in memory via the
73
+ // real `filterPluginCatalog`. The real `search-plugins.js` module is left
74
+ // unmocked so filtering + error classes behave exactly as in production.
75
+ const getCatalogSpy = mock(
76
+ async (_ref: string, _deps: SearchPluginsDeps): Promise<PluginCatalog> => {
77
+ throw new Error("getCatalogSpy default impl not configured");
68
78
  },
69
79
  );
70
80
 
71
- mock.module("../../../cli/lib/search-plugins.js", () => ({
72
- // Pass through the real error classes — the route handler checks
73
- // `instanceof InvalidSearchPatternError`.
74
- InvalidSearchPatternError,
75
- searchPlugins: searchSpy,
81
+ mock.module("../../../cli/lib/plugin-catalog-cache.js", () => ({
82
+ getPluginCatalog: getCatalogSpy,
76
83
  }));
77
84
 
78
85
  // Mock uninstallPlugin. The handler's error mapping is the wiring under
@@ -89,16 +96,50 @@ mock.module("../../../cli/lib/uninstall-plugin.js", () => ({
89
96
  uninstallPlugin: uninstallSpy,
90
97
  }));
91
98
 
99
+ // Mock installPlugin. As with the other libs, the handler's error mapping
100
+ // is the wiring under test; the lib's own behavior is covered separately.
101
+ const installSpy = mock(
102
+ async (_opts: InstallPluginOptions): Promise<InstallPluginResult> => {
103
+ throw new Error("installSpy default impl not configured");
104
+ },
105
+ );
106
+
92
107
  // `InvalidPluginNameError` is re-exported from uninstall-plugin.js but
93
108
  // the canonical definition lives in install-from-github.js. The
94
109
  // handler imports from install-from-github.js, so mock that too so the
95
- // `instanceof` check inside the handler resolves to the same class as
96
- // the one the spy throws.
110
+ // `instanceof` checks inside the handler resolve to the same classes as
111
+ // the ones the spies throw. The error classes are passed through real so
112
+ // `instanceof` aligns.
97
113
  mock.module("../../../cli/lib/install-from-github.js", () => ({
114
+ DEFAULT_PLUGIN_REF: "main",
98
115
  InvalidPluginNameError,
116
+ PluginAlreadyInstalledError,
117
+ PluginNotFoundError,
118
+ PluginSourceUnavailableError,
119
+ installPlugin: installSpy,
120
+ }));
121
+
122
+ // Mock getPluginDetails: the detail handler unions disk + manifest + repo
123
+ // in the lib (covered by plugin-details.test.ts); here we isolate the
124
+ // route's error mapping and pass-through.
125
+ const detailsSpy = mock(
126
+ async (_opts: PluginDetailsOptions): Promise<PluginDetails> => {
127
+ throw new Error("detailsSpy default impl not configured");
128
+ },
129
+ );
130
+
131
+ mock.module("../../../cli/lib/plugin-details.js", () => ({
132
+ PluginDetailsNotFoundError,
133
+ getPluginDetails: detailsSpy,
99
134
  }));
100
135
 
101
- import { BadRequestError, InternalError, NotFoundError } from "../errors.js";
136
+ import {
137
+ BadRequestError,
138
+ ConflictError,
139
+ InternalError,
140
+ NotFoundError,
141
+ ServiceUnavailableError,
142
+ } from "../errors.js";
102
143
  import { ROUTES as PLUGINS_ROUTES } from "../plugins-routes.js";
103
144
  import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
104
145
 
@@ -111,10 +152,12 @@ function findHandler(operationId: string): RouteDefinition["handler"] {
111
152
  const listHandler = findHandler("plugins_list");
112
153
  const searchHandler = findHandler("plugins_search");
113
154
  const uninstallHandler = findHandler("plugins_uninstall");
155
+ const getHandler = findHandler("plugins_get");
156
+ const installHandler = findHandler("plugins_install");
114
157
 
115
- function invoke(
116
- args: RouteHandlerArgs = {},
117
- ): { plugins: Array<Record<string, unknown>> } {
158
+ function invoke(args: RouteHandlerArgs = {}): {
159
+ plugins: Array<Record<string, unknown>>;
160
+ } {
118
161
  return listHandler(args) as { plugins: Array<Record<string, unknown>> };
119
162
  }
120
163
 
@@ -268,7 +311,9 @@ describe("GET /v1/plugins", () => {
268
311
 
269
312
  // matches multiple
270
313
  expect(
271
- invoke({ queryParams: { q: "o" } }).plugins.map((p) => p.id).sort(),
314
+ invoke({ queryParams: { q: "o" } })
315
+ .plugins.map((p) => p.id)
316
+ .sort(),
272
317
  ).toEqual(["calendar-sync", "todo", "weather"].sort());
273
318
 
274
319
  // no match
@@ -305,81 +350,129 @@ describe("GET /v1/plugins", () => {
305
350
  // GET /v1/plugins/search
306
351
  // ---------------------------------------------------------------------------
307
352
 
353
+ function catalog(
354
+ ref: string,
355
+ matches: readonly PluginSearchMatch[],
356
+ ): PluginCatalog {
357
+ return { ref, matches };
358
+ }
359
+
308
360
  describe("GET /v1/plugins/search", () => {
309
361
  beforeEach(() => {
310
- searchSpy.mockClear();
311
- // Default to a happy-path resolution; individual tests override as needed.
312
- searchSpy.mockImplementation(async (opts) => ({
313
- query: opts.query,
314
- ref: opts.ref ?? "main",
315
- matches: [],
316
- }));
362
+ getCatalogSpy.mockClear();
363
+ // Default to a happy-path empty catalog; individual tests override.
364
+ getCatalogSpy.mockImplementation(async (ref) => catalog(ref, []));
317
365
  });
318
366
 
319
- test("forwards ?q= and ?ref= to searchPlugins; returns its result", async () => {
320
- searchSpy.mockImplementation(async (opts) => ({
321
- query: opts.query,
322
- ref: opts.ref ?? "main",
323
- matches: [
324
- { name: "simple-memory", path: "experimental/plugins/simple-memory" },
325
- { name: "simple-router", path: "experimental/plugins/simple-router" },
326
- ],
327
- }));
367
+ test("resolves the catalog at the requested ref and filters by ?q=", async () => {
368
+ getCatalogSpy.mockImplementation(async (ref) =>
369
+ catalog(ref, [
370
+ {
371
+ name: "simple-memory",
372
+ path: "experimental/plugins/simple-memory",
373
+ source: { kind: "first-party" },
374
+ },
375
+ {
376
+ name: "caveman",
377
+ path: "github:JuliusBrussee/caveman@v1.8.2",
378
+ description: "Ultra-compressed communication mode.",
379
+ source: {
380
+ kind: "github",
381
+ repo: "JuliusBrussee/caveman",
382
+ ref: "v1.8.2",
383
+ },
384
+ },
385
+ ]),
386
+ );
328
387
 
329
388
  const result = await invokeSearch({
330
389
  queryParams: { q: "^simple", ref: "my-feature-branch" },
331
390
  });
332
391
 
333
- // The lib received the exact query + ref we surfaced from the route.
334
- expect(searchSpy).toHaveBeenCalledTimes(1);
335
- const [opts] = searchSpy.mock.calls[0]!;
336
- expect(opts.query).toBe("^simple");
337
- expect(opts.ref).toBe("my-feature-branch");
392
+ // The cache was consulted once at the requested ref.
393
+ expect(getCatalogSpy).toHaveBeenCalledTimes(1);
394
+ const [ref] = getCatalogSpy.mock.calls[0]!;
395
+ expect(ref).toBe("my-feature-branch");
338
396
 
397
+ // The query is applied in-memory by the real filter: `^simple` matches
398
+ // only `simple-memory`, and the source discriminator is preserved.
339
399
  expect(result).toEqual({
340
400
  query: "^simple",
341
401
  ref: "my-feature-branch",
342
402
  matches: [
343
- { name: "simple-memory", path: "experimental/plugins/simple-memory" },
344
- { name: "simple-router", path: "experimental/plugins/simple-router" },
403
+ {
404
+ name: "simple-memory",
405
+ path: "experimental/plugins/simple-memory",
406
+ source: { kind: "first-party" },
407
+ },
345
408
  ],
346
409
  });
347
410
  });
348
411
 
349
- test("missing ?q= passes empty string through (match-all per lib contract)", async () => {
350
- await invokeSearch();
351
- const [opts] = searchSpy.mock.calls[0]!;
352
- expect(opts.query).toBe("");
353
- // ref is omitted (undefined) when caller doesn't supply one — lib
354
- // applies its DEFAULT_PLUGIN_REF fallback.
355
- expect(opts.ref).toBeUndefined();
412
+ test("missing ?q= matches all (empty-string query) at the default ref", async () => {
413
+ getCatalogSpy.mockImplementation(async (ref) =>
414
+ catalog(ref, [
415
+ {
416
+ name: "caveman",
417
+ path: "github:JuliusBrussee/caveman@v1.8.2",
418
+ source: {
419
+ kind: "github",
420
+ repo: "JuliusBrussee/caveman",
421
+ ref: "v1.8.2",
422
+ },
423
+ },
424
+ ]),
425
+ );
426
+ const result = await invokeSearch();
427
+ const [ref] = getCatalogSpy.mock.calls[0]!;
428
+ // No ref supplied → route resolves the default ref before caching.
429
+ expect(ref).toBe("main");
430
+ expect(result.query).toBe("");
431
+ expect(result.matches.map((m) => m.name)).toEqual(["caveman"]);
356
432
  });
357
433
 
358
- test("whitespace-only ?ref= is treated as missing", async () => {
434
+ test("whitespace-only ?ref= falls back to the default ref", async () => {
359
435
  await invokeSearch({ queryParams: { q: "x", ref: " " } });
360
- const [opts] = searchSpy.mock.calls[0]!;
361
- expect(opts.ref).toBeUndefined();
436
+ const [ref] = getCatalogSpy.mock.calls[0]!;
437
+ expect(ref).toBe("main");
362
438
  });
363
439
 
364
- test("supplies a bound globalThis.fetch as the lib's fetch dep", async () => {
440
+ test("supplies a bound globalThis.fetch as the catalog loader's fetch dep", async () => {
365
441
  await invokeSearch({ queryParams: { q: "memory" } });
366
- const [, deps] = searchSpy.mock.calls[0]!;
442
+ const [, deps] = getCatalogSpy.mock.calls[0]!;
367
443
  expect(typeof deps.fetch).toBe("function");
368
444
  });
369
445
 
370
- test("InvalidSearchPatternError → BadRequestError (400)", async () => {
371
- searchSpy.mockImplementation(async () => {
372
- throw new InvalidSearchPatternError("(", new SyntaxError("unbalanced"));
373
- });
374
-
446
+ test("InvalidSearchPatternError → BadRequestError (400), before any catalog load", async () => {
447
+ // GIVEN a malformed regex query and a cold cache
448
+ // WHEN the search runs
449
+ // THEN it's a deterministic 400 AND the catalog is never loaded, so a
450
+ // user typo can't waste a GitHub request (which would otherwise surface
451
+ // as 503 on a rate-limited cold cache).
375
452
  await expect(
376
453
  invokeSearch({ queryParams: { q: "(" } }),
377
454
  ).rejects.toBeInstanceOf(BadRequestError);
455
+ expect(getCatalogSpy).not.toHaveBeenCalled();
456
+ });
457
+
458
+ test("PluginCatalogUnavailableError → ServiceUnavailableError (503)", async () => {
459
+ getCatalogSpy.mockImplementation(async () => {
460
+ throw new PluginCatalogUnavailableError(
461
+ "GitHub contents listing failed for experimental/plugins @ main: HTTP 403",
462
+ 403,
463
+ );
464
+ });
465
+
466
+ // A rate-limited upstream (with no cache to fall back on) is transient
467
+ // and retryable — the route surfaces it as 503, not a misleading 500.
468
+ await expect(
469
+ invokeSearch({ queryParams: { q: "memory" } }),
470
+ ).rejects.toBeInstanceOf(ServiceUnavailableError);
378
471
  });
379
472
 
380
473
  test("unknown errors → InternalError with original message preserved", async () => {
381
- searchSpy.mockImplementation(async () => {
382
- throw new Error("GitHub contents listing failed: HTTP 502");
474
+ getCatalogSpy.mockImplementation(async () => {
475
+ throw new Error("GitHub contents listing failed: HTTP 404");
383
476
  });
384
477
 
385
478
  await expect(
@@ -394,20 +487,28 @@ describe("GET /v1/plugins/search", () => {
394
487
 
395
488
  test("re-packs readonly match array into a mutable copy", async () => {
396
489
  const frozenMatches = Object.freeze([
397
- Object.freeze({ name: "a", path: "experimental/plugins/a" }),
490
+ Object.freeze({
491
+ name: "a",
492
+ path: "experimental/plugins/a",
493
+ source: { kind: "first-party" } as const,
494
+ }),
398
495
  ]) as readonly PluginSearchMatch[];
399
- searchSpy.mockImplementation(async (opts) => ({
400
- query: opts.query,
401
- ref: opts.ref ?? "main",
402
- matches: frozenMatches,
403
- }));
496
+ getCatalogSpy.mockImplementation(async (ref) =>
497
+ catalog(ref, frozenMatches),
498
+ );
404
499
 
405
500
  const result = await invokeSearch({ queryParams: { q: "a" } });
406
501
  // The route returns a non-frozen array we can mutate without
407
502
  // touching the lib's internal cache. This matters when serializers
408
503
  // (or downstream test fixtures) reach in.
409
504
  expect(Object.isFrozen(result.matches)).toBe(false);
410
- expect(() => result.matches.push({ name: "b", path: "x" })).not.toThrow();
505
+ expect(() =>
506
+ result.matches.push({
507
+ name: "b",
508
+ path: "x",
509
+ source: { kind: "first-party" },
510
+ }),
511
+ ).not.toThrow();
411
512
  });
412
513
  });
413
514
 
@@ -510,3 +611,252 @@ describe("DELETE /v1/plugins/:name", () => {
510
611
  expect((caught as Error).message).toBe("plugin uninstall failed");
511
612
  });
512
613
  });
614
+
615
+ function pluginDetails(overrides: Partial<PluginDetails> = {}): PluginDetails {
616
+ return {
617
+ name: overrides.name ?? "caveman",
618
+ installed: overrides.installed ?? false,
619
+ description: overrides.description ?? null,
620
+ homepage: overrides.homepage ?? null,
621
+ license: overrides.license ?? null,
622
+ version: overrides.version ?? null,
623
+ source: overrides.source ?? { kind: "first-party" },
624
+ readme: overrides.readme ?? null,
625
+ ref: overrides.ref ?? "main",
626
+ };
627
+ }
628
+
629
+ async function invokeGet(args: RouteHandlerArgs = {}): Promise<PluginDetails> {
630
+ return (await getHandler(args)) as PluginDetails;
631
+ }
632
+
633
+ describe("GET /v1/plugins/:name", () => {
634
+ beforeEach(() => {
635
+ detailsSpy.mockReset();
636
+ });
637
+
638
+ test("forwards name + ref to getPluginDetails and returns the detail view", async () => {
639
+ const view = pluginDetails({
640
+ name: "caveman",
641
+ installed: true,
642
+ description: "A loud agent plugin",
643
+ homepage: "https://example.com/caveman",
644
+ license: "MIT",
645
+ version: "1.8.2",
646
+ source: { kind: "github", repo: "example-org/caveman", ref: "v1.8.2" },
647
+ readme: "# Caveman\n\nHello.",
648
+ ref: "v1.8.2",
649
+ });
650
+ detailsSpy.mockImplementation(async () => view);
651
+
652
+ const result = await invokeGet({
653
+ pathParams: { name: "caveman" },
654
+ queryParams: { ref: "v1.8.2" },
655
+ });
656
+
657
+ expect(result).toEqual(view);
658
+ expect(detailsSpy.mock.calls[0]?.[0]).toEqual({
659
+ name: "caveman",
660
+ ref: "v1.8.2",
661
+ });
662
+ });
663
+
664
+ test("omits ref (passes undefined) when the query param is absent or blank", async () => {
665
+ detailsSpy.mockImplementation(async () => pluginDetails());
666
+
667
+ await invokeGet({ pathParams: { name: "caveman" }, queryParams: {} });
668
+ expect(detailsSpy.mock.calls[0]?.[0]).toEqual({
669
+ name: "caveman",
670
+ ref: undefined,
671
+ });
672
+
673
+ await invokeGet({
674
+ pathParams: { name: "caveman" },
675
+ queryParams: { ref: " " },
676
+ });
677
+ expect(detailsSpy.mock.calls[1]?.[0]).toEqual({
678
+ name: "caveman",
679
+ ref: undefined,
680
+ });
681
+ });
682
+
683
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
684
+ detailsSpy.mockImplementation(async () => {
685
+ throw new InvalidPluginNameError("../escape");
686
+ });
687
+
688
+ await expect(
689
+ invokeGet({ pathParams: { name: "../escape" } }),
690
+ ).rejects.toBeInstanceOf(BadRequestError);
691
+ });
692
+
693
+ test("PluginDetailsNotFoundError → NotFoundError (404)", async () => {
694
+ detailsSpy.mockImplementation(async () => {
695
+ throw new PluginDetailsNotFoundError("ghost", "main");
696
+ });
697
+
698
+ await expect(
699
+ invokeGet({ pathParams: { name: "ghost" } }),
700
+ ).rejects.toBeInstanceOf(NotFoundError);
701
+ });
702
+
703
+ test("unknown errors → InternalError with original message preserved", async () => {
704
+ detailsSpy.mockImplementation(async () => {
705
+ throw new Error("ENOTFOUND api.github.com");
706
+ });
707
+
708
+ let caught: unknown;
709
+ try {
710
+ await invokeGet({ pathParams: { name: "caveman" } });
711
+ } catch (err) {
712
+ caught = err;
713
+ }
714
+ expect(caught).toBeInstanceOf(InternalError);
715
+ expect((caught as Error).message).toContain("ENOTFOUND");
716
+ });
717
+ });
718
+
719
+ async function invokeInstall(args: RouteHandlerArgs = {}): Promise<{
720
+ ok: true;
721
+ name: string;
722
+ target: string;
723
+ fileCount: number;
724
+ ref: string;
725
+ }> {
726
+ return (await installHandler(args)) as {
727
+ ok: true;
728
+ name: string;
729
+ target: string;
730
+ fileCount: number;
731
+ ref: string;
732
+ };
733
+ }
734
+
735
+ describe("POST /v1/plugins/install", () => {
736
+ beforeEach(() => {
737
+ installSpy.mockReset();
738
+ });
739
+
740
+ test("forwards name/force and shapes the result, pinning ref to the default", async () => {
741
+ installSpy.mockImplementation(async (opts) => ({
742
+ name: opts.name,
743
+ target: `/workspace/.vellum/plugins/${opts.name}`,
744
+ fileCount: 7,
745
+ ref: opts.ref ?? "main",
746
+ commit: null,
747
+ }));
748
+
749
+ const result = await invokeInstall({
750
+ body: { name: "caveman", force: true },
751
+ });
752
+
753
+ expect(result).toEqual({
754
+ ok: true,
755
+ name: "caveman",
756
+ target: "/workspace/.vellum/plugins/caveman",
757
+ fileCount: 7,
758
+ ref: "main",
759
+ });
760
+ expect(installSpy.mock.calls[0]?.[0]).toEqual({
761
+ name: "caveman",
762
+ ref: "main",
763
+ force: true,
764
+ });
765
+ });
766
+
767
+ test("ignores a caller-supplied ref and pins to the curated default", async () => {
768
+ // Security boundary: installing from an unreviewed ref (a PR branch,
769
+ // fork ref, ...) could load attacker-controlled marketplace/first-party
770
+ // code, so the HTTP route never honors a body `ref` — it always resolves
771
+ // against the curated default ref.
772
+ installSpy.mockImplementation(async (opts) => ({
773
+ name: opts.name,
774
+ target: `/workspace/.vellum/plugins/${opts.name}`,
775
+ fileCount: 7,
776
+ ref: opts.ref ?? "main",
777
+ commit: null,
778
+ }));
779
+
780
+ const result = await invokeInstall({
781
+ body: { name: "caveman", ref: "attacker-pr-branch" },
782
+ });
783
+
784
+ expect(result.ref).toBe("main");
785
+ expect(installSpy.mock.calls[0]?.[0]).toEqual({
786
+ name: "caveman",
787
+ ref: "main",
788
+ force: undefined,
789
+ });
790
+ });
791
+
792
+ test("a missing name short-circuits to BadRequestError without calling the lib", async () => {
793
+ await expect(invokeInstall({ body: {} })).rejects.toBeInstanceOf(
794
+ BadRequestError,
795
+ );
796
+ expect(installSpy).not.toHaveBeenCalled();
797
+ });
798
+
799
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
800
+ installSpy.mockImplementation(async () => {
801
+ throw new InvalidPluginNameError("../escape");
802
+ });
803
+
804
+ await expect(
805
+ invokeInstall({ body: { name: "../escape" } }),
806
+ ).rejects.toBeInstanceOf(BadRequestError);
807
+ });
808
+
809
+ test("PluginAlreadyInstalledError → ConflictError (409)", async () => {
810
+ installSpy.mockImplementation(async (opts) => {
811
+ throw new PluginAlreadyInstalledError(
812
+ opts.name,
813
+ `/workspace/.vellum/plugins/${opts.name}`,
814
+ );
815
+ });
816
+
817
+ await expect(
818
+ invokeInstall({ body: { name: "caveman" } }),
819
+ ).rejects.toBeInstanceOf(ConflictError);
820
+ });
821
+
822
+ test("PluginNotFoundError → NotFoundError (404)", async () => {
823
+ installSpy.mockImplementation(async (opts) => {
824
+ throw new PluginNotFoundError(opts.name, "main", "example-org/ghost");
825
+ });
826
+
827
+ await expect(
828
+ invokeInstall({ body: { name: "ghost" } }),
829
+ ).rejects.toBeInstanceOf(NotFoundError);
830
+ });
831
+
832
+ test("PluginSourceUnavailableError → ServiceUnavailableError (503)", async () => {
833
+ // A rate-limited or temporarily-down GitHub source is retryable: the
834
+ // route surfaces 503 so the client can back off and try again, rather
835
+ // than a misleading 500 that reads as a permanent failure.
836
+ installSpy.mockImplementation(async () => {
837
+ throw new PluginSourceUnavailableError(
838
+ "GitHub tree listing for JuliusBrussee/caveman @ v1.8.2: HTTP 403",
839
+ 403,
840
+ );
841
+ });
842
+
843
+ await expect(
844
+ invokeInstall({ body: { name: "caveman" } }),
845
+ ).rejects.toBeInstanceOf(ServiceUnavailableError);
846
+ });
847
+
848
+ test("unknown errors → InternalError with original message preserved", async () => {
849
+ installSpy.mockImplementation(async () => {
850
+ throw new Error("ECONNRESET");
851
+ });
852
+
853
+ let caught: unknown;
854
+ try {
855
+ await invokeInstall({ body: { name: "caveman" } });
856
+ } catch (err) {
857
+ caught = err;
858
+ }
859
+ expect(caught).toBeInstanceOf(InternalError);
860
+ expect((caught as Error).message).toContain("ECONNRESET");
861
+ });
862
+ });
@@ -42,7 +42,7 @@ const findBySurfaceCalls: string[] = [];
42
42
  const getOrCreateCalls: string[] = [];
43
43
  const rawGetCalls: Array<{ sql: string; params: unknown[] }> = [];
44
44
 
45
- mock.module("../../../daemon/conversation-store.js", () => ({
45
+ mock.module("../../../daemon/conversation-registry.js", () => ({
46
46
  findConversation: (id: string) => {
47
47
  findConvCalls.push(id);
48
48
  return memoryById ?? undefined;
@@ -51,6 +51,9 @@ mock.module("../../../daemon/conversation-store.js", () => ({
51
51
  findBySurfaceCalls.push(surfaceId);
52
52
  return memoryBySurface ?? undefined;
53
53
  },
54
+ }));
55
+
56
+ mock.module("../../../daemon/conversation-store.js", () => ({
54
57
  getOrCreateConversation: async (id: string) => {
55
58
  getOrCreateCalls.push(id);
56
59
  if (!rehydrated) {
@@ -183,9 +186,7 @@ describe("triggerSurfaceAction handler", () => {
183
186
  // SQL must filter the messages table by ui_surface payload pattern.
184
187
  expect(rawGetCalls[0]!.sql).toContain("FROM messages");
185
188
  expect(rawGetCalls[0]!.sql).toContain("LIKE");
186
- expect(rawGetCalls[0]!.params).toEqual([
187
- `%"surfaceId":"surf-evicted"%`,
188
- ]);
189
+ expect(rawGetCalls[0]!.params).toEqual([`%"surfaceId":"surf-evicted"%`]);
189
190
  expect(getOrCreateCalls).toEqual(["conv-from-db"]);
190
191
  expect(rehydrated.surfaceActionCalls).toEqual([
191
192
  { surfaceId: "surf-evicted", actionId: "act-3", data: undefined },
@@ -51,7 +51,7 @@ const findBySurfaceCalls: string[] = [];
51
51
  const getOrCreateCalls: string[] = [];
52
52
  const rawGetCalls: Array<{ sql: string; params: unknown[] }> = [];
53
53
 
54
- mock.module("../../../daemon/conversation-store.js", () => ({
54
+ mock.module("../../../daemon/conversation-registry.js", () => ({
55
55
  findConversation: (id: string) => {
56
56
  findConvCalls.push(id);
57
57
  return memoryById ?? undefined;
@@ -60,6 +60,9 @@ mock.module("../../../daemon/conversation-store.js", () => ({
60
60
  findBySurfaceCalls.push(surfaceId);
61
61
  return undefined;
62
62
  },
63
+ }));
64
+
65
+ mock.module("../../../daemon/conversation-store.js", () => ({
63
66
  getOrCreateConversation: async (id: string) => {
64
67
  getOrCreateCalls.push(id);
65
68
  if (!rehydrated) {