@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
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for {@link getPluginCatalog}.
3
+ *
4
+ * The underlying `loadPluginCatalog` is mocked so we exercise the cache's
5
+ * TTL window, refresh-on-expiry, and stale-on-error fallback in isolation —
6
+ * no network and no dependence on the real GitHub-backed loader.
7
+ */
8
+
9
+ import {
10
+ afterEach,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ mock,
15
+ setSystemTime,
16
+ test,
17
+ } from "bun:test";
18
+
19
+ import type { PluginCatalog, SearchPluginsDeps } from "../search-plugins.js";
20
+ import { PluginCatalogUnavailableError } from "../search-plugins.js";
21
+
22
+ // Mock the loader the cache wraps. `loadSpy` records every call and returns
23
+ // (or throws) whatever the test configures. The real error class is passed
24
+ // through so propagation preserves its identity.
25
+ const loadSpy = mock(
26
+ async (
27
+ _opts: { ref: string },
28
+ _deps: SearchPluginsDeps,
29
+ ): Promise<PluginCatalog> => {
30
+ throw new Error("loadSpy default impl not configured");
31
+ },
32
+ );
33
+
34
+ mock.module("../search-plugins.js", () => ({
35
+ PluginCatalogUnavailableError,
36
+ loadPluginCatalog: loadSpy,
37
+ }));
38
+
39
+ const {
40
+ PLUGIN_CATALOG_CACHE_TTL_MS,
41
+ getPluginCatalog,
42
+ invalidatePluginCatalogCache,
43
+ } = await import("../plugin-catalog-cache.js");
44
+
45
+ const deps: SearchPluginsDeps = {
46
+ fetch: (async () => new Response("", { status: 200 })) as never,
47
+ };
48
+
49
+ // A non-zero base time. Bun treats `setSystemTime(new Date(0))` as "reset to
50
+ // the real clock", so the fake clock must start from a positive epoch.
51
+ const BASE_TIME_MS = 1_700_000_000_000;
52
+
53
+ function catalog(ref: string, names: string[]): PluginCatalog {
54
+ return {
55
+ ref,
56
+ matches: names.map((name) => ({
57
+ name,
58
+ path: `experimental/plugins/${name}`,
59
+ source: { kind: "first-party" as const },
60
+ })),
61
+ };
62
+ }
63
+
64
+ describe("getPluginCatalog", () => {
65
+ beforeEach(() => {
66
+ loadSpy.mockClear();
67
+ invalidatePluginCatalogCache();
68
+ });
69
+
70
+ afterEach(() => {
71
+ // Restore real time in case a test installed a fake clock.
72
+ setSystemTime();
73
+ });
74
+
75
+ test("loads once and serves the cached catalog within the TTL window", async () => {
76
+ // GIVEN a loader that returns a catalog
77
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["a"]));
78
+
79
+ // WHEN we request the same ref twice in quick succession
80
+ const first = await getPluginCatalog("main", deps);
81
+ const second = await getPluginCatalog("main", deps);
82
+
83
+ // THEN the loader is consulted exactly once — the second call is cached
84
+ expect(loadSpy).toHaveBeenCalledTimes(1);
85
+ expect(first).toBe(second);
86
+ expect(first.matches.map((m) => m.name)).toEqual(["a"]);
87
+ });
88
+
89
+ test("caches per ref independently", async () => {
90
+ // GIVEN a loader that echoes the requested ref
91
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, [ref]));
92
+
93
+ // WHEN we request two different refs
94
+ const main = await getPluginCatalog("main", deps);
95
+ const branch = await getPluginCatalog("feature", deps);
96
+
97
+ // THEN each ref triggers its own load and is keyed separately
98
+ expect(loadSpy).toHaveBeenCalledTimes(2);
99
+ expect(main.ref).toBe("main");
100
+ expect(branch.ref).toBe("feature");
101
+ });
102
+
103
+ test("refreshes after the TTL elapses", async () => {
104
+ // GIVEN a fresh load at t0
105
+ setSystemTime(new Date(BASE_TIME_MS));
106
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["v1"]));
107
+ await getPluginCatalog("main", deps);
108
+
109
+ // AND the loader now returns updated data
110
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["v2"]));
111
+
112
+ // WHEN the TTL has elapsed and we request again
113
+ setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
114
+ const refreshed = await getPluginCatalog("main", deps);
115
+
116
+ // THEN the loader is consulted again and the new catalog is returned
117
+ expect(loadSpy).toHaveBeenCalledTimes(2);
118
+ expect(refreshed.matches.map((m) => m.name)).toEqual(["v2"]);
119
+ });
120
+
121
+ test("serves the stale catalog when a refresh fails", async () => {
122
+ // GIVEN a good catalog cached at t0
123
+ setSystemTime(new Date(BASE_TIME_MS));
124
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
125
+ const fresh = await getPluginCatalog("main", deps);
126
+
127
+ // AND the upstream later fails (rate-limited)
128
+ loadSpy.mockImplementation(async () => {
129
+ throw new PluginCatalogUnavailableError("rate limited", 403);
130
+ });
131
+
132
+ // WHEN the TTL has elapsed and the refresh fails
133
+ setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
134
+ const stale = await getPluginCatalog("main", deps);
135
+
136
+ // THEN the last good catalog is served instead of throwing
137
+ expect(stale).toBe(fresh);
138
+ expect(stale.matches.map((m) => m.name)).toEqual(["good"]);
139
+ });
140
+
141
+ test("resets the TTL window after serving stale so it doesn't re-fetch every call", async () => {
142
+ // GIVEN a cached catalog and a now-failing upstream
143
+ setSystemTime(new Date(BASE_TIME_MS));
144
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
145
+ await getPluginCatalog("main", deps);
146
+ loadSpy.mockImplementation(async () => {
147
+ throw new PluginCatalogUnavailableError("rate limited", 403);
148
+ });
149
+
150
+ // WHEN the TTL elapses (one failed refresh) and we immediately ask again
151
+ setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
152
+ await getPluginCatalog("main", deps);
153
+ const callsAfterFirstStale = loadSpy.mock.calls.length;
154
+ await getPluginCatalog("main", deps);
155
+
156
+ // THEN the second request is served from the reset window without
157
+ // re-entering the failing loader
158
+ expect(loadSpy.mock.calls.length).toBe(callsAfterFirstStale);
159
+ });
160
+
161
+ test("propagates a hard error even with a cache, and does not serve stale", async () => {
162
+ // GIVEN a good catalog cached at t0
163
+ setSystemTime(new Date(BASE_TIME_MS));
164
+ loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
165
+ await getPluginCatalog("main", deps);
166
+
167
+ // AND the upstream later fails with a HARD error (e.g. the prefix is
168
+ // gone), NOT a transient PluginCatalogUnavailableError
169
+ loadSpy.mockImplementation(async () => {
170
+ throw new Error("GitHub contents listing failed: HTTP 404");
171
+ });
172
+
173
+ // WHEN the TTL has elapsed and the refresh hard-fails
174
+ setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
175
+ const err = await getPluginCatalog("main", deps).catch((e: unknown) => e);
176
+
177
+ // THEN the hard error propagates instead of masking the misconfiguration
178
+ // with stale data
179
+ expect(err).toBeInstanceOf(Error);
180
+ expect(err).not.toBeInstanceOf(PluginCatalogUnavailableError);
181
+ expect((err as Error).message).toMatch(/HTTP 404/);
182
+ });
183
+
184
+ test("propagates the error when there is no cache to fall back on", async () => {
185
+ // GIVEN no prior successful load and a failing upstream
186
+ loadSpy.mockImplementation(async () => {
187
+ throw new PluginCatalogUnavailableError("rate limited", 403);
188
+ });
189
+
190
+ // WHEN we request a cold catalog
191
+ const err = await getPluginCatalog("main", deps).catch((e: unknown) => e);
192
+
193
+ // THEN the failure propagates so the caller can map it to 503
194
+ expect(err).toBeInstanceOf(PluginCatalogUnavailableError);
195
+ });
196
+ });
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Tests for {@link getPluginDetails}.
3
+ *
4
+ * Network is replaced with an in-memory GitHub Contents API fixture passed via
5
+ * the `fetch` dependency, and the installed-copy path is exercised against a
6
+ * real temp directory passed via `workspacePluginsDir` — no globals are
7
+ * monkey-patched. The fixture answers three URL shapes:
8
+ * - the marketplace manifest file (raw JSON body),
9
+ * - directory listings (`/contents/<path>` → entry array or 404),
10
+ * - raw file downloads (a listing entry's `download_url`).
11
+ */
12
+
13
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
17
+
18
+ import type { FetchLike } from "../install-from-github.js";
19
+ import {
20
+ getPluginDetails,
21
+ PluginDetailsNotFoundError,
22
+ } from "../plugin-details.js";
23
+
24
+ interface ContentEntry {
25
+ name: string;
26
+ path: string;
27
+ type: "file" | "dir" | "symlink" | "submodule";
28
+ download_url: string | null;
29
+ }
30
+
31
+ function fileEntry(name: string, downloadUrl: string): ContentEntry {
32
+ return { name, path: name, type: "file", download_url: downloadUrl };
33
+ }
34
+
35
+ interface FixtureConfig {
36
+ /** Manifest object served at `experimental/plugins/marketplace.json`. Omit for 404. */
37
+ marketplace?: unknown;
38
+ /** Directory listings keyed by `<owner>/<repo>[/<path>]`. Missing key → 404. */
39
+ listings?: Record<string, ContentEntry[]>;
40
+ /** Raw file bodies keyed by `download_url`. Missing key → 404. */
41
+ raw?: Record<string, string>;
42
+ /** URL substrings that should reject with a network error. */
43
+ failOn?: string[];
44
+ }
45
+
46
+ /**
47
+ * Build a `fetch` that routes GitHub Contents API requests against in-memory
48
+ * fixtures. Anything unrecognised returns 500 so test bugs surface loudly.
49
+ */
50
+ function makeFetch(config: FixtureConfig): FetchLike {
51
+ return (async (input: RequestInfo | URL) => {
52
+ const url = typeof input === "string" ? input : input.toString();
53
+
54
+ for (const needle of config.failOn ?? []) {
55
+ if (url.includes(needle)) throw new Error(`network down: ${needle}`);
56
+ }
57
+
58
+ if (url.includes("marketplace.json")) {
59
+ if (config.marketplace === undefined) {
60
+ return new Response("not found", { status: 404 });
61
+ }
62
+ return new Response(JSON.stringify(config.marketplace), { status: 200 });
63
+ }
64
+
65
+ if (config.raw && url in config.raw) {
66
+ return new Response(config.raw[url], { status: 200 });
67
+ }
68
+
69
+ if (url.includes("/contents")) {
70
+ const key = listingKey(url);
71
+ const entries = config.listings?.[key];
72
+ if (!entries) return new Response("not found", { status: 404 });
73
+ return new Response(JSON.stringify(entries), {
74
+ status: 200,
75
+ headers: { "content-type": "application/json" },
76
+ });
77
+ }
78
+
79
+ return new Response("unexpected url: " + url, { status: 500 });
80
+ }) as FetchLike;
81
+ }
82
+
83
+ /** Derive the `<owner>/<repo>[/<path>]` listing key from a contents URL. */
84
+ function listingKey(url: string): string {
85
+ const afterRepos = url.split("/repos/")[1] ?? "";
86
+ const [ownerRepo, rest = ""] = splitOnce(afterRepos, "/contents");
87
+ const pathPart = rest.split("?")[0] ?? ""; // leading "/" or ""
88
+ return `${ownerRepo}${pathPart}`;
89
+ }
90
+
91
+ function splitOnce(s: string, sep: string): [string, string] {
92
+ const i = s.indexOf(sep);
93
+ if (i === -1) return [s, ""];
94
+ return [s.slice(0, i), s.slice(i + sep.length)];
95
+ }
96
+
97
+ let workspace: string;
98
+
99
+ beforeEach(() => {
100
+ workspace = mkdtempSync(join(tmpdir(), "plugin-details-"));
101
+ });
102
+
103
+ afterEach(() => {
104
+ rmSync(workspace, { recursive: true, force: true });
105
+ });
106
+
107
+ describe("getPluginDetails", () => {
108
+ test("resolves an external plugin: manifest metadata + repo README/package.json", async () => {
109
+ // GIVEN a marketplace entry for an external, not-installed plugin
110
+ const fetch = makeFetch({
111
+ marketplace: {
112
+ name: "vellum",
113
+ plugins: [
114
+ {
115
+ name: "caveman",
116
+ source: {
117
+ source: "github",
118
+ repo: "example-org/caveman",
119
+ ref: "v1",
120
+ },
121
+ description: "manifest description",
122
+ homepage: "https://example.com/caveman",
123
+ license: "MIT",
124
+ },
125
+ ],
126
+ },
127
+ // AND no first-party directory for the name (404), but the external repo
128
+ // root lists a README and package.json
129
+ listings: {
130
+ "example-org/caveman": [
131
+ fileEntry("README.md", "raw://caveman/readme"),
132
+ fileEntry("package.json", "raw://caveman/pkg"),
133
+ ],
134
+ },
135
+ raw: {
136
+ "raw://caveman/readme": "# Caveman\n\nGrug brain plugin.",
137
+ "raw://caveman/pkg": JSON.stringify({
138
+ version: "1.8.2",
139
+ description: "package description",
140
+ homepage: "https://pkg.example.com",
141
+ }),
142
+ },
143
+ });
144
+
145
+ // WHEN we resolve the detail view at a ref
146
+ const details = await getPluginDetails(
147
+ { name: "caveman", ref: "v1" },
148
+ { fetch, workspacePluginsDir: workspace },
149
+ );
150
+
151
+ // THEN the source is the external repo and the README comes from it
152
+ expect(details.source).toEqual({
153
+ kind: "github",
154
+ repo: "example-org/caveman",
155
+ ref: "v1",
156
+ });
157
+ expect(details.installed).toBe(false);
158
+ expect(details.readme).toContain("Grug brain plugin");
159
+ // AND manifest fields win over the repo package.json for description/homepage
160
+ expect(details.description).toBe("manifest description");
161
+ expect(details.homepage).toBe("https://example.com/caveman");
162
+ expect(details.license).toBe("MIT");
163
+ // AND version falls back to the repo package.json (manifest has none)
164
+ expect(details.version).toBe("1.8.2");
165
+ expect(details.ref).toBe("v1");
166
+ });
167
+
168
+ test("reads an external plugin at its pinned source ref, not the catalog ref", async () => {
169
+ // GIVEN a marketplace entry pinned to a ref that differs from the catalog
170
+ // ref the detail view is resolved at
171
+ const marketplace = {
172
+ name: "vellum",
173
+ plugins: [
174
+ {
175
+ name: "caveman",
176
+ source: {
177
+ source: "github",
178
+ repo: "example-org/caveman",
179
+ ref: "v1.8.2",
180
+ },
181
+ },
182
+ ],
183
+ };
184
+ // AND a fetch that records the ref query param of every contents request
185
+ const contentsRefs = new Map<string, string>();
186
+ const fetch = (async (input: RequestInfo | URL) => {
187
+ const url = typeof input === "string" ? input : input.toString();
188
+ if (url.includes("marketplace.json")) {
189
+ return new Response(JSON.stringify(marketplace), { status: 200 });
190
+ }
191
+ if (url.includes("/contents")) {
192
+ const ref = new URL(url).searchParams.get("ref") ?? "";
193
+ const key = url.includes("example-org/caveman")
194
+ ? "external"
195
+ : "first-party";
196
+ contentsRefs.set(key, ref);
197
+ if (key === "external") {
198
+ return new Response(
199
+ JSON.stringify([fileEntry("README.md", "raw://caveman/readme")]),
200
+ { status: 200, headers: { "content-type": "application/json" } },
201
+ );
202
+ }
203
+ return new Response("not found", { status: 404 });
204
+ }
205
+ if (url === "raw://caveman/readme") {
206
+ return new Response("# Caveman", { status: 200 });
207
+ }
208
+ return new Response("unexpected url: " + url, { status: 500 });
209
+ }) as FetchLike;
210
+
211
+ // WHEN we resolve the detail view at the catalog ref `main`
212
+ const details = await getPluginDetails(
213
+ { name: "caveman", ref: "main" },
214
+ { fetch, workspacePluginsDir: workspace },
215
+ );
216
+
217
+ // THEN the external repo is read at the plugin's pinned ref, not `main`
218
+ expect(contentsRefs.get("external")).toBe("v1.8.2");
219
+ // AND the first-party collision probe still uses the catalog ref
220
+ expect(contentsRefs.get("first-party")).toBe("main");
221
+ // AND the README from the pinned ref is surfaced
222
+ expect(details.readme).toContain("Caveman");
223
+ });
224
+
225
+ test("first-party directory wins a name also claimed by the marketplace", async () => {
226
+ // GIVEN both a marketplace entry AND a first-party in-repo directory for
227
+ // the same name
228
+ const fetch = makeFetch({
229
+ marketplace: {
230
+ name: "vellum",
231
+ plugins: [
232
+ {
233
+ name: "simple-memory",
234
+ source: {
235
+ source: "github",
236
+ repo: "example-org/simple-memory",
237
+ ref: "v9",
238
+ },
239
+ },
240
+ ],
241
+ },
242
+ listings: {
243
+ "vellum-ai/vellum-assistant/experimental/plugins/simple-memory": [
244
+ fileEntry("README.md", "raw://fp/readme"),
245
+ ],
246
+ },
247
+ raw: { "raw://fp/readme": "# Simple Memory (first-party)" },
248
+ });
249
+
250
+ // WHEN we resolve the detail view
251
+ const details = await getPluginDetails(
252
+ { name: "simple-memory" },
253
+ { fetch, workspacePluginsDir: workspace },
254
+ );
255
+
256
+ // THEN the first-party source is used, not the external one
257
+ expect(details.source).toEqual({ kind: "first-party" });
258
+ expect(details.readme).toContain("first-party");
259
+ });
260
+
261
+ test("prefers an installed copy's README and package.json over the repo", async () => {
262
+ // GIVEN an installed copy on disk with its own README + package.json
263
+ const target = join(workspace, "caveman");
264
+ mkdirSync(target, { recursive: true });
265
+ writeFileSync(join(target, "README.md"), "# Installed Caveman");
266
+ writeFileSync(
267
+ join(target, "package.json"),
268
+ JSON.stringify({ version: "2.0.0", license: "Apache-2.0" }),
269
+ );
270
+
271
+ // AND a marketplace entry + external repo that would otherwise be used
272
+ const fetch = makeFetch({
273
+ marketplace: {
274
+ name: "vellum",
275
+ plugins: [
276
+ {
277
+ name: "caveman",
278
+ source: {
279
+ source: "github",
280
+ repo: "example-org/caveman",
281
+ ref: "v1",
282
+ },
283
+ description: "manifest description",
284
+ },
285
+ ],
286
+ },
287
+ listings: {
288
+ "example-org/caveman": [fileEntry("README.md", "raw://caveman/readme")],
289
+ },
290
+ raw: { "raw://caveman/readme": "# Remote Caveman" },
291
+ });
292
+
293
+ // WHEN we resolve the detail view
294
+ const details = await getPluginDetails(
295
+ { name: "caveman" },
296
+ { fetch, workspacePluginsDir: workspace },
297
+ );
298
+
299
+ // THEN the installed README + version/license win; manifest fills the gap
300
+ expect(details.installed).toBe(true);
301
+ expect(details.readme).toBe("# Installed Caveman");
302
+ expect(details.version).toBe("2.0.0");
303
+ expect(details.license).toBe("Apache-2.0");
304
+ expect(details.description).toBe("manifest description");
305
+ });
306
+
307
+ test("throws PluginDetailsNotFoundError when nothing claims the name", async () => {
308
+ // GIVEN no install, no first-party directory, and an empty marketplace
309
+ const fetch = makeFetch({
310
+ marketplace: { name: "vellum", plugins: [] },
311
+ });
312
+
313
+ // WHEN / THEN resolving an unknown name rejects with the not-found error
314
+ await expect(
315
+ getPluginDetails(
316
+ { name: "ghost" },
317
+ { fetch, workspacePluginsDir: workspace },
318
+ ),
319
+ ).rejects.toBeInstanceOf(PluginDetailsNotFoundError);
320
+ });
321
+
322
+ test("degrades to manifest metadata when the repo listing fails", async () => {
323
+ // GIVEN a marketplace entry whose external repo listing errors out
324
+ const fetch = makeFetch({
325
+ marketplace: {
326
+ name: "vellum",
327
+ plugins: [
328
+ {
329
+ name: "caveman",
330
+ source: {
331
+ source: "github",
332
+ repo: "example-org/caveman",
333
+ ref: "v1",
334
+ },
335
+ description: "manifest description",
336
+ license: "MIT",
337
+ },
338
+ ],
339
+ },
340
+ failOn: ["example-org/caveman"],
341
+ });
342
+
343
+ // WHEN we resolve the detail view
344
+ const details = await getPluginDetails(
345
+ { name: "caveman", ref: "v1" },
346
+ { fetch, workspacePluginsDir: workspace },
347
+ );
348
+
349
+ // THEN it still renders manifest metadata with a null README rather than throwing
350
+ expect(details.readme).toBeNull();
351
+ expect(details.description).toBe("manifest description");
352
+ expect(details.license).toBe("MIT");
353
+ expect(details.source).toEqual({
354
+ kind: "github",
355
+ repo: "example-org/caveman",
356
+ ref: "v1",
357
+ });
358
+ });
359
+
360
+ test("rejects an invalid (path-traversal) plugin name", async () => {
361
+ // GIVEN a name that fails the install-name sanitizer
362
+ const fetch = makeFetch({ marketplace: { name: "vellum", plugins: [] } });
363
+
364
+ // WHEN / THEN resolution throws before any lookup
365
+ await expect(
366
+ getPluginDetails(
367
+ { name: "../escape" },
368
+ { fetch, workspacePluginsDir: workspace },
369
+ ),
370
+ ).rejects.toThrow();
371
+ });
372
+ });