@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091853.fbaa2ae

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 (475) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/docs/activation-funnel-telemetry.md +310 -0
  4. package/examples/plugins/echo/README.md +61 -60
  5. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  6. package/examples/plugins/echo/hooks/stop.ts +16 -0
  7. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  8. package/examples/plugins/echo/package.json +1 -2
  9. package/examples/plugins/echo/src/emit.ts +19 -0
  10. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  11. package/openapi.yaml +263 -121
  12. package/package.json +2 -2
  13. package/src/__tests__/activation-early-marking.test.ts +120 -0
  14. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  15. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  16. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  17. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  18. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  19. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  20. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  21. package/src/__tests__/agent-loop.test.ts +441 -96
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  23. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  24. package/src/__tests__/anthropic-provider.test.ts +23 -8
  25. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/app-executors.test.ts +132 -0
  29. package/src/__tests__/approval-cascade.test.ts +6 -5
  30. package/src/__tests__/approval-routes-http.test.ts +4 -1
  31. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  32. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  33. package/src/__tests__/channel-approvals.test.ts +1 -1
  34. package/src/__tests__/compaction-circuit.test.ts +258 -0
  35. package/src/__tests__/compaction-direct.test.ts +146 -0
  36. package/src/__tests__/compaction-events.test.ts +7 -7
  37. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  38. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  39. package/src/__tests__/context-overflow-reducer.test.ts +265 -125
  40. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  41. package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
  42. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  43. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  44. package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
  45. package/src/__tests__/conversation-agent-loop.test.ts +342 -623
  46. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  47. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
  48. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  49. package/src/__tests__/conversation-error.test.ts +15 -1
  50. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  52. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
  53. package/src/__tests__/conversation-process-callsite.test.ts +15 -15
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
  55. package/src/__tests__/conversation-queue.test.ts +10 -10
  56. package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
  57. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  58. package/src/__tests__/conversation-slash-queue.test.ts +7 -5
  59. package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
  60. package/src/__tests__/conversation-speed-override.test.ts +11 -10
  61. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  62. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
  64. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  65. package/src/__tests__/conversation-undo.test.ts +2 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
  67. package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/corrected-target.test.ts +93 -0
  71. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  72. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  73. package/src/__tests__/db-acp-history.test.ts +101 -0
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +1 -1
  76. package/src/__tests__/file-write-tool.test.ts +63 -0
  77. package/src/__tests__/gateway-only-guard.test.ts +17 -2
  78. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  79. package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
  80. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  81. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  82. package/src/__tests__/heartbeat-service.test.ts +1 -0
  83. package/src/__tests__/history-repair-hook.test.ts +5 -1
  84. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  85. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  86. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  87. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  88. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  89. package/src/__tests__/injector-background-turn.test.ts +13 -23
  90. package/src/__tests__/injector-chain.test.ts +268 -44
  91. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  92. package/src/__tests__/injector-document-comments.test.ts +97 -114
  93. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  94. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  95. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  96. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  97. package/src/__tests__/llm-resolver.test.ts +73 -0
  98. package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
  99. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  100. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  101. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  102. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  103. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  104. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  105. package/src/__tests__/plugin-bootstrap.test.ts +91 -81
  106. package/src/__tests__/plugin-registry.test.ts +3 -49
  107. package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
  108. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  109. package/src/__tests__/plugin-types.test.ts +0 -70
  110. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  111. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  112. package/src/__tests__/published-app-updater.test.ts +138 -0
  113. package/src/__tests__/reaction-persistence.test.ts +1 -1
  114. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  115. package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
  116. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  117. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  118. package/src/__tests__/subagent-detail.test.ts +25 -7
  119. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  120. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  121. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  122. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  123. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  124. package/src/__tests__/title-generate-hook.test.ts +5 -1
  125. package/src/__tests__/tool-error-hook.test.ts +1 -1
  126. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  127. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  128. package/src/__tests__/web-fetch.test.ts +45 -0
  129. package/src/acp/__tests__/agent-process.test.ts +161 -0
  130. package/src/acp/__tests__/client-handler.test.ts +40 -0
  131. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  132. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  133. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  134. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  135. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  136. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  137. package/src/acp/agent-process.ts +61 -1
  138. package/src/acp/auto-install.test.ts +280 -0
  139. package/src/acp/auto-install.ts +232 -0
  140. package/src/acp/client-handler.ts +31 -0
  141. package/src/acp/prepare-agent-env.ts +80 -27
  142. package/src/acp/resolve-agent.test.ts +188 -28
  143. package/src/acp/resolve-agent.ts +119 -42
  144. package/src/acp/resume-hint.ts +23 -0
  145. package/src/acp/session-manager.ts +507 -73
  146. package/src/agent/compaction-circuit.ts +60 -102
  147. package/src/agent/loop.ts +403 -251
  148. package/src/api/responses/conversation-message.ts +22 -2
  149. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  150. package/src/approvals/guardian-request-resolvers.ts +27 -1
  151. package/src/background-wake/next-wake.ts +1 -0
  152. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  153. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  154. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  155. package/src/cli/commands/memory-v3.ts +57 -199
  156. package/src/cli/commands/plugins.ts +43 -37
  157. package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
  158. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  159. package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
  160. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
  161. package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
  162. package/src/cli/lib/install-from-github.ts +797 -60
  163. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  164. package/src/cli/lib/plugin-details.ts +416 -0
  165. package/src/cli/lib/plugin-marketplace.ts +215 -0
  166. package/src/cli/lib/search-plugins.ts +201 -29
  167. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  168. package/src/config/acp-defaults.test.ts +10 -0
  169. package/src/config/acp-defaults.ts +9 -3
  170. package/src/config/acp-schema.ts +1 -7
  171. package/src/config/bundled-skills/acp/SKILL.md +80 -41
  172. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  173. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  174. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  175. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  176. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  177. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  178. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  180. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  181. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  182. package/src/config/bundled-tool-registry.ts +4 -0
  183. package/src/config/call-site-defaults.ts +0 -3
  184. package/src/config/feature-flag-registry.json +10 -16
  185. package/src/config/llm-resolver.ts +39 -7
  186. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  187. package/src/config/schemas/call-site-catalog.ts +0 -21
  188. package/src/config/schemas/heartbeat.ts +9 -0
  189. package/src/config/schemas/llm.ts +17 -3
  190. package/src/config/schemas/memory-v3.ts +58 -8
  191. package/src/config/seed-inference-profiles.ts +18 -0
  192. package/src/context/compactor.ts +22 -4
  193. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  194. package/src/context/strip-injections.ts +8 -2
  195. package/src/daemon/conversation-agent-loop-handlers.ts +18 -36
  196. package/src/daemon/conversation-agent-loop.ts +308 -1297
  197. package/src/daemon/conversation-error.ts +31 -4
  198. package/src/daemon/conversation-history.ts +1 -1
  199. package/src/daemon/conversation-lifecycle.ts +11 -255
  200. package/src/daemon/conversation-media-retry.ts +19 -6
  201. package/src/daemon/conversation-messaging.ts +17 -0
  202. package/src/daemon/conversation-process.ts +22 -141
  203. package/src/daemon/conversation-queue-manager.ts +8 -0
  204. package/src/daemon/conversation-registry.ts +159 -0
  205. package/src/daemon/conversation-runtime-assembly.ts +361 -393
  206. package/src/daemon/conversation-store.ts +9 -90
  207. package/src/daemon/conversation-surfaces.ts +165 -11
  208. package/src/daemon/conversation-workspace.ts +17 -0
  209. package/src/daemon/conversation.ts +444 -62
  210. package/src/daemon/disk-pressure-policy.ts +0 -1
  211. package/src/daemon/external-plugins-bootstrap.ts +36 -46
  212. package/src/daemon/handlers/config-channels.ts +11 -2
  213. package/src/daemon/handlers/conversations.ts +3 -1
  214. package/src/daemon/handlers/skills.ts +4 -1
  215. package/src/daemon/host-app-control-proxy.ts +72 -57
  216. package/src/daemon/host-proxy-preactivation.ts +1 -3
  217. package/src/daemon/lifecycle.ts +21 -7
  218. package/src/daemon/persist-unsendable-image.ts +62 -25
  219. package/src/daemon/process-message.ts +1 -1
  220. package/src/daemon/server.ts +2 -0
  221. package/src/daemon/tool-side-effects.ts +15 -0
  222. package/src/daemon/wake-conversation-ops.ts +269 -0
  223. package/src/embedded/plugin-api.ts +2 -2
  224. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  225. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  226. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  227. package/src/heartbeat/heartbeat-service.ts +26 -0
  228. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  229. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  230. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  231. package/src/ipc/skill-routes/memory.ts +4 -2
  232. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  233. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  234. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  235. package/src/memory/activation-session-store.ts +43 -0
  236. package/src/memory/conversation-crud.ts +29 -19
  237. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  238. package/src/memory/db-init.ts +6 -0
  239. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  240. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  241. package/src/memory/jobs-worker.ts +25 -1
  242. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  243. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  244. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  245. package/src/memory/migrations/index.ts +3 -0
  246. package/src/memory/onboarding-events-store.ts +66 -18
  247. package/src/memory/schema/acp.ts +4 -0
  248. package/src/memory/schema/infrastructure.ts +13 -0
  249. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  250. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  251. package/src/memory/v2/consolidation-job.ts +2 -63
  252. package/src/memory/v2/types.ts +5 -0
  253. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  254. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  255. package/src/notifications/access-request-copy.ts +298 -0
  256. package/src/notifications/adapters/slack.ts +3 -3
  257. package/src/notifications/adapters/telegram.ts +9 -2
  258. package/src/notifications/copy-composer.ts +49 -267
  259. package/src/notifications/decision-engine.ts +16 -35
  260. package/src/notifications/home-feed-side-effect.ts +1 -6
  261. package/src/plugin-api/constants.ts +4 -0
  262. package/src/plugin-api/index.ts +40 -11
  263. package/src/plugin-api/types.ts +108 -0
  264. package/src/plugins/defaults/compaction/compact.ts +106 -0
  265. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
  266. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  267. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  268. package/src/plugins/defaults/compaction/package.json +1 -2
  269. package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
  270. package/src/plugins/defaults/empty-response/package.json +0 -1
  271. package/src/plugins/defaults/history-repair/package.json +0 -1
  272. package/src/plugins/defaults/index.ts +156 -26
  273. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  274. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
  275. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  276. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  277. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  278. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  282. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
  283. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
  284. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
  285. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  286. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
  287. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  288. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  289. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  290. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
  291. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
  293. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
  294. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
  295. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  296. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  297. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  298. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  299. package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
  300. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
  301. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
  302. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  303. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
  304. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  305. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
  306. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  307. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  308. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  309. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
  310. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
  311. package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
  312. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
  313. package/src/plugins/defaults/title-generate/package.json +0 -1
  314. package/src/plugins/defaults/tool-error/package.json +0 -1
  315. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  316. package/src/plugins/external-api.ts +12 -2
  317. package/src/plugins/pipeline.ts +6 -293
  318. package/src/plugins/registry.ts +9 -37
  319. package/src/plugins/types.ts +76 -381
  320. package/src/plugins/user-loader.ts +30 -127
  321. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  322. package/src/prompts/system-prompt.ts +61 -10
  323. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
  324. package/src/providers/anthropic/client.ts +9 -10
  325. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  326. package/src/providers/inference/logit-bias.ts +55 -0
  327. package/src/providers/model-catalog.ts +34 -0
  328. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  329. package/src/providers/openai/chat-completions-provider.ts +44 -1
  330. package/src/providers/retry.ts +22 -0
  331. package/src/providers/types.ts +6 -0
  332. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  333. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  334. package/src/runtime/agent-wake.ts +108 -209
  335. package/src/runtime/assistant-event-hub.ts +1 -1
  336. package/src/runtime/channel-approvals.ts +1 -1
  337. package/src/runtime/interactive-ui.ts +1 -1
  338. package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
  339. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  340. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  341. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  342. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  343. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  344. package/src/runtime/routes/acp-routes.test.ts +185 -32
  345. package/src/runtime/routes/acp-routes.ts +328 -30
  346. package/src/runtime/routes/app-management-routes.ts +3 -0
  347. package/src/runtime/routes/approval-routes.ts +1 -1
  348. package/src/runtime/routes/browser-routes.ts +1 -1
  349. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  350. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  351. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  352. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  353. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  354. package/src/runtime/routes/conversation-routes.ts +30 -2
  355. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  356. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  357. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  358. package/src/runtime/routes/host-cu-routes.ts +1 -1
  359. package/src/runtime/routes/identity-routes.ts +18 -3
  360. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  361. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  362. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  363. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  364. package/src/runtime/routes/memory-v3-routes.ts +65 -305
  365. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  366. package/src/runtime/routes/playground/helpers.ts +1 -1
  367. package/src/runtime/routes/plugins-routes.ts +337 -35
  368. package/src/runtime/routes/stt-routes.ts +45 -12
  369. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  370. package/src/runtime/routes/work-items-routes.ts +2 -4
  371. package/src/runtime/routes/workspace-routes.ts +50 -15
  372. package/src/runtime/services/conversation-serializer.ts +1 -1
  373. package/src/runtime/verification-outbound-actions.ts +147 -2
  374. package/src/runtime/verification-templates.ts +29 -3
  375. package/src/services/published-app-updater.ts +30 -8
  376. package/src/signals/cancel.ts +2 -4
  377. package/src/subagent/manager.ts +21 -5
  378. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  379. package/src/telemetry/activation-funnel.ts +167 -0
  380. package/src/telemetry/types.ts +13 -0
  381. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  382. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  383. package/src/tools/acp/context.ts +20 -0
  384. package/src/tools/acp/list-agents.test.ts +9 -19
  385. package/src/tools/acp/list-agents.ts +3 -15
  386. package/src/tools/acp/spawn.test.ts +173 -202
  387. package/src/tools/acp/spawn.ts +37 -172
  388. package/src/tools/acp/steer.test.ts +105 -8
  389. package/src/tools/acp/steer.ts +48 -17
  390. package/src/tools/apps/executors.ts +166 -50
  391. package/src/tools/browser/browser-execution.ts +12 -2
  392. package/src/tools/filesystem/write.ts +34 -0
  393. package/src/tools/network/web-fetch.ts +65 -24
  394. package/src/tools/skills/load.ts +1 -1
  395. package/src/tools/subagent/spawn.ts +2 -4
  396. package/src/tools/ui-surface/definitions.ts +32 -5
  397. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  398. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  399. package/docs/plugins.md +0 -832
  400. package/examples/plugins/echo/register.ts +0 -143
  401. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  402. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  403. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  404. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  405. package/src/__tests__/pipeline-runner.test.ts +0 -554
  406. package/src/daemon/wake-target-adapter.ts +0 -253
  407. package/src/memory/v3/__tests__/assign.test.ts +0 -242
  408. package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
  409. package/src/memory/v3/__tests__/core.test.ts +0 -39
  410. package/src/memory/v3/__tests__/health.test.ts +0 -219
  411. package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
  412. package/src/memory/v3/__tests__/needle.test.ts +0 -107
  413. package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
  414. package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
  415. package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
  416. package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
  417. package/src/memory/v3/__tests__/router.test.ts +0 -332
  418. package/src/memory/v3/__tests__/selector.test.ts +0 -470
  419. package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
  420. package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
  421. package/src/memory/v3/__tests__/tree.test.ts +0 -192
  422. package/src/memory/v3/assign.ts +0 -268
  423. package/src/memory/v3/capabilities.ts +0 -124
  424. package/src/memory/v3/core.ts +0 -26
  425. package/src/memory/v3/health.ts +0 -0
  426. package/src/memory/v3/maintain-job.ts +0 -314
  427. package/src/memory/v3/needle.ts +0 -115
  428. package/src/memory/v3/orchestrate.ts +0 -126
  429. package/src/memory/v3/page-content.ts +0 -34
  430. package/src/memory/v3/provider-blocks.ts +0 -26
  431. package/src/memory/v3/reconcile.ts +0 -523
  432. package/src/memory/v3/router.ts +0 -190
  433. package/src/memory/v3/selection-log-store.ts +0 -84
  434. package/src/memory/v3/selector.ts +0 -226
  435. package/src/memory/v3/shadow-plugin.ts +0 -411
  436. package/src/memory/v3/snapshot.ts +0 -209
  437. package/src/memory/v3/tree.ts +0 -174
  438. package/src/memory/v3/types.ts +0 -59
  439. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  440. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  441. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  442. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  443. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  444. package/src/plugins/defaults/compaction/register.ts +0 -35
  445. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  446. package/src/plugins/defaults/empty-response/register.ts +0 -23
  447. package/src/plugins/defaults/history-repair/register.ts +0 -24
  448. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
  449. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  450. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  451. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  452. package/src/plugins/defaults/title-generate/register.ts +0 -35
  453. package/src/plugins/defaults/tool-error/register.ts +0 -23
  454. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  455. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  456. package/src/proactive-artifact/decision.test.ts +0 -226
  457. package/src/proactive-artifact/decision.ts +0 -165
  458. package/src/proactive-artifact/index.ts +0 -7
  459. package/src/proactive-artifact/job.test.ts +0 -962
  460. package/src/proactive-artifact/job.ts +0 -372
  461. package/src/proactive-artifact/message-copy.ts +0 -58
  462. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  463. package/src/proactive-artifact/trigger-state.ts +0 -123
  464. package/src/util/map-limit.ts +0 -27
  465. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  466. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  467. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  468. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  469. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  470. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  471. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  472. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  473. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  474. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  475. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.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,381 @@
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: "1111111111111111111111111111111111111111",
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: "1111111111111111111111111111111111111111",
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: "2222222222222222222222222222222222222222",
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(
219
+ "2222222222222222222222222222222222222222",
220
+ );
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);
224
+ // AND the README from the pinned ref is surfaced
225
+ expect(details.readme).toContain("Caveman");
226
+ });
227
+
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
232
+ const fetch = makeFetch({
233
+ marketplace: {
234
+ name: "vellum",
235
+ plugins: [
236
+ {
237
+ name: "simple-memory",
238
+ source: {
239
+ source: "github",
240
+ repo: "example-org/simple-memory",
241
+ ref: "9999999999999999999999999999999999999999",
242
+ },
243
+ },
244
+ ],
245
+ },
246
+ listings: {
247
+ "example-org/simple-memory": [
248
+ fileEntry("README.md", "raw://ext/readme"),
249
+ ],
250
+ },
251
+ raw: { "raw://ext/readme": "# Simple Memory (external)" },
252
+ });
253
+
254
+ // WHEN we resolve the detail view
255
+ const details = await getPluginDetails(
256
+ { name: "simple-memory" },
257
+ { fetch, workspacePluginsDir: workspace },
258
+ );
259
+
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
262
+ expect(details.source).toEqual({
263
+ kind: "github",
264
+ repo: "example-org/simple-memory",
265
+ ref: "9999999999999999999999999999999999999999",
266
+ });
267
+ expect(details.readme).toContain("external");
268
+ });
269
+
270
+ test("prefers an installed copy's README and package.json over the repo", async () => {
271
+ // GIVEN an installed copy on disk with its own README + package.json
272
+ const target = join(workspace, "caveman");
273
+ mkdirSync(target, { recursive: true });
274
+ writeFileSync(join(target, "README.md"), "# Installed Caveman");
275
+ writeFileSync(
276
+ join(target, "package.json"),
277
+ JSON.stringify({ version: "2.0.0", license: "Apache-2.0" }),
278
+ );
279
+
280
+ // AND a marketplace entry + external repo that would otherwise be used
281
+ const fetch = makeFetch({
282
+ marketplace: {
283
+ name: "vellum",
284
+ plugins: [
285
+ {
286
+ name: "caveman",
287
+ source: {
288
+ source: "github",
289
+ repo: "example-org/caveman",
290
+ ref: "1111111111111111111111111111111111111111",
291
+ },
292
+ description: "manifest description",
293
+ },
294
+ ],
295
+ },
296
+ listings: {
297
+ "example-org/caveman": [fileEntry("README.md", "raw://caveman/readme")],
298
+ },
299
+ raw: { "raw://caveman/readme": "# Remote Caveman" },
300
+ });
301
+
302
+ // WHEN we resolve the detail view
303
+ const details = await getPluginDetails(
304
+ { name: "caveman" },
305
+ { fetch, workspacePluginsDir: workspace },
306
+ );
307
+
308
+ // THEN the installed README + version/license win; manifest fills the gap
309
+ expect(details.installed).toBe(true);
310
+ expect(details.readme).toBe("# Installed Caveman");
311
+ expect(details.version).toBe("2.0.0");
312
+ expect(details.license).toBe("Apache-2.0");
313
+ expect(details.description).toBe("manifest description");
314
+ });
315
+
316
+ test("throws PluginDetailsNotFoundError when nothing claims the name", async () => {
317
+ // GIVEN no install, no first-party directory, and an empty marketplace
318
+ const fetch = makeFetch({
319
+ marketplace: { name: "vellum", plugins: [] },
320
+ });
321
+
322
+ // WHEN / THEN resolving an unknown name rejects with the not-found error
323
+ await expect(
324
+ getPluginDetails(
325
+ { name: "ghost" },
326
+ { fetch, workspacePluginsDir: workspace },
327
+ ),
328
+ ).rejects.toBeInstanceOf(PluginDetailsNotFoundError);
329
+ });
330
+
331
+ test("degrades to manifest metadata when the repo listing fails", async () => {
332
+ // GIVEN a marketplace entry whose external repo listing errors out
333
+ const fetch = makeFetch({
334
+ marketplace: {
335
+ name: "vellum",
336
+ plugins: [
337
+ {
338
+ name: "caveman",
339
+ source: {
340
+ source: "github",
341
+ repo: "example-org/caveman",
342
+ ref: "1111111111111111111111111111111111111111",
343
+ },
344
+ description: "manifest description",
345
+ license: "MIT",
346
+ },
347
+ ],
348
+ },
349
+ failOn: ["example-org/caveman"],
350
+ });
351
+
352
+ // WHEN we resolve the detail view
353
+ const details = await getPluginDetails(
354
+ { name: "caveman", ref: "v1" },
355
+ { fetch, workspacePluginsDir: workspace },
356
+ );
357
+
358
+ // THEN it still renders manifest metadata with a null README rather than throwing
359
+ expect(details.readme).toBeNull();
360
+ expect(details.description).toBe("manifest description");
361
+ expect(details.license).toBe("MIT");
362
+ expect(details.source).toEqual({
363
+ kind: "github",
364
+ repo: "example-org/caveman",
365
+ ref: "1111111111111111111111111111111111111111",
366
+ });
367
+ });
368
+
369
+ test("rejects an invalid (path-traversal) plugin name", async () => {
370
+ // GIVEN a name that fails the install-name sanitizer
371
+ const fetch = makeFetch({ marketplace: { name: "vellum", plugins: [] } });
372
+
373
+ // WHEN / THEN resolution throws before any lookup
374
+ await expect(
375
+ getPluginDetails(
376
+ { name: "../escape" },
377
+ { fetch, workspacePluginsDir: workspace },
378
+ ),
379
+ ).rejects.toThrow();
380
+ });
381
+ });