@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62

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