@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -55,8 +55,12 @@ function catalog(ref: string, names: string[]): PluginCatalog {
55
55
  ref,
56
56
  matches: names.map((name) => ({
57
57
  name,
58
- path: `experimental/plugins/${name}`,
59
- source: { kind: "first-party" as const },
58
+ path: `github:acme/${name}@${"0".repeat(40)}`,
59
+ source: {
60
+ kind: "github" as const,
61
+ repo: `acme/${name}`,
62
+ ref: "0".repeat(40),
63
+ },
60
64
  })),
61
65
  };
62
66
  }
@@ -33,7 +33,7 @@ function fileEntry(name: string, downloadUrl: string): ContentEntry {
33
33
  }
34
34
 
35
35
  interface FixtureConfig {
36
- /** Manifest object served at `experimental/plugins/marketplace.json`. Omit for 404. */
36
+ /** Manifest object served at `plugins/marketplace.json`. Omit for 404. */
37
37
  marketplace?: unknown;
38
38
  /** Directory listings keyed by `<owner>/<repo>[/<path>]`. Missing key → 404. */
39
39
  listings?: Record<string, ContentEntry[]>;
@@ -124,8 +124,7 @@ describe("getPluginDetails", () => {
124
124
  },
125
125
  ],
126
126
  },
127
- // AND no first-party directory for the name (404), but the external repo
128
- // root lists a README and package.json
127
+ // AND the external repo root lists a README and package.json
129
128
  listings: {
130
129
  "example-org/caveman": [
131
130
  fileEntry("README.md", "raw://caveman/readme"),
@@ -190,9 +189,7 @@ describe("getPluginDetails", () => {
190
189
  }
191
190
  if (url.includes("/contents")) {
192
191
  const ref = new URL(url).searchParams.get("ref") ?? "";
193
- const key = url.includes("example-org/caveman")
194
- ? "external"
195
- : "first-party";
192
+ const key = url.includes("example-org/caveman") ? "external" : "other";
196
193
  contentsRefs.set(key, ref);
197
194
  if (key === "external") {
198
195
  return new Response(
@@ -218,17 +215,15 @@ describe("getPluginDetails", () => {
218
215
  expect(contentsRefs.get("external")).toBe(
219
216
  "2222222222222222222222222222222222222222",
220
217
  );
221
- // AND the marketplace entry owns the name, so the first-party directory is
222
- // never probed (its same-named in-repo dir would be the adapter stub)
223
- expect(contentsRefs.has("first-party")).toBe(false);
218
+ // AND the pinned external repo is the only contents request — no other
219
+ // GitHub lookups are made for the name
220
+ expect(contentsRefs.has("other")).toBe(false);
224
221
  // AND the README from the pinned ref is surfaced
225
222
  expect(details.readme).toContain("Caveman");
226
223
  });
227
224
 
228
- test("a marketplace entry owns a name shared by an in-repo stub dir", async () => {
229
- // GIVEN a marketplace entry for "simple-memory"
230
- // AND a same-named in-repo directory — caveman-style, this is the plugin's
231
- // adapter stub, not a standalone first-party plugin
225
+ test("resolves the external marketplace source for an uninstalled plugin", async () => {
226
+ // GIVEN a marketplace entry for "simple-memory" and no installed copy
232
227
  const fetch = makeFetch({
233
228
  marketplace: {
234
229
  name: "vellum",
@@ -257,8 +252,7 @@ describe("getPluginDetails", () => {
257
252
  { fetch, workspacePluginsDir: workspace },
258
253
  );
259
254
 
260
- // THEN the external marketplace source is used — the stub dir is an
261
- // adapter overlay for the clone, so it does not override the claim
255
+ // THEN the external marketplace source is used
262
256
  expect(details.source).toEqual({
263
257
  kind: "github",
264
258
  repo: "example-org/simple-memory",
@@ -314,7 +308,7 @@ describe("getPluginDetails", () => {
314
308
  });
315
309
 
316
310
  test("throws PluginDetailsNotFoundError when nothing claims the name", async () => {
317
- // GIVEN no install, no first-party directory, and an empty marketplace
311
+ // GIVEN no installed copy and an empty marketplace
318
312
  const fetch = makeFetch({
319
313
  marketplace: { name: "vellum", plugins: [] },
320
314
  });
@@ -16,7 +16,7 @@ import {
16
16
  } from "../plugin-marketplace.js";
17
17
 
18
18
  const MANIFEST_URL_PREFIX =
19
- "https://api.github.com/repos/vellum-ai/vellum-assistant/contents/experimental/plugins/marketplace.json";
19
+ "https://api.github.com/repos/vellum-ai/vellum-assistant/contents/plugins/marketplace.json";
20
20
 
21
21
  // External marketplace refs must be full commit SHAs (immutable). Fixtures use
22
22
  // realistic 40-char hex object names rather than tags/branches.
@@ -264,7 +264,7 @@ describe("resolveMarketplaceSource", () => {
264
264
  test("returns null for a name not in the whitelist", () => {
265
265
  // GIVEN whitelisted entries
266
266
  // WHEN resolving an unknown name
267
- // THEN no source is returned (caller falls back to first-party)
267
+ // THEN no source is returned (the name is not installable)
268
268
  expect(resolveMarketplaceSource("unknown", entries)).toBeNull();
269
269
  });
270
270
  });
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Tests for {@link searchPlugins}.
3
3
  *
4
- * Network is replaced with an in-memory fixture passed via the `fetch`
5
- * dependency — no globals are monkey-patched and no `--test-hook` exports
6
- * leak into production code.
4
+ * The catalog is the whitelisted external plugins in the curated
5
+ * `plugins/marketplace.json` manifest. Network is replaced with an in-memory
6
+ * fixture passed via the `fetch` dependency — no globals are monkey-patched and
7
+ * no `--test-hook` exports leak into production code.
7
8
  */
8
9
 
9
10
  import { describe, expect, test } from "bun:test";
10
11
 
12
+ import { MarketplaceFetchError } from "../plugin-marketplace.js";
11
13
  import {
12
14
  type FetchLike,
13
15
  InvalidSearchPatternError,
@@ -15,52 +17,64 @@ import {
15
17
  searchPlugins,
16
18
  } from "../search-plugins.js";
17
19
 
18
- /**
19
- * Build a GitHub Contents API fixture from an in-memory directory listing.
20
- *
21
- * `entries` maps each name under `experimental/plugins/` to its `type`. The
22
- * fixture answers GET requests against
23
- * - `https://api.github.com/repos/vellum-ai/vellum-assistant/contents/experimental/plugins...`
24
- * and returns 500 for anything else (forces test bugs to surface loudly).
25
- */
26
- function fixtureFetch(
27
- entries: Record<string, "dir" | "file" | "symlink" | "submodule">,
28
- ): FetchLike {
29
- const PREFIX_API =
30
- "https://api.github.com/repos/vellum-ai/vellum-assistant/contents/experimental/plugins";
20
+ const MANIFEST_URL_PREFIX =
21
+ "https://api.github.com/repos/vellum-ai/vellum-assistant/contents/plugins/marketplace.json";
22
+
23
+ // External marketplace refs must be full commit SHAs (immutable). Fixtures use
24
+ // realistic 40-char hex object names rather than tags/branches.
25
+ const SHA_A = "63a91ecadbf4c4719a4602a5abb00883f9966034";
26
+ const SHA_B = "0123456789abcdef0123456789abcdef01234567";
27
+ const SHA_C = "89abcdef0123456789abcdef0123456789abcdef";
28
+
29
+ interface ManifestPlugin {
30
+ name: string;
31
+ source: { source: "github"; repo: string; path?: string; ref: string };
32
+ description?: string;
33
+ }
31
34
 
35
+ /** Serve `plugins` as the raw marketplace manifest at the manifest URL. */
36
+ function manifestFetch(plugins: ManifestPlugin[]): FetchLike {
32
37
  return (async (input: RequestInfo | URL) => {
33
38
  const url = typeof input === "string" ? input : input.toString();
34
- if (!url.startsWith(PREFIX_API)) {
39
+ if (!url.startsWith(MANIFEST_URL_PREFIX)) {
35
40
  return new Response("unexpected url: " + url, { status: 500 });
36
41
  }
37
- const body = Object.entries(entries).map(([name, type]) => ({
38
- name,
39
- path: `experimental/plugins/${name}`,
40
- type,
41
- size: type === "file" ? 1 : 0,
42
- download_url:
43
- type === "file"
44
- ? `https://raw.githubusercontent.com/vellum-ai/vellum-assistant/main/experimental/plugins/${name}`
45
- : null,
46
- }));
47
- return new Response(JSON.stringify(body), {
42
+ return new Response(JSON.stringify({ name: "vellum-assistant", plugins }), {
48
43
  status: 200,
49
44
  headers: { "content-type": "application/json" },
50
45
  });
51
46
  }) as FetchLike;
52
47
  }
53
48
 
49
+ /** A github-sourced manifest entry, with optional path/description. */
50
+ function entry(
51
+ name: string,
52
+ repo: string,
53
+ ref: string,
54
+ extra?: { path?: string; description?: string },
55
+ ): ManifestPlugin {
56
+ return {
57
+ name,
58
+ source: {
59
+ source: "github",
60
+ repo,
61
+ ...(extra?.path ? { path: extra.path } : {}),
62
+ ref,
63
+ },
64
+ ...(extra?.description ? { description: extra.description } : {}),
65
+ };
66
+ }
67
+
54
68
  describe("searchPlugins", () => {
55
- test("matches the query as a case-insensitive regex against directory names", async () => {
69
+ test("matches the query as a case-insensitive regex against plugin names", async () => {
56
70
  const result = await searchPlugins(
57
71
  { query: "memory" },
58
72
  {
59
- fetch: fixtureFetch({
60
- "simple-memory": "dir",
61
- "memory-graph": "dir",
62
- "git-tools": "dir",
63
- }),
73
+ fetch: manifestFetch([
74
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
75
+ entry("memory-graph", "acme/memory-graph", SHA_B),
76
+ entry("git-tools", "acme/git-tools", SHA_C),
77
+ ]),
64
78
  },
65
79
  );
66
80
 
@@ -68,7 +82,7 @@ describe("searchPlugins", () => {
68
82
  "memory-graph",
69
83
  "simple-memory",
70
84
  ]);
71
- expect(result.matches[0]!.path).toBe("experimental/plugins/memory-graph");
85
+ expect(result.matches[0]!.path).toBe(`github:acme/memory-graph@${SHA_B}`);
72
86
  expect(result.query).toBe("memory");
73
87
  expect(result.ref).toBe("main");
74
88
  });
@@ -76,7 +90,11 @@ describe("searchPlugins", () => {
76
90
  test("matches regardless of query casing (case-insensitive)", async () => {
77
91
  const result = await searchPlugins(
78
92
  { query: "MEMORY" },
79
- { fetch: fixtureFetch({ "simple-memory": "dir" }) },
93
+ {
94
+ fetch: manifestFetch([
95
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
96
+ ]),
97
+ },
80
98
  );
81
99
  expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
82
100
  });
@@ -85,24 +103,24 @@ describe("searchPlugins", () => {
85
103
  const result = await searchPlugins(
86
104
  { query: "^memory-" },
87
105
  {
88
- fetch: fixtureFetch({
89
- "memory-graph": "dir",
90
- "simple-memory": "dir",
91
- }),
106
+ fetch: manifestFetch([
107
+ entry("memory-graph", "acme/memory-graph", SHA_B),
108
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
109
+ ]),
92
110
  },
93
111
  );
94
112
  expect(result.matches.map((m) => m.name)).toEqual(["memory-graph"]);
95
113
  });
96
114
 
97
- test("empty query matches all directories", async () => {
115
+ test("empty query matches every whitelisted entry", async () => {
98
116
  const result = await searchPlugins(
99
117
  { query: "" },
100
118
  {
101
- fetch: fixtureFetch({
102
- "simple-memory": "dir",
103
- "memory-graph": "dir",
104
- "git-tools": "dir",
105
- }),
119
+ fetch: manifestFetch([
120
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
121
+ entry("memory-graph", "acme/memory-graph", SHA_B),
122
+ entry("git-tools", "acme/git-tools", SHA_C),
123
+ ]),
106
124
  },
107
125
  );
108
126
  expect(result.matches.map((m) => m.name)).toEqual([
@@ -112,19 +130,35 @@ describe("searchPlugins", () => {
112
130
  ]);
113
131
  });
114
132
 
115
- test("skips entries that are not directories", async () => {
133
+ test("projects each entry onto a github source with a display locator", async () => {
134
+ // GIVEN a whitelisted external plugin nested in a subdirectory
135
+ // WHEN we search
116
136
  const result = await searchPlugins(
117
- { query: "" },
137
+ { query: "nested" },
118
138
  {
119
- fetch: fixtureFetch({
120
- "simple-memory": "dir",
121
- "README.md": "file",
122
- "broken-symlink": "symlink",
123
- "old-plugin": "submodule",
124
- }),
139
+ fetch: manifestFetch([
140
+ entry("nested", "acme/monorepo", SHA_B, {
141
+ path: "packages/nested",
142
+ description: "A nested plugin.",
143
+ }),
144
+ ]),
125
145
  },
126
146
  );
127
- expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
147
+
148
+ // THEN the match carries the github coordinates and a human locator
149
+ expect(result.matches).toEqual([
150
+ {
151
+ name: "nested",
152
+ path: `github:acme/monorepo/packages/nested@${SHA_B}`,
153
+ description: "A nested plugin.",
154
+ source: {
155
+ kind: "github",
156
+ repo: "acme/monorepo",
157
+ path: "packages/nested",
158
+ ref: SHA_B,
159
+ },
160
+ },
161
+ ]);
128
162
  });
129
163
 
130
164
  test("rejects invalid regex patterns up front (no network call)", async () => {
@@ -143,7 +177,11 @@ describe("searchPlugins", () => {
143
177
  test("empty result set on no matches", async () => {
144
178
  const result = await searchPlugins(
145
179
  { query: "nothing-matches" },
146
- { fetch: fixtureFetch({ "simple-memory": "dir" }) },
180
+ {
181
+ fetch: manifestFetch([
182
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
183
+ ]),
184
+ },
147
185
  );
148
186
  expect(result.matches).toEqual([]);
149
187
  });
@@ -161,15 +199,12 @@ describe("searchPlugins", () => {
161
199
  const m = /[?&]ref=([^&]+)/.exec(url);
162
200
  seenRef = m ? decodeURIComponent(m[1]!) : undefined;
163
201
  return new Response(
164
- JSON.stringify([
165
- {
166
- name: "simple-memory",
167
- path: "experimental/plugins/simple-memory",
168
- type: "dir",
169
- size: 0,
170
- download_url: null,
171
- },
172
- ]),
202
+ JSON.stringify({
203
+ name: "vellum-assistant",
204
+ plugins: [
205
+ entry("simple-memory", "vellum-ai/simple-memory", SHA_A),
206
+ ],
207
+ }),
173
208
  { status: 200, headers: { "content-type": "application/json" } },
174
209
  );
175
210
  }) as FetchLike,
@@ -233,32 +268,46 @@ describe("searchPlugins", () => {
233
268
  expect((err as Error).message).toMatch(/HTTP 403/);
234
269
  });
235
270
 
236
- test("404 on the plugins prefix is a hard error (not silently empty)", async () => {
237
- // Distinct from `installPlugin`, where 404 on a specific plugin name is
238
- // normal "not found". For the search, 404 on the prefix means the
239
- // canonical source path itself is gone — that's a real misconfiguration
240
- // worth surfacing, not a clean empty result and not a transient outage.
241
- const err = await searchPlugins(
271
+ test("a missing manifest (404) is a clean empty catalog", async () => {
272
+ // Unlike a 5xx or a bare 403, a 404 on `marketplace.json` means no
273
+ // whitelist has been published at this ref yet — a normal empty catalog,
274
+ // not a misconfiguration. The search returns no matches without error.
275
+ const result = await searchPlugins(
242
276
  { query: "memory" },
243
277
  {
244
278
  fetch: (async () =>
245
279
  new Response("not found", { status: 404 })) as FetchLike,
246
280
  },
281
+ );
282
+ expect(result.matches).toEqual([]);
283
+ });
284
+
285
+ test("a malformed manifest is a hard error (not silently empty)", async () => {
286
+ // GIVEN the manifest body is not valid JSON
287
+ // WHEN we search
288
+ // THEN it surfaces as a hard MarketplaceFetchError — the catalog is the
289
+ // source of truth, so a broken whitelist must not masquerade as empty,
290
+ // and it is NOT transient (retrying won't fix malformed bytes).
291
+ const err = await searchPlugins(
292
+ { query: "" },
293
+ {
294
+ fetch: (async () =>
295
+ new Response("{ not json", { status: 200 })) as FetchLike,
296
+ },
247
297
  ).catch((e: unknown) => e);
248
- expect(err).toBeInstanceOf(Error);
298
+ expect(err).toBeInstanceOf(MarketplaceFetchError);
249
299
  expect(err).not.toBeInstanceOf(PluginCatalogUnavailableError);
250
- expect((err as Error).message).toMatch(/HTTP 404/);
251
300
  });
252
301
 
253
302
  test("returns matches sorted by name", async () => {
254
303
  const result = await searchPlugins(
255
304
  { query: "" },
256
305
  {
257
- fetch: fixtureFetch({
258
- "zeta-plugin": "dir",
259
- "alpha-plugin": "dir",
260
- "mu-plugin": "dir",
261
- }),
306
+ fetch: manifestFetch([
307
+ entry("zeta-plugin", "acme/zeta", SHA_A),
308
+ entry("alpha-plugin", "acme/alpha", SHA_B),
309
+ entry("mu-plugin", "acme/mu", SHA_C),
310
+ ]),
262
311
  },
263
312
  );
264
313
  expect(result.matches.map((m) => m.name)).toEqual([
@@ -268,177 +317,33 @@ describe("searchPlugins", () => {
268
317
  ]);
269
318
  });
270
319
 
271
- test("merges whitelisted marketplace entries with first-party dirs", async () => {
272
- // GIVEN the canonical repo serves both a first-party plugin listing and a
273
- // marketplace manifest whitelisting an external plugin
274
- const manifest = {
275
- name: "vellum-assistant",
276
- plugins: [
277
- {
278
- name: "caveman",
279
- source: {
280
- source: "github",
281
- repo: "JuliusBrussee/caveman",
282
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
283
- },
284
- description: "Ultra-compressed communication mode.",
285
- },
286
- ],
287
- };
288
- const fetch: FetchLike = (async (input: RequestInfo | URL) => {
289
- const url = typeof input === "string" ? input : input.toString();
290
- if (url.includes("marketplace.json")) {
291
- return new Response(JSON.stringify(manifest), { status: 200 });
292
- }
293
- return new Response(
294
- JSON.stringify([
295
- {
296
- name: "simple-memory",
297
- path: "experimental/plugins/simple-memory",
298
- type: "dir",
299
- size: 0,
300
- download_url: null,
301
- },
302
- ]),
303
- { status: 200, headers: { "content-type": "application/json" } },
304
- );
305
- }) as FetchLike;
306
-
307
- // WHEN we search the catalog with a match-all query
308
- const result = await searchPlugins({ query: "" }, { fetch });
309
-
310
- // THEN both sources appear, sorted by name, each tagged with its origin
311
- expect(result.matches).toEqual([
312
- {
313
- name: "caveman",
314
- path: "github:JuliusBrussee/caveman@63a91ecadbf4c4719a4602a5abb00883f9966034",
315
- description: "Ultra-compressed communication mode.",
316
- source: {
317
- kind: "github",
318
- repo: "JuliusBrussee/caveman",
319
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
320
- },
321
- },
320
+ test("dedupes entries that repeat a name, keeping the first", async () => {
321
+ // GIVEN a manifest that lists the same name twice (e.g. a bad edit)
322
+ // WHEN we search
323
+ // THEN the name surfaces once, from the first entry, so a duplicate can't
324
+ // inflate the catalog or shadow the reviewed source
325
+ const result = await searchPlugins(
326
+ { query: "caveman" },
322
327
  {
323
- name: "simple-memory",
324
- path: "experimental/plugins/simple-memory",
325
- source: { kind: "first-party" },
326
- },
327
- ]);
328
- });
329
-
330
- test("filters marketplace entries by the query too", async () => {
331
- // GIVEN a marketplace whitelisting an external plugin and no first-party dirs
332
- const manifest = {
333
- name: "vellum-assistant",
334
- plugins: [
335
- {
336
- name: "caveman",
337
- source: {
338
- source: "github",
339
- repo: "JuliusBrussee/caveman",
340
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
341
- },
342
- },
343
- ],
344
- };
345
- const fetch: FetchLike = (async (input: RequestInfo | URL) => {
346
- const url = typeof input === "string" ? input : input.toString();
347
- if (url.includes("marketplace.json")) {
348
- return new Response(JSON.stringify(manifest), { status: 200 });
349
- }
350
- return new Response(JSON.stringify([]), { status: 200 });
351
- }) as FetchLike;
352
-
353
- // WHEN the query matches no plugin name
354
- const result = await searchPlugins({ query: "memory" }, { fetch });
355
-
356
- // THEN the non-matching marketplace entry is excluded
357
- expect(result.matches).toEqual([]);
358
- });
359
-
360
- test("a marketplace entry owns a name shared by an in-repo stub dir", async () => {
361
- // GIVEN a marketplace entry named "caveman"
362
- const manifest = {
363
- name: "vellum-assistant",
364
- plugins: [
365
- {
366
- name: "caveman",
367
- source: {
368
- source: "github",
369
- repo: "JuliusBrussee/caveman",
370
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
371
- },
372
- },
373
- ],
374
- };
375
- // AND a same-named in-repo directory, which is caveman's adapter stub
376
- // rather than a standalone first-party plugin
377
- const fetch: FetchLike = (async (input: RequestInfo | URL) => {
378
- const url = typeof input === "string" ? input : input.toString();
379
- if (url.includes("marketplace.json")) {
380
- return new Response(JSON.stringify(manifest), { status: 200 });
381
- }
382
- return new Response(
383
- JSON.stringify([
384
- {
385
- name: "caveman",
386
- path: "experimental/plugins/caveman",
387
- type: "dir",
388
- size: 0,
389
- download_url: null,
390
- },
328
+ fetch: manifestFetch([
329
+ entry("caveman", "JuliusBrussee/caveman", SHA_A),
330
+ entry("caveman", "impostor/caveman", SHA_B),
391
331
  ]),
392
- { status: 200 },
393
- );
394
- }) as FetchLike;
395
-
396
- // WHEN we search
397
- const result = await searchPlugins({ query: "caveman" }, { fetch });
398
-
399
- // THEN the name surfaces once, as the external marketplace entry — the
400
- // stub dir is its adapter overlay, not a competing first-party listing
332
+ },
333
+ );
401
334
  expect(result.matches).toEqual([
402
335
  {
403
336
  name: "caveman",
404
- path: "github:JuliusBrussee/caveman@63a91ecadbf4c4719a4602a5abb00883f9966034",
337
+ path: `github:JuliusBrussee/caveman@${SHA_A}`,
405
338
  source: {
406
339
  kind: "github",
407
340
  repo: "JuliusBrussee/caveman",
408
- ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
341
+ ref: SHA_A,
409
342
  },
410
343
  },
411
344
  ]);
412
345
  });
413
346
 
414
- test("a broken marketplace manifest degrades to the first-party listing", async () => {
415
- // GIVEN the manifest is malformed but the first-party listing is healthy
416
- const fetch: FetchLike = (async (input: RequestInfo | URL) => {
417
- const url = typeof input === "string" ? input : input.toString();
418
- if (url.includes("marketplace.json")) {
419
- return new Response("{ not json", { status: 200 });
420
- }
421
- return new Response(
422
- JSON.stringify([
423
- {
424
- name: "simple-memory",
425
- path: "experimental/plugins/simple-memory",
426
- type: "dir",
427
- size: 0,
428
- download_url: null,
429
- },
430
- ]),
431
- { status: 200 },
432
- );
433
- }) as FetchLike;
434
-
435
- // WHEN we search
436
- const result = await searchPlugins({ query: "" }, { fetch });
437
-
438
- // THEN the core catalog is unaffected by the broken whitelist
439
- expect(result.matches.map((m) => m.name)).toEqual(["simple-memory"]);
440
- });
441
-
442
347
  test("sends no Authorization header (canonical source is a public repo)", async () => {
443
348
  let seenAuth: string | undefined;
444
349
  let seenUserAgent: string | undefined;
@@ -449,10 +354,10 @@ describe("searchPlugins", () => {
449
354
  const headers = init?.headers as Record<string, string> | undefined;
450
355
  seenAuth = headers?.Authorization;
451
356
  seenUserAgent = headers?.["User-Agent"];
452
- return new Response("[]", {
453
- status: 200,
454
- headers: { "content-type": "application/json" },
455
- });
357
+ return new Response(
358
+ JSON.stringify({ name: "vellum-assistant", plugins: [] }),
359
+ { status: 200, headers: { "content-type": "application/json" } },
360
+ );
456
361
  }) as FetchLike,
457
362
  },
458
363
  );