@vellumai/assistant 0.9.0 → 0.10.0-dev.202606192226.8ed8a6a

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 (624) hide show
  1. package/ARCHITECTURE.md +19 -35
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +1128 -119
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/agent-loop-pushes-post-hook-prompt.test.ts +306 -0
  28. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  29. package/src/__tests__/app-compiler.test.ts +15 -1
  30. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  31. package/src/__tests__/approval-cascade.test.ts +6 -0
  32. package/src/__tests__/approval-routes-http.test.ts +128 -0
  33. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  34. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  35. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  36. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  37. package/src/__tests__/btw-routes.test.ts +6 -8
  38. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  39. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  40. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  41. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  42. package/src/__tests__/checker.test.ts +0 -3
  43. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  44. package/src/__tests__/compaction-events.test.ts +6 -0
  45. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  46. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  47. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  48. package/src/__tests__/config-schema.test.ts +35 -0
  49. package/src/__tests__/config-watcher.test.ts +0 -18
  50. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  51. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  52. package/src/__tests__/contacts-tools.test.ts +29 -0
  53. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  57. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  58. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -0
  59. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  60. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  61. package/src/__tests__/conversation-process-callsite.test.ts +6 -0
  62. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  63. package/src/__tests__/conversation-queue.test.ts +6 -0
  64. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  65. package/src/__tests__/conversation-slash-queue.test.ts +6 -0
  66. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  67. package/src/__tests__/conversation-speed-override.test.ts +6 -0
  68. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  69. package/src/__tests__/conversation-title-service.test.ts +62 -0
  70. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -0
  71. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  72. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  73. package/src/__tests__/credential-broker.test.ts +449 -1
  74. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  75. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  76. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  77. package/src/__tests__/credential-routes.test.ts +360 -0
  78. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  79. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  80. package/src/__tests__/disk-pressure-tools.test.ts +41 -1
  81. package/src/__tests__/disk-usage.test.ts +65 -0
  82. package/src/__tests__/document-tool-security.test.ts +9 -6
  83. package/src/__tests__/document-update-default-surface.test.ts +202 -0
  84. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  85. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  86. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  87. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  88. package/src/__tests__/gemini-count-tokens.test.ts +70 -0
  89. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  90. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  91. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  92. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  93. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  94. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  95. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  96. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  97. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  98. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  99. package/src/__tests__/identity-routes.test.ts +0 -189
  100. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  101. package/src/__tests__/injector-disk-pressure.test.ts +10 -5
  102. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  103. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  104. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  105. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  106. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  107. package/src/__tests__/llm-resolver.test.ts +70 -24
  108. package/src/__tests__/llm-schema.test.ts +1 -0
  109. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  110. package/src/__tests__/mcp-health-check.test.ts +6 -7
  111. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  112. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  113. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  114. package/src/__tests__/path-policy.test.ts +34 -0
  115. package/src/__tests__/persona-resolver.test.ts +49 -14
  116. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  117. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  118. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  119. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  120. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  121. package/src/__tests__/reaction-persistence.test.ts +150 -29
  122. package/src/__tests__/registry.test.ts +2 -7
  123. package/src/__tests__/relay-server.test.ts +285 -0
  124. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  125. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  126. package/src/__tests__/schedule-routes.test.ts +0 -30
  127. package/src/__tests__/schedule-tools.test.ts +2 -18
  128. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  129. package/src/__tests__/secret-response-routing.test.ts +35 -0
  130. package/src/__tests__/secret-routes-platform-proxy.test.ts +16 -2
  131. package/src/__tests__/skill-execute-input.test.ts +177 -1
  132. package/src/__tests__/skill-load-tool.test.ts +72 -0
  133. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  134. package/src/__tests__/skills.test.ts +51 -0
  135. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  136. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  137. package/src/__tests__/subagent-tools.test.ts +266 -0
  138. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  139. package/src/__tests__/system-storage-cleanup-skill.test.ts +56 -0
  140. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  141. package/src/__tests__/title-generate-hook.test.ts +100 -3
  142. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  143. package/src/__tests__/token-manager.test.ts +519 -0
  144. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  145. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  146. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  147. package/src/__tests__/tool-executor.test.ts +0 -79
  148. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  149. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  150. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  151. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  152. package/src/__tests__/twilio-routes.test.ts +81 -1
  153. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  154. package/src/__tests__/weak-open-model.test.ts +30 -0
  155. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  156. package/src/__tests__/workspace-greetings.test.ts +152 -0
  157. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  158. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  159. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  160. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  161. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  162. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  163. package/src/agent/loop.ts +75 -29
  164. package/src/api/README.md +6 -6
  165. package/src/api/events/tool-result.ts +6 -0
  166. package/src/api/events/workflow-completed.ts +53 -0
  167. package/src/api/events/workflow-leaf-finished.ts +38 -0
  168. package/src/api/events/workflow-leaf-started.ts +35 -0
  169. package/src/api/events/workflow-progress.ts +32 -0
  170. package/src/api/events/workflow-started.ts +31 -0
  171. package/src/api/index.ts +40 -0
  172. package/src/api/responses/conversation-message.ts +28 -4
  173. package/src/api/responses/home.ts +26 -4
  174. package/src/api/responses/workflow-journal.ts +53 -0
  175. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  176. package/src/approvals/guardian-decision-primitive.ts +26 -3
  177. package/src/approvals/guardian-request-resolvers.ts +183 -80
  178. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  179. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  180. package/src/calls/call-pointer-messages.ts +10 -4
  181. package/src/calls/channel-admission-reader.ts +104 -0
  182. package/src/calls/guardian-dispatch.ts +17 -45
  183. package/src/calls/media-stream-server.ts +84 -2
  184. package/src/calls/relay-access-wait.ts +1 -1
  185. package/src/calls/relay-server.ts +66 -0
  186. package/src/calls/relay-setup-router.ts +82 -1
  187. package/src/calls/twilio-routes.ts +17 -8
  188. package/src/calls/voice-session-bridge.ts +2 -2
  189. package/src/cli/commands/clients.ts +3 -0
  190. package/src/cli/commands/db/status.ts +37 -3
  191. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  192. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  193. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  194. package/src/cli/commands/memory/index.ts +30 -0
  195. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  196. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  197. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  198. package/src/cli/commands/oauth/status.test.ts +36 -0
  199. package/src/cli/commands/oauth/status.ts +23 -3
  200. package/src/cli/commands/plugins.ts +400 -14
  201. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  202. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  203. package/src/cli/lib/__tests__/install-from-github.test.ts +31 -0
  204. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  205. package/src/cli/lib/__tests__/plugin-pin-history.test.ts +162 -0
  206. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  207. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  208. package/src/cli/lib/diff-plugin.ts +346 -0
  209. package/src/cli/lib/inspect-plugin.ts +12 -1
  210. package/src/cli/lib/install-from-github.ts +128 -19
  211. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  212. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  213. package/src/cli/lib/plugin-marketplace.ts +11 -0
  214. package/src/cli/lib/plugin-pin-history.ts +257 -0
  215. package/src/cli/lib/plugin-surfaces.ts +104 -0
  216. package/src/cli/lib/upgrade-plugin.ts +298 -10
  217. package/src/cli/program.ts +2 -6
  218. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  219. package/src/config/assistant-feature-flags.ts +22 -7
  220. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  221. package/src/config/bundled-skills/document-editor/SKILL.md +3 -3
  222. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  223. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  224. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  225. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  226. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  227. package/src/config/bundled-skills/system-storage-cleanup/SKILL.md +74 -0
  228. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  229. package/src/config/bundled-tool-registry.ts +2 -7
  230. package/src/config/call-site-defaults.ts +15 -2
  231. package/src/config/feature-flag-registry.json +46 -31
  232. package/src/config/inference-profile-validation.ts +26 -0
  233. package/src/config/llm-resolver.ts +3 -0
  234. package/src/config/loader.ts +4 -0
  235. package/src/config/memory-v3-gate.ts +11 -0
  236. package/src/config/profile-order.ts +28 -0
  237. package/src/config/schema.ts +8 -6
  238. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  239. package/src/config/schemas/call-site-catalog.ts +7 -0
  240. package/src/config/schemas/channels.ts +11 -0
  241. package/src/config/schemas/elevenlabs.ts +0 -1
  242. package/src/config/schemas/llm.ts +31 -0
  243. package/src/config/schemas/memory-lifecycle.ts +3 -7
  244. package/src/config/schemas/memory-v3.ts +6 -0
  245. package/src/config/schemas/platform.ts +0 -8
  246. package/src/config/schemas/services.ts +18 -0
  247. package/src/config/seed-inference-profiles.ts +109 -44
  248. package/src/config/skills.ts +21 -0
  249. package/src/config/sync-gated-profiles.ts +220 -0
  250. package/src/contacts/contact-store.ts +89 -106
  251. package/src/contacts/contacts-write.ts +5 -22
  252. package/src/contacts/types.ts +0 -1
  253. package/src/context/compactor.ts +88 -54
  254. package/src/context/strip-injections.ts +58 -10
  255. package/src/context/token-estimator.ts +1 -1
  256. package/src/credential-execution/process-manager.ts +55 -14
  257. package/src/credential-execution/prompted-credential.ts +2 -3
  258. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  259. package/src/daemon/__tests__/conversation-tool-setup.test.ts +44 -0
  260. package/src/daemon/config-watcher.ts +0 -4
  261. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  262. package/src/daemon/conversation-agent-loop.ts +114 -22
  263. package/src/daemon/conversation-history.ts +1 -1
  264. package/src/daemon/conversation-lifecycle.ts +3 -5
  265. package/src/daemon/conversation-process.ts +13 -5
  266. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  267. package/src/daemon/conversation-slash.ts +2 -23
  268. package/src/daemon/conversation-surfaces.ts +26 -0
  269. package/src/daemon/conversation-tool-setup.ts +53 -19
  270. package/src/daemon/conversation.ts +131 -20
  271. package/src/daemon/disk-pressure-policy.ts +5 -3
  272. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  273. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  274. package/src/daemon/handlers/config-a2a.ts +0 -2
  275. package/src/daemon/handlers/config-channels.ts +15 -16
  276. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  277. package/src/daemon/handlers/conversations.ts +107 -0
  278. package/src/daemon/host-browser-proxy.ts +41 -0
  279. package/src/daemon/lifecycle.ts +55 -27
  280. package/src/daemon/memory-v2-startup.test.ts +131 -0
  281. package/src/daemon/memory-v2-startup.ts +100 -1
  282. package/src/daemon/message-provenance.ts +2 -0
  283. package/src/daemon/message-types/contacts.ts +0 -1
  284. package/src/daemon/message-types/conversations.ts +3 -3
  285. package/src/daemon/message-types/sync.ts +0 -1
  286. package/src/daemon/message-types/web-activity.ts +7 -1
  287. package/src/daemon/message-types/workflows.ts +83 -1
  288. package/src/daemon/orphan-reaper.test.ts +0 -19
  289. package/src/daemon/orphan-reaper.ts +2 -24
  290. package/src/daemon/server.ts +0 -10
  291. package/src/daemon/tool-setup-types.ts +4 -0
  292. package/src/daemon/trust-context.ts +1 -1
  293. package/src/daemon/wake-conversation-ops.ts +1 -0
  294. package/src/events/tool-audit-listener.ts +2 -2
  295. package/src/home/feed-source-enrichment.test.ts +151 -0
  296. package/src/home/feed-source-enrichment.ts +176 -0
  297. package/src/home/relationship-state.ts +2 -4
  298. package/src/instrument.ts +18 -6
  299. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  300. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  301. package/src/ipc/assistant-server.ts +37 -4
  302. package/src/ipc/gateway-flag-listener.ts +18 -2
  303. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  304. package/src/memory/__tests__/db-logs-attach.test.ts +122 -0
  305. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  306. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  307. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  308. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  309. package/src/memory/auth-fallback-events-store.ts +2 -2
  310. package/src/memory/auto-analysis-enqueue.ts +3 -5
  311. package/src/memory/bookmark-crud.ts +1 -2
  312. package/src/memory/canonical-guardian-store.ts +39 -1
  313. package/src/memory/conversation-crud.ts +9 -4
  314. package/src/memory/conversation-key-store.ts +17 -2
  315. package/src/memory/conversation-title-service.ts +64 -7
  316. package/src/memory/db-async-query.ts +20 -2
  317. package/src/memory/db-connection.ts +49 -5
  318. package/src/memory/db-init.ts +17 -17
  319. package/src/memory/embedding-backend.ts +38 -1
  320. package/src/memory/embedding-billing-breaker.ts +96 -0
  321. package/src/memory/jobs-store.ts +25 -13
  322. package/src/memory/jobs-worker.ts +54 -1
  323. package/src/memory/lifecycle-events-store.ts +2 -2
  324. package/src/memory/memory-retrospective-constants.ts +4 -4
  325. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  326. package/src/memory/memory-retrospective-job.ts +28 -227
  327. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  328. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  329. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  330. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  331. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  332. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  333. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  334. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  335. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  336. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  337. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  338. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  339. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  340. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  341. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  342. package/src/memory/migrations/index.ts +6 -0
  343. package/src/memory/migrations/run-migrations.ts +41 -0
  344. package/src/memory/migrations/validate-migration-state.ts +1 -1
  345. package/src/memory/onboarding-events-store.ts +3 -3
  346. package/src/memory/schema/contacts.ts +0 -5
  347. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  348. package/src/memory/skill-loaded-events-store.ts +2 -2
  349. package/src/memory/tool-executed-events-store.test.ts +7 -7
  350. package/src/memory/turn-trace-store.test.ts +736 -0
  351. package/src/memory/turn-trace-store.ts +364 -0
  352. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  353. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  354. package/src/memory/v2/consolidation-job.ts +2 -2
  355. package/src/memory/v2/skill-content.ts +25 -7
  356. package/src/memory/v2/skill-store.ts +7 -1
  357. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  358. package/src/memory/v3-eval/eval-packets.ts +546 -0
  359. package/src/messaging/providers/slack/adapter.ts +1 -1
  360. package/src/messaging/providers/slack/api.ts +31 -0
  361. package/src/messaging/providers/slack/send.test.ts +114 -2
  362. package/src/messaging/providers/slack/send.ts +30 -7
  363. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  364. package/src/messaging/providers/slack/withdraw.ts +161 -0
  365. package/src/notifications/AGENTS.md +2 -0
  366. package/src/notifications/access-request-copy.ts +72 -59
  367. package/src/notifications/adapters/shared.ts +29 -0
  368. package/src/notifications/adapters/slack.ts +58 -103
  369. package/src/notifications/adapters/telegram.ts +2 -20
  370. package/src/notifications/approval-card-data.ts +333 -0
  371. package/src/notifications/broadcaster.ts +16 -3
  372. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  373. package/src/notifications/copy-composer.ts +3 -3
  374. package/src/notifications/decision-engine.ts +4 -2
  375. package/src/notifications/destination-resolver.ts +4 -6
  376. package/src/notifications/guardian-question-mode.ts +10 -0
  377. package/src/notifications/home-feed-side-effect.ts +7 -16
  378. package/src/notifications/notification-utils.ts +19 -20
  379. package/src/notifications/signal.ts +79 -43
  380. package/src/notifications/types.ts +98 -121
  381. package/src/oauth/AGENTS.md +5 -24
  382. package/src/permissions/checker.test.ts +51 -0
  383. package/src/permissions/checker.ts +185 -26
  384. package/src/permissions/ipc-risk-types.ts +24 -0
  385. package/src/permissions/question-prompter.test.ts +27 -0
  386. package/src/permissions/question-prompter.ts +4 -0
  387. package/src/permissions/secret-prompter.ts +15 -1
  388. package/src/platform/client.test.ts +119 -0
  389. package/src/platform/client.ts +66 -0
  390. package/src/platform/consent-cache.test.ts +267 -0
  391. package/src/platform/consent-cache.ts +174 -0
  392. package/src/plugin-api/constants.ts +1 -1
  393. package/src/plugin-api/index.ts +33 -1
  394. package/src/plugin-api/model-profiles.ts +33 -0
  395. package/src/plugin-api/types.ts +50 -2
  396. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  397. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  398. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  399. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  400. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  401. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  402. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  403. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  404. package/src/plugins/defaults/advisor/config.ts +21 -0
  405. package/src/plugins/defaults/advisor/consult.ts +93 -0
  406. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  407. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  408. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  409. package/src/plugins/defaults/advisor/package.json +14 -0
  410. package/src/plugins/defaults/advisor/steering.ts +67 -0
  411. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  412. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  413. package/src/plugins/defaults/compaction/window-manager.ts +18 -0
  414. package/src/plugins/defaults/index.ts +60 -0
  415. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  416. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  417. package/src/plugins/defaults/memory-retrieval/injectors.ts +4 -4
  418. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  419. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  420. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  421. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  422. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  423. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  424. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  425. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  426. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  427. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  428. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  429. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  430. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  431. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  432. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  433. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  434. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  435. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  436. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  437. package/src/prompts/persona-resolver.ts +14 -4
  438. package/src/prompts/templates/system-sections.ts +7 -2
  439. package/src/providers/__tests__/count-tokens-forwarding.test.ts +98 -0
  440. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  441. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  442. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  443. package/src/providers/anthropic/client.ts +230 -175
  444. package/src/providers/atlascloud/client.ts +85 -0
  445. package/src/providers/call-site-routing.ts +10 -0
  446. package/src/providers/fetch-provider-catalog.ts +85 -0
  447. package/src/providers/gemini/client.ts +43 -0
  448. package/src/providers/inference/adapter-factory.ts +3 -0
  449. package/src/providers/model-catalog.ts +58 -0
  450. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  451. package/src/providers/openai/chat-completions-provider.ts +7 -0
  452. package/src/providers/openai/responses-provider.ts +10 -0
  453. package/src/providers/provider-send-message.ts +11 -3
  454. package/src/providers/ratelimit.ts +9 -0
  455. package/src/providers/retry.ts +62 -12
  456. package/src/providers/search-provider-catalog.ts +10 -0
  457. package/src/providers/types.ts +16 -0
  458. package/src/providers/usage-tracking.ts +7 -0
  459. package/src/providers/weak-open-model.ts +22 -0
  460. package/src/runtime/AGENTS.md +0 -1
  461. package/src/runtime/__tests__/agent-wake.test.ts +227 -0
  462. package/src/runtime/__tests__/client-health.test.ts +44 -0
  463. package/src/runtime/access-request-helper.ts +21 -53
  464. package/src/runtime/actor-trust-resolver.ts +59 -63
  465. package/src/runtime/agent-wake.ts +71 -0
  466. package/src/runtime/assistant-event-hub.ts +18 -4
  467. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  468. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  469. package/src/runtime/btw-sidechain.ts +3 -6
  470. package/src/runtime/capabilities.test.ts +120 -0
  471. package/src/runtime/capabilities.ts +197 -0
  472. package/src/runtime/channel-approval-types.ts +22 -45
  473. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  474. package/src/runtime/channel-retry-sweep.ts +1 -0
  475. package/src/runtime/channel-verification-service.ts +3 -3
  476. package/src/runtime/client-health.ts +26 -0
  477. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  478. package/src/runtime/effective-capabilities.test.ts +128 -0
  479. package/src/runtime/effective-capabilities.ts +84 -0
  480. package/src/runtime/guardian-reply-router.ts +106 -21
  481. package/src/runtime/invite-redemption-service.ts +9 -25
  482. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +126 -0
  483. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  484. package/src/runtime/pending-interactions.ts +35 -0
  485. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  486. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  487. package/src/runtime/routes/__tests__/plugins-routes.test.ts +383 -1
  488. package/src/runtime/routes/app-routes.ts +1 -1
  489. package/src/runtime/routes/approval-routes.ts +40 -9
  490. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  491. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  492. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  493. package/src/runtime/routes/btw-routes.ts +1 -27
  494. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  495. package/src/runtime/routes/client-routes.ts +10 -0
  496. package/src/runtime/routes/contact-routes.ts +31 -8
  497. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  498. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  499. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  500. package/src/runtime/routes/conversation-routes.ts +39 -14
  501. package/src/runtime/routes/credential-routes.ts +40 -16
  502. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  503. package/src/runtime/routes/events-routes.ts +1 -3
  504. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  505. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  506. package/src/runtime/routes/home-feed-routes.ts +8 -3
  507. package/src/runtime/routes/identity-routes.ts +1 -296
  508. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  509. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  510. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  511. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  512. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  513. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  514. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  515. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  516. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  517. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  518. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  519. package/src/runtime/routes/index.ts +2 -0
  520. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  521. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  522. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  523. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  524. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  525. package/src/runtime/routes/notification-routes.ts +122 -133
  526. package/src/runtime/routes/platform-routes.ts +2 -2
  527. package/src/runtime/routes/plugins-routes.ts +366 -11
  528. package/src/runtime/routes/schedule-routes.ts +0 -22
  529. package/src/runtime/routes/secret-routes.ts +15 -0
  530. package/src/runtime/routes/surface-action-routes.ts +2 -1
  531. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  532. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  533. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  534. package/src/runtime/routes/workflow-routes.ts +131 -29
  535. package/src/runtime/routes/workspace-greetings.ts +55 -0
  536. package/src/runtime/sync/resource-sync-events.ts +1 -11
  537. package/src/runtime/tool-grant-request-helper.ts +18 -16
  538. package/src/runtime/trust-context-resolver.ts +8 -5
  539. package/src/schedule/inference-profile.ts +2 -14
  540. package/src/schedule/schedule-store.ts +1 -1
  541. package/src/schedule/scheduler-types.ts +5 -1
  542. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  543. package/src/security/secret-patterns.ts +3 -0
  544. package/src/subagent/manager.ts +17 -4
  545. package/src/subagent/types.ts +6 -0
  546. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  547. package/src/telemetry/trace-collection-policy.ts +30 -0
  548. package/src/telemetry/types.ts +89 -0
  549. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  550. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  551. package/src/tools/AGENTS.md +3 -3
  552. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  553. package/src/tools/browser/browser-execution.ts +30 -19
  554. package/src/tools/document/document-tool.ts +30 -6
  555. package/src/tools/executor.ts +5 -3
  556. package/src/tools/host-terminal/host-shell.ts +5 -4
  557. package/src/tools/memory/register.ts +2 -2
  558. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  559. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  560. package/src/tools/network/web-fetch.ts +372 -1
  561. package/src/tools/network/web-search-error.ts +1 -1
  562. package/src/tools/network/web-search.ts +213 -10
  563. package/src/tools/permission-checker.ts +4 -3
  564. package/src/tools/registry.ts +20 -0
  565. package/src/tools/schedule/create.ts +7 -12
  566. package/src/tools/schedule/update.ts +4 -11
  567. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  568. package/src/tools/side-effects.ts +2 -17
  569. package/src/tools/skills/execute.ts +99 -2
  570. package/src/tools/skills/load.ts +43 -2
  571. package/src/tools/subagent/spawn.ts +64 -15
  572. package/src/tools/terminal/shell.ts +10 -4
  573. package/src/tools/tool-approval-handler.ts +18 -13
  574. package/src/tools/tool-manifest.ts +0 -2
  575. package/src/tools/types.ts +10 -0
  576. package/src/tools/ui-surface/definitions.ts +64 -3
  577. package/src/tools/verification-control-plane-policy.ts +3 -1
  578. package/src/tools/workflows/run-workflow.test.ts +8 -18
  579. package/src/tools/workflows/run-workflow.ts +1 -0
  580. package/src/util/disk-usage.ts +78 -23
  581. package/src/util/logs-db-path.ts +22 -0
  582. package/src/util/platform.ts +10 -3
  583. package/src/watcher/telemetry.ts +2 -2
  584. package/src/workflows/capabilities.ts +2 -3
  585. package/src/workflows/engine.test.ts +175 -1
  586. package/src/workflows/engine.ts +82 -0
  587. package/src/workflows/journal-store.test.ts +70 -0
  588. package/src/workflows/journal-store.ts +18 -3
  589. package/src/workflows/run-manager.test.ts +171 -28
  590. package/src/workflows/run-manager.ts +66 -24
  591. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  592. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  593. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  594. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  595. package/src/workspace/migrations/registry.ts +8 -0
  596. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  597. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  598. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  599. package/src/__tests__/credential-vault.test.ts +0 -1706
  600. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  601. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  602. package/src/cli/commands/__tests__/task.test.ts +0 -914
  603. package/src/cli/commands/task.ts +0 -771
  604. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  605. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  606. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  607. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  608. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  609. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  610. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  611. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  612. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  613. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  614. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  615. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  616. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  617. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  618. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  619. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  620. package/src/memory/preloaded-apps.ts +0 -116
  621. package/src/notifications/tool-approval-copy.ts +0 -142
  622. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  623. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  624. package/src/tools/credentials/vault.ts +0 -712
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Three-way merge of a plugin tree, used by `plugins upgrade --strategy` to
3
+ * carry local edits forward across an upgrade instead of discarding them.
4
+ *
5
+ * An upgrade has three inputs, exactly like a git merge:
6
+ * - **base** — the tree the plugin was installed at (the recorded commit,
7
+ * re-materialized through the install pipeline; see {@link ./diff-plugin}).
8
+ * - **ours** — the current on-disk install, carrying any local edits.
9
+ * - **theirs** — the marketplace's current pin, the tree being upgraded to.
10
+ *
11
+ * Per file the merge is delegated to `git merge-file`, the same line-level
12
+ * three-way algorithm git itself uses: a hunk edited on only one side is taken
13
+ * from that side, so non-conflicting edits from *both* sides survive. Only a
14
+ * hunk edited differently on both sides is a true conflict, resolved per the
15
+ * caller's strategy:
16
+ * - `ours` keeps the local hunk (`git merge-file --ours`),
17
+ * - `theirs` keeps the pinned hunk (`git merge-file --theirs`),
18
+ * - `assistant` writes standard git conflict markers into the file (no resolve
19
+ * flag) and reports the path, leaving the conflict for the assistant to
20
+ * resolve — exactly the mid-merge working-tree UX.
21
+ *
22
+ * File-level add/delete divergence (a file added, removed, or modified on only
23
+ * one side) is resolved here before any line merge, since `git merge-file`
24
+ * operates on three existing blobs.
25
+ *
26
+ * Binary files cannot be line-merged, so a binary file that diverged on both
27
+ * sides is resolved whole-file by the strategy (`ours`/`theirs`); under
28
+ * `assistant` the local copy is kept and the path is reported as a binary
29
+ * conflict, since markers cannot be written into binary content.
30
+ *
31
+ * The `overwrite` strategy never reaches here — it discards local edits and is
32
+ * a plain re-install at the pin.
33
+ */
34
+
35
+ import { execFile } from "node:child_process";
36
+ import {
37
+ mkdirSync,
38
+ mkdtempSync,
39
+ readFileSync,
40
+ rmSync,
41
+ writeFileSync,
42
+ } from "node:fs";
43
+ import { tmpdir } from "node:os";
44
+ import { dirname, join } from "node:path";
45
+ import { promisify } from "node:util";
46
+
47
+ import { INSTALL_META_FILENAME } from "./install-from-github.js";
48
+ import { computeFingerprint } from "./plugin-fingerprint.js";
49
+
50
+ const execFileAsync = promisify(execFile);
51
+
52
+ /** Cap on a single `git merge-file`; a per-file line merge is near-instant. */
53
+ const MERGE_TIMEOUT_MS = 30_000;
54
+
55
+ /**
56
+ * How hunks edited differently on both sides are reconciled. `ours`/`theirs`
57
+ * auto-resolve toward that side; `assistant` writes conflict markers and
58
+ * reports the conflict for later resolution. `overwrite` is not a merge
59
+ * strategy — the caller re-installs the pin wholesale instead of merging.
60
+ */
61
+ export type PluginMergeStrategy = "ours" | "theirs" | "assistant";
62
+
63
+ /** Human-readable labels for the conflict markers `assistant` writes. */
64
+ export interface ConflictLabels {
65
+ readonly ours: string;
66
+ readonly base: string;
67
+ readonly theirs: string;
68
+ }
69
+
70
+ const DEFAULT_CONFLICT_LABELS: ConflictLabels = {
71
+ ours: "local edits",
72
+ base: "install baseline",
73
+ theirs: "upgrade pin",
74
+ };
75
+
76
+ /** Inputs for a three-way plugin-tree merge. */
77
+ export interface MergePluginTreeOptions {
78
+ /** Re-materialized install-commit tree (the merge base). */
79
+ readonly baseDir: string;
80
+ /** Current on-disk install, carrying local edits (`ours`). */
81
+ readonly oursDir: string;
82
+ /** Marketplace-pinned tree being upgraded to (`theirs`). */
83
+ readonly theirsDir: string;
84
+ /** Empty directory the merged tree is written into. */
85
+ readonly destDir: string;
86
+ /** How to resolve hunks edited differently on both sides. */
87
+ readonly strategy: PluginMergeStrategy;
88
+ /** Marker labels for the `assistant` strategy. Defaults are used when omitted. */
89
+ readonly conflictLabels?: ConflictLabels;
90
+ }
91
+
92
+ /** Outcome of a three-way plugin-tree merge. */
93
+ export interface PluginMergeResult {
94
+ /** Number of files written into the destination tree. */
95
+ readonly fileCount: number;
96
+ /**
97
+ * Paths (relative to the tree root) left for the assistant to resolve.
98
+ * Text files carry git conflict markers; modify/delete divergences keep the
99
+ * surviving content. Empty for `ours`/`theirs`, which auto-resolve.
100
+ */
101
+ readonly conflicts: readonly string[];
102
+ /**
103
+ * Paths of binary files that diverged on both sides. The local copy is kept
104
+ * (markers cannot be written into binary content), so the assistant must
105
+ * choose a version rather than edit markers. Empty for `ours`/`theirs`.
106
+ */
107
+ readonly binaryConflicts: readonly string[];
108
+ }
109
+
110
+ /** Result of merging a single file. */
111
+ interface MergedFile {
112
+ readonly content: Buffer;
113
+ /** Conflict markers were written into `content` (text, `assistant` only). */
114
+ readonly conflicted: boolean;
115
+ /** A binary file conflicted; `content` is the kept local copy (`assistant`). */
116
+ readonly binaryConflicted: boolean;
117
+ }
118
+
119
+ /** A NUL byte in the leading bytes is the heuristic git uses to flag a blob as binary. */
120
+ function isBinary(buf: Buffer): boolean {
121
+ const len = Math.min(buf.length, 8000);
122
+ for (let i = 0; i < len; i++) {
123
+ if (buf[i] === 0) return true;
124
+ }
125
+ return false;
126
+ }
127
+
128
+ /** Write `content` to `destDir/rel`, creating parent directories as needed. */
129
+ function writeInto(destDir: string, rel: string, content: Buffer): void {
130
+ const abs = join(destDir, rel);
131
+ mkdirSync(dirname(abs), { recursive: true });
132
+ writeFileSync(abs, content);
133
+ }
134
+
135
+ /**
136
+ * Line-merge three blobs with `git merge-file`. Non-conflicting hunks from both
137
+ * sides are always kept. `ours`/`theirs` auto-resolve conflicting hunks toward
138
+ * that side (no markers); `assistant` writes git conflict markers and flags the
139
+ * file as conflicted.
140
+ *
141
+ * Binary input cannot be line-merged: a side that matches the base did not
142
+ * change, so the other side's edit is taken. A binary blob changed on *both*
143
+ * sides is a true conflict — resolved whole-file by `ours`/`theirs`, or kept as
144
+ * the local copy and flagged under `assistant` (no markers possible).
145
+ */
146
+ async function threeWayMergeFile(
147
+ ours: Buffer,
148
+ base: Buffer,
149
+ theirs: Buffer,
150
+ strategy: PluginMergeStrategy,
151
+ labels: ConflictLabels,
152
+ ): Promise<MergedFile> {
153
+ if (isBinary(ours) || isBinary(base) || isBinary(theirs)) {
154
+ if (ours.equals(base))
155
+ return { content: theirs, conflicted: false, binaryConflicted: false };
156
+ if (theirs.equals(base))
157
+ return { content: ours, conflicted: false, binaryConflicted: false };
158
+ if (strategy === "assistant") {
159
+ return { content: ours, conflicted: false, binaryConflicted: true };
160
+ }
161
+ return {
162
+ content: strategy === "ours" ? ours : theirs,
163
+ conflicted: false,
164
+ binaryConflicted: false,
165
+ };
166
+ }
167
+
168
+ const scratch = mkdtempSync(join(tmpdir(), "plugin-merge-file-"));
169
+ try {
170
+ const oursPath = join(scratch, "ours");
171
+ const basePath = join(scratch, "base");
172
+ const theirsPath = join(scratch, "theirs");
173
+ writeFileSync(oursPath, ours);
174
+ writeFileSync(basePath, base);
175
+ writeFileSync(theirsPath, theirs);
176
+
177
+ // `-p` prints the merged result to stdout instead of editing `ours` in
178
+ // place. `--ours`/`--theirs` auto-resolve conflicting hunks toward that
179
+ // side; with neither (the `assistant` strategy) git writes conflict
180
+ // markers, labelled via `-L` so the assistant can tell the sides apart.
181
+ const args = ["merge-file", "-p"];
182
+ if (strategy === "ours" || strategy === "theirs") {
183
+ args.push(`--${strategy}`);
184
+ } else {
185
+ args.push("-L", labels.ours, "-L", labels.base, "-L", labels.theirs);
186
+ }
187
+ args.push(oursPath, basePath, theirsPath);
188
+ try {
189
+ const { stdout } = await execFileAsync("git", args, {
190
+ cwd: scratch,
191
+ encoding: "buffer",
192
+ timeout: MERGE_TIMEOUT_MS,
193
+ maxBuffer: 64 * 1024 * 1024,
194
+ });
195
+ return { content: stdout, conflicted: false, binaryConflicted: false };
196
+ } catch (err) {
197
+ // `git merge-file` exits with the (positive) conflict count when markers
198
+ // were left, still printing the full merged bytes to stdout — that is the
199
+ // only rejection we may install. A killed process (timeout) or a
200
+ // `maxBuffer` overflow also rejects here, but with truncated/partial
201
+ // stdout we must NOT install it, or a large conflicted file would be
202
+ // silently corrupted; surface those as errors. A resolving flag never
203
+ // leaves conflicts, so any non-zero exit there is likewise a real
204
+ // failure.
205
+ const e = err as {
206
+ stdout?: Buffer;
207
+ code?: unknown;
208
+ killed?: boolean;
209
+ signal?: string | null;
210
+ };
211
+ const isConflictExit =
212
+ strategy === "assistant" &&
213
+ e.killed !== true &&
214
+ e.signal == null &&
215
+ typeof e.code === "number" &&
216
+ e.code > 0;
217
+ if (isConflictExit && Buffer.isBuffer(e.stdout)) {
218
+ return { content: e.stdout, conflicted: true, binaryConflicted: false };
219
+ }
220
+ throw err;
221
+ }
222
+ } finally {
223
+ rmSync(scratch, { recursive: true, force: true });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Three-way merge `oursDir`/`theirsDir` against `baseDir` into `destDir` per
229
+ * `strategy`. The provenance sidecar is excluded on every side — it is
230
+ * rewritten by the caller after the swap and must not be carried through the
231
+ * merge.
232
+ *
233
+ * Returns the file count plus, for the `assistant` strategy, the paths left for
234
+ * the assistant to resolve (text files with conflict markers and modify/delete
235
+ * divergences) and the binary files that conflicted.
236
+ */
237
+ export async function mergePluginTree({
238
+ baseDir,
239
+ oursDir,
240
+ theirsDir,
241
+ destDir,
242
+ strategy,
243
+ conflictLabels,
244
+ }: MergePluginTreeOptions): Promise<PluginMergeResult> {
245
+ const labels = conflictLabels ?? DEFAULT_CONFLICT_LABELS;
246
+ const exclude = [INSTALL_META_FILENAME];
247
+ const base = computeFingerprint(baseDir, exclude).files;
248
+ const ours = computeFingerprint(oursDir, exclude).files;
249
+ const theirs = computeFingerprint(theirsDir, exclude).files;
250
+
251
+ const paths = new Set([
252
+ ...Object.keys(base),
253
+ ...Object.keys(ours),
254
+ ...Object.keys(theirs),
255
+ ]);
256
+
257
+ const readBase = (rel: string) => readFileSync(join(baseDir, rel));
258
+ const readOurs = (rel: string) => readFileSync(join(oursDir, rel));
259
+ const readTheirs = (rel: string) => readFileSync(join(theirsDir, rel));
260
+
261
+ let fileCount = 0;
262
+ const conflicts: string[] = [];
263
+ const binaryConflicts: string[] = [];
264
+ const keep = (rel: string, content: Buffer): void => {
265
+ writeInto(destDir, rel, content);
266
+ fileCount++;
267
+ };
268
+
269
+ for (const rel of paths) {
270
+ const b = base[rel];
271
+ const o = ours[rel];
272
+ const t = theirs[rel];
273
+
274
+ if (o !== undefined && t !== undefined) {
275
+ // Present on both sides: identical needs no merge; otherwise line-merge
276
+ // (an empty base when the file was added on both sides).
277
+ if (o === t) {
278
+ keep(rel, readOurs(rel));
279
+ } else {
280
+ const merged = await threeWayMergeFile(
281
+ readOurs(rel),
282
+ b !== undefined ? readBase(rel) : Buffer.alloc(0),
283
+ readTheirs(rel),
284
+ strategy,
285
+ labels,
286
+ );
287
+ keep(rel, merged.content);
288
+ if (merged.conflicted) conflicts.push(rel);
289
+ if (merged.binaryConflicted) binaryConflicts.push(rel);
290
+ }
291
+ continue;
292
+ }
293
+
294
+ if (o !== undefined) {
295
+ // Present only locally. A local-only addition (no base) always survives.
296
+ // A file deleted upstream is a delete: drop it when unchanged locally.
297
+ // On a modify/delete conflict `ours` keeps the edit and `theirs` honors
298
+ // the deletion; `assistant` keeps the edit and flags it (a whole-file
299
+ // conflict markers can't express).
300
+ if (b === undefined || (b !== o && strategy !== "theirs")) {
301
+ keep(rel, readOurs(rel));
302
+ if (b !== undefined && b !== o && strategy === "assistant") {
303
+ conflicts.push(rel);
304
+ }
305
+ }
306
+ continue;
307
+ }
308
+
309
+ if (t !== undefined) {
310
+ // Present only at the pin. A remote-only addition (no base) always lands.
311
+ // A file deleted locally is a delete: keep upstream's removal when the
312
+ // pin left it unchanged. On a delete/modify conflict `theirs` keeps the
313
+ // pin's edit and `ours` honors the local deletion; `assistant` keeps the
314
+ // pin's edit and flags it.
315
+ if (b === undefined || (b !== t && strategy !== "ours")) {
316
+ keep(rel, readTheirs(rel));
317
+ if (b !== undefined && b !== t && strategy === "assistant") {
318
+ conflicts.push(rel);
319
+ }
320
+ }
321
+ continue;
322
+ }
323
+
324
+ // Present only in the base: removed on both sides, so it stays removed.
325
+ }
326
+
327
+ return { fileCount, conflicts, binaryConflicts };
328
+ }
@@ -120,6 +120,20 @@ export function compareFingerprint(
120
120
  };
121
121
  }
122
122
 
123
+ /**
124
+ * Whether two fingerprints cover the same files with identical digests. Used to
125
+ * confirm a re-materialized tree faithfully reproduces a recorded baseline
126
+ * before it is trusted as a merge base.
127
+ */
128
+ export function fingerprintsEqual(a: Fingerprint, b: Fingerprint): boolean {
129
+ const aKeys = Object.keys(a.files);
130
+ if (aKeys.length !== Object.keys(b.files).length) return false;
131
+ for (const key of aKeys) {
132
+ if (a.files[key] !== b.files[key]) return false;
133
+ }
134
+ return true;
135
+ }
136
+
123
137
  /**
124
138
  * Parse a fingerprint from already-decoded JSON. Lenient by design — any shape
125
139
  * problem yields `null` so an older or hand-edited sidecar simply reports "no
@@ -35,6 +35,17 @@ const MARKETPLACE_SOURCE_OWNER = "vellum-ai";
35
35
  const MARKETPLACE_SOURCE_REPO = "vellum-assistant";
36
36
  const MARKETPLACE_FILE_PATH = "plugins/marketplace.json";
37
37
 
38
+ /**
39
+ * Canonical GitHub coordinates of the marketplace manifest, exported so
40
+ * sibling readers (e.g. {@link ./plugin-pin-history}) resolve the same
41
+ * `owner/repo:path` rather than re-declaring it.
42
+ */
43
+ export const MARKETPLACE_MANIFEST_LOCATION = {
44
+ owner: MARKETPLACE_SOURCE_OWNER,
45
+ repo: MARKETPLACE_SOURCE_REPO,
46
+ path: MARKETPLACE_FILE_PATH,
47
+ } as const;
48
+
38
49
  // ---------------------------------------------------------------------------
39
50
  // Manifest schema (subset of the Claude Code marketplace schema)
40
51
  // ---------------------------------------------------------------------------
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Reconstruct the history of marketplace pins for a single plugin.
3
+ *
4
+ * The curated `plugins/marketplace.json` manifest (see {@link ./plugin-marketplace})
5
+ * records only the *current* pin for each plugin — there is no stored version
6
+ * list. But the manifest is a file in a Git repo, and every time a curator bumps
7
+ * a plugin's pin, that is a commit to the manifest. So the history of reviewed
8
+ * pins already exists as the commit history of that one file: this module walks
9
+ * it (newest → oldest) via the GitHub API and reports the distinct pins a plugin
10
+ * has been promoted to over time, each tagged with the marketplace commit an
11
+ * install can reproduce it from.
12
+ *
13
+ * Why this is the safe source for "install an older version": every commit on
14
+ * the default branch was reviewed and merged, so a pin drawn from this history
15
+ * is a reviewed, known-good revision — unlike an arbitrary caller-supplied ref,
16
+ * which the install route deliberately refuses. Resolving an older pin to the
17
+ * marketplace commit that introduced it (rather than the plugin SHA directly)
18
+ * also means the install reads that era's manifest, so the curated adapter stub
19
+ * matches the pin and the historical version reproduces coherently.
20
+ *
21
+ * Designed for direct programmatic use with an injected `fetch`, mirroring the
22
+ * sibling plugin libraries.
23
+ */
24
+
25
+ import { type FetchLike, sanitizePluginName } from "./install-from-github.js";
26
+ import {
27
+ fetchMarketplaceEntries,
28
+ MARKETPLACE_MANIFEST_LOCATION,
29
+ } from "./plugin-marketplace.js";
30
+
31
+ /** Default number of distinct historical pins surfaced by `plugins versions`. */
32
+ export const DEFAULT_PIN_HISTORY_LIMIT = 5;
33
+
34
+ /**
35
+ * Cap on how many manifest-touching commits are inspected in one walk. Bounds
36
+ * the GitHub API cost: a plugin whose pin changes rarely could otherwise force
37
+ * a fetch of the manifest at every commit that touched it for *any* plugin.
38
+ * Well above {@link DEFAULT_PIN_HISTORY_LIMIT} so the default listing is
39
+ * complete in all but pathological histories.
40
+ */
41
+ const MAX_MARKETPLACE_COMMITS_SCANNED = 100;
42
+
43
+ /** A single point in a plugin's marketplace-pin history. */
44
+ export interface PluginPinHistoryEntry {
45
+ /** Plugin commit SHA pinned at this point in history. */
46
+ readonly pin: string;
47
+ /**
48
+ * Marketplace-manifest commit to install this pin from — the newest commit
49
+ * that carries it. Installing with this as the marketplace ref reproduces the
50
+ * pin together with the curated adapter stub of the same era.
51
+ */
52
+ readonly marketplaceCommit: string;
53
+ /**
54
+ * ISO-8601 committer date (UTC) of {@link PluginPinHistoryEntry.marketplaceCommit},
55
+ * i.e. when this pin was promoted; `null` when the date could not be read.
56
+ */
57
+ readonly promotedAt: string | null;
58
+ /** True for the pin currently active on the default branch. */
59
+ readonly current: boolean;
60
+ }
61
+
62
+ /** Options controlling the pin-history walk. */
63
+ export interface ListPinHistoryOptions {
64
+ /** Max distinct pins to return (newest first). Defaults to {@link DEFAULT_PIN_HISTORY_LIMIT}. */
65
+ readonly limit?: number;
66
+ /** Marketplace branch to read history from. Defaults to `main`. */
67
+ readonly ref?: string;
68
+ }
69
+
70
+ /** Dependencies injected by the caller. */
71
+ export interface ListPinHistoryDeps {
72
+ /** HTTP client. Production callers pass `globalThis.fetch.bind(globalThis)`. */
73
+ readonly fetch: FetchLike;
74
+ }
75
+
76
+ /** Shape of a commit entry from the GitHub commits API. */
77
+ interface GitHubCommitListEntry {
78
+ readonly sha: string;
79
+ readonly commit: {
80
+ readonly committer?: { readonly date?: string } | null;
81
+ } | null;
82
+ }
83
+
84
+ /** A manifest-touching commit and its committer date. */
85
+ interface ManifestCommit {
86
+ readonly sha: string;
87
+ readonly date: string | null;
88
+ }
89
+
90
+ /** The pin-history walk could not read the marketplace commit list. */
91
+ export class PluginPinHistoryError extends Error {
92
+ constructor(message: string) {
93
+ super(message);
94
+ this.name = "PluginPinHistoryError";
95
+ }
96
+ }
97
+
98
+ /**
99
+ * List the commits that touched the marketplace manifest on `ref`, newest
100
+ * first, capped at {@link MAX_MARKETPLACE_COMMITS_SCANNED}. A 404 (the file
101
+ * never existed at this ref) yields an empty list.
102
+ */
103
+ async function listManifestCommits(
104
+ ref: string,
105
+ fetchFn: FetchLike,
106
+ ): Promise<ManifestCommit[]> {
107
+ const { owner, repo, path } = MARKETPLACE_MANIFEST_LOCATION;
108
+ const url =
109
+ `https://api.github.com/repos/${owner}/${repo}/commits` +
110
+ `?path=${encodeURIComponent(path)}` +
111
+ `&sha=${encodeURIComponent(ref)}` +
112
+ `&per_page=${MAX_MARKETPLACE_COMMITS_SCANNED}`;
113
+
114
+ const res = await fetchFn(url, {
115
+ headers: {
116
+ Accept: "application/vnd.github+json",
117
+ "User-Agent": "vellum-assistant-cli",
118
+ },
119
+ });
120
+ if (res.status === 404) return [];
121
+ if (!res.ok) {
122
+ throw new PluginPinHistoryError(
123
+ `Marketplace commit history fetch failed for ${path} @ ${ref}: HTTP ${res.status}`,
124
+ );
125
+ }
126
+
127
+ let body: unknown;
128
+ try {
129
+ body = JSON.parse(await res.text());
130
+ } catch (err) {
131
+ throw new PluginPinHistoryError(
132
+ `Marketplace commit history is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
133
+ );
134
+ }
135
+ if (!Array.isArray(body)) {
136
+ throw new PluginPinHistoryError(
137
+ "Marketplace commit history response was not an array",
138
+ );
139
+ }
140
+
141
+ const commits: ManifestCommit[] = [];
142
+ for (const item of body as GitHubCommitListEntry[]) {
143
+ if (typeof item?.sha !== "string") continue;
144
+ const rawDate = item.commit?.committer?.date;
145
+ commits.push({
146
+ sha: item.sha,
147
+ date: typeof rawDate === "string" ? rawDate : null,
148
+ });
149
+ }
150
+ return commits;
151
+ }
152
+
153
+ /**
154
+ * Resolve the plugin's pinned SHA in the manifest at a specific marketplace
155
+ * commit, or `null` when no entry claims the name at that revision.
156
+ */
157
+ async function pinAtCommit(
158
+ name: string,
159
+ marketplaceCommit: string,
160
+ fetchFn: FetchLike,
161
+ ): Promise<string | null> {
162
+ const entries = await fetchMarketplaceEntries(
163
+ { fetch: fetchFn },
164
+ { ref: marketplaceCommit },
165
+ );
166
+ return entries.find((e) => e.name === name)?.source.ref ?? null;
167
+ }
168
+
169
+ /**
170
+ * Walk a plugin's distinct marketplace pins, newest first. Each pin is yielded
171
+ * once, tagged with the newest marketplace commit carrying it. Revisions where
172
+ * the plugin is absent from the manifest are skipped (a gap, not a pin). The
173
+ * `current` flag is set against the pin live on `ref` today.
174
+ *
175
+ * Bounded by {@link MAX_MARKETPLACE_COMMITS_SCANNED} commits scanned.
176
+ */
177
+ async function* iteratePinHistory(
178
+ name: string,
179
+ ref: string,
180
+ fetchFn: FetchLike,
181
+ ): AsyncGenerator<PluginPinHistoryEntry> {
182
+ const commits = await listManifestCommits(ref, fetchFn);
183
+ if (commits.length === 0) return;
184
+
185
+ // The pin live on the branch today, used to flag the current entry. Reading
186
+ // it explicitly (rather than assuming the newest commit carries it) stays
187
+ // correct even if the plugin was just removed from the manifest at the tip.
188
+ const currentPin = await pinAtCommit(name, ref, fetchFn);
189
+
190
+ let lastPin: string | null = null;
191
+ for (const commit of commits) {
192
+ const pin = await pinAtCommit(name, commit.sha, fetchFn);
193
+ if (pin === null) continue;
194
+ if (pin === lastPin) continue;
195
+ lastPin = pin;
196
+ yield {
197
+ pin,
198
+ marketplaceCommit: commit.sha,
199
+ promotedAt: commit.date,
200
+ current: currentPin !== null && pin === currentPin,
201
+ };
202
+ }
203
+ }
204
+
205
+ /**
206
+ * List a plugin's distinct marketplace pins, newest first, capped at
207
+ * `limit` (default {@link DEFAULT_PIN_HISTORY_LIMIT}). The first entry is the
208
+ * current pin. An empty list means the plugin has no resolvable history at this
209
+ * ref (never in the manifest, or the manifest does not exist).
210
+ *
211
+ * Throws {@link PluginPinHistoryError} when the commit list cannot be read, and
212
+ * propagates a `MarketplaceFetchError` if a manifest revision fails to parse.
213
+ */
214
+ export async function listPinHistory(
215
+ name: string,
216
+ deps: ListPinHistoryDeps,
217
+ opts: ListPinHistoryOptions = {},
218
+ ): Promise<PluginPinHistoryEntry[]> {
219
+ const sanitized = sanitizePluginName(name);
220
+ const ref = opts.ref ?? "main";
221
+ const limit = opts.limit ?? DEFAULT_PIN_HISTORY_LIMIT;
222
+
223
+ const out: PluginPinHistoryEntry[] = [];
224
+ for await (const entry of iteratePinHistory(sanitized, ref, deps.fetch)) {
225
+ out.push(entry);
226
+ if (out.length >= limit) break;
227
+ }
228
+ return out;
229
+ }
230
+
231
+ /**
232
+ * Resolve a plugin pin (a full commit SHA) to the marketplace commit it should
233
+ * be installed from, by searching the plugin's pin history. Returns `null` when
234
+ * the pin is not found in the scanned history — i.e. it is not a reviewed pin
235
+ * this plugin was ever promoted to (or it predates the scan window).
236
+ *
237
+ * Case-insensitive on the SHA, matching the rest of the install pipeline. The
238
+ * search walks the full scan window, not just the `limit` shown by
239
+ * {@link listPinHistory}, so a pin older than the default listing still
240
+ * resolves.
241
+ */
242
+ export async function resolvePinToMarketplaceCommit(
243
+ name: string,
244
+ pin: string,
245
+ deps: ListPinHistoryDeps,
246
+ opts: { readonly ref?: string } = {},
247
+ ): Promise<PluginPinHistoryEntry | null> {
248
+ const sanitized = sanitizePluginName(name);
249
+ const ref = opts.ref ?? "main";
250
+ const target = pin.trim().toLowerCase();
251
+ if (target.length === 0) return null;
252
+
253
+ for await (const entry of iteratePinHistory(sanitized, ref, deps.fetch)) {
254
+ if (entry.pin.toLowerCase() === target) return entry;
255
+ }
256
+ return null;
257
+ }