@vellumai/assistant 0.8.8 → 0.8.9-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -1,11 +1,19 @@
1
1
  /**
2
- * Install an external plugin by name from the canonical GitHub source.
2
+ * Install a plugin by name from the canonical GitHub source.
3
3
  *
4
- * The plugin source convention is fixed at
5
- * `vellum-ai/vellum-assistant/experimental/plugins/<name>/` on the configured
6
- * git ref. The {@link installPlugin} entry point fetches the directory tree
7
- * via the GitHub Contents API and materializes it into
8
- * `<workspacePluginsDir>/<name>/` so the daemon discovers it on next start.
4
+ * A name resolves to one of two sources, materialized into
5
+ * `<workspacePluginsDir>/<name>/` so the daemon discovers it on next start:
6
+ * 1. A whitelisted external ecosystem plugin, when the name matches an entry
7
+ * in the curated `experimental/plugins/marketplace.json` manifest. The
8
+ * pinned `owner/repo[/path]@ref` (see {@link ./plugin-marketplace}) is
9
+ * fetched with a shallow `git` clone at that ref — one network operation
10
+ * regardless of repo size, immune to GitHub's unauthenticated API
11
+ * rate-limit, and recording the exact resolved commit for provenance.
12
+ * 2. Otherwise the first-party convention
13
+ * `vellum-ai/vellum-assistant/experimental/plugins/<name>/` at the
14
+ * configured ref, fetched via the GitHub Contents API (a small handful of
15
+ * in-repo files — cloning the whole monorepo to install one would be
16
+ * wasteful).
9
17
  *
10
18
  * Designed for direct programmatic use. The CLI command
11
19
  * `assistant plugins install <name>` is a thin wrapper that supplies
@@ -15,10 +23,27 @@
15
23
  * a test fixture) and an override workspace directory.
16
24
  */
17
25
 
18
- import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
26
+ import { execFile } from "node:child_process";
27
+ import {
28
+ copyFileSync,
29
+ existsSync,
30
+ mkdirSync,
31
+ readdirSync,
32
+ renameSync,
33
+ rmSync,
34
+ statSync,
35
+ writeFileSync,
36
+ } from "node:fs";
19
37
  import { dirname, join } from "node:path";
38
+ import { promisify } from "node:util";
20
39
 
21
40
  import { getWorkspacePluginsDir } from "../../util/platform.js";
41
+ import {
42
+ fetchMarketplaceEntries,
43
+ resolveMarketplaceSource,
44
+ } from "./plugin-marketplace.js";
45
+
46
+ const execFileAsync = promisify(execFile);
22
47
 
23
48
  const PLUGIN_SOURCE_OWNER = "vellum-ai";
24
49
  const PLUGIN_SOURCE_REPO = "vellum-assistant";
@@ -47,6 +72,16 @@ export type FetchLike = (
47
72
  init?: RequestInit,
48
73
  ) => Promise<Response>;
49
74
 
75
+ /**
76
+ * Runs a `git` subcommand in `cwd` and resolves its stdout. Injected so tests
77
+ * can simulate a clone without spawning a real git process; production callers
78
+ * fall back to {@link defaultGitRunner}.
79
+ */
80
+ export type GitRunner = (
81
+ args: readonly string[],
82
+ opts: { readonly cwd: string },
83
+ ) => Promise<{ readonly stdout: string }>;
84
+
50
85
  /** Options that control which plugin to install and how. */
51
86
  export interface InstallPluginOptions {
52
87
  readonly name: string;
@@ -63,6 +98,8 @@ export interface InstallPluginDeps {
63
98
  readonly fetch: FetchLike;
64
99
  /** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
65
100
  readonly workspacePluginsDir?: string;
101
+ /** Override the git runner used to clone external plugin sources. Falls back to {@link defaultGitRunner}. */
102
+ readonly runGit?: GitRunner;
66
103
  }
67
104
 
68
105
  /** Successful install result. */
@@ -72,12 +109,16 @@ export interface InstallPluginResult {
72
109
  readonly target: string;
73
110
  readonly fileCount: number;
74
111
  readonly ref: string;
112
+ /** Resolved commit SHA for git-cloned external sources; null for first-party (no clone). */
113
+ readonly commit: string | null;
75
114
  }
76
115
 
77
116
  /** Plugin name failed sanitization. */
78
117
  export class InvalidPluginNameError extends Error {
79
118
  constructor(name: string) {
80
- super(`Invalid plugin name "${name}". Names must match /^[a-z0-9][a-z0-9_-]*$/.`);
119
+ super(
120
+ `Invalid plugin name "${name}". Names must match /^[a-z0-9][a-z0-9_-]*$/.`,
121
+ );
81
122
  this.name = "InvalidPluginNameError";
82
123
  }
83
124
  }
@@ -98,13 +139,142 @@ export class PluginNotFoundError extends Error {
98
139
  constructor(
99
140
  readonly pluginName: string,
100
141
  readonly ref: string,
142
+ /** `owner/repo/path` the plugin was looked for at. */
143
+ sourceLabel: string,
101
144
  ) {
102
- const sourcePath = `${PLUGIN_SOURCE_OWNER}/${PLUGIN_SOURCE_REPO}/${PLUGIN_SOURCE_PATH_PREFIX}/${pluginName}`;
103
- super(`Plugin "${pluginName}" not found at ${sourcePath} (ref ${ref}).`);
145
+ super(`Plugin "${pluginName}" not found at ${sourceLabel} (ref ${ref}).`);
104
146
  this.name = "PluginNotFoundError";
105
147
  }
106
148
  }
107
149
 
150
+ /**
151
+ * The plugin source is temporarily unreachable — GitHub rate-limited us or
152
+ * returned a 5xx. Distinct from a hard failure (the plugin genuinely doesn't
153
+ * exist) so the caller can surface a retryable 503 instead of a 500.
154
+ */
155
+ export class PluginSourceUnavailableError extends Error {
156
+ readonly status: number;
157
+ constructor(message: string, status: number) {
158
+ super(message);
159
+ this.name = "PluginSourceUnavailableError";
160
+ this.status = status;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Classify an upstream GitHub status as transient (worth retrying) vs hard.
166
+ * A 429 or 5xx is always transient. A 403 is GitHub's unauthenticated
167
+ * rate-limit signal only when the remaining-quota header is exhausted —
168
+ * a 403 without it is a genuine authorization failure and stays hard.
169
+ */
170
+ function isTransientUpstreamStatus(res: Response): boolean {
171
+ if (res.status === 429 || res.status >= 500) return true;
172
+ if (res.status === 403) {
173
+ return res.headers.get("x-ratelimit-remaining") === "0";
174
+ }
175
+ return false;
176
+ }
177
+
178
+ /** Resolved GitHub coordinates a plugin name is fetched from. */
179
+ interface PluginFetchSource {
180
+ /** Whether the plugin lives in our monorepo or an external whitelisted repo. */
181
+ readonly kind: "first-party" | "external";
182
+ readonly owner: string;
183
+ readonly repo: string;
184
+ /** Repo-relative directory holding the plugin root; `""` = repo root. */
185
+ readonly rootPath: string;
186
+ readonly ref: string;
187
+ }
188
+
189
+ /** Build the `owner/repo/path` label used in not-found errors. */
190
+ function sourceLabel(source: PluginFetchSource): string {
191
+ return source.rootPath
192
+ ? `${source.owner}/${source.repo}/${source.rootPath}`
193
+ : `${source.owner}/${source.repo}`;
194
+ }
195
+
196
+ /** First-party `experimental/plugins/<name>` coordinates at a given ref. */
197
+ function firstPartySource(name: string, ref: string): PluginFetchSource {
198
+ return {
199
+ kind: "first-party",
200
+ owner: PLUGIN_SOURCE_OWNER,
201
+ repo: PLUGIN_SOURCE_REPO,
202
+ rootPath: `${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
203
+ ref,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Probe whether a first-party plugin directory exists at the given source.
209
+ *
210
+ * A transient listing failure resolves to `false` so a marketplace-claimed
211
+ * name still reaches its external source — the rare collision guarantee gives
212
+ * way to keeping the common external-only install path working under flaky
213
+ * network conditions.
214
+ */
215
+ async function firstPartyPluginExists(
216
+ source: PluginFetchSource,
217
+ fetchFn: FetchLike,
218
+ ): Promise<boolean> {
219
+ try {
220
+ const entries = await listDir(
221
+ source.owner,
222
+ source.repo,
223
+ source.rootPath,
224
+ source.ref,
225
+ fetchFn,
226
+ );
227
+ return entries !== null && entries.length > 0;
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Resolve a plugin name to concrete GitHub coordinates.
235
+ *
236
+ * First-party plugins win a name collision: a name claimed by the curated
237
+ * marketplace is fetched from its pinned external repo only when no
238
+ * `experimental/plugins/<name>` directory exists in-repo. This mirrors the
239
+ * search catalog, where an in-repo plugin suppresses a same-named marketplace
240
+ * entry — install must advertise and install the same source.
241
+ *
242
+ * A missing or malformed manifest degrades to first-party resolution — the
243
+ * whitelist is supplementary and must never block installing a first-party
244
+ * plugin. An external name then surfaces a clear not-found error downstream.
245
+ */
246
+ async function resolvePluginSource(
247
+ name: string,
248
+ marketplaceRef: string,
249
+ fetchFn: FetchLike,
250
+ ): Promise<PluginFetchSource> {
251
+ let resolved = null;
252
+ try {
253
+ const entries = await fetchMarketplaceEntries(
254
+ { fetch: fetchFn },
255
+ { ref: marketplaceRef },
256
+ );
257
+ resolved = resolveMarketplaceSource(name, entries);
258
+ } catch {
259
+ // Degrade to first-party resolution below.
260
+ }
261
+
262
+ const firstParty = firstPartySource(name, marketplaceRef);
263
+ if (!resolved) return firstParty;
264
+
265
+ if (await firstPartyPluginExists(firstParty, fetchFn)) {
266
+ return firstParty;
267
+ }
268
+
269
+ return {
270
+ kind: "external",
271
+ owner: resolved.owner,
272
+ repo: resolved.repo,
273
+ rootPath: resolved.path,
274
+ ref: resolved.ref,
275
+ };
276
+ }
277
+
108
278
  /**
109
279
  * Reject plugin names that could escape the canonical source path or the
110
280
  * install target. The source convention is a flat namespace under
@@ -141,7 +311,9 @@ function assertSafeFilename(label: string, candidate: string): void {
141
311
  candidate.includes("\0") ||
142
312
  candidate.split(/[/\\]/).some((seg) => seg === "..")
143
313
  ) {
144
- throw new Error(`Unsafe ${label} from GitHub response: ${JSON.stringify(candidate)}`);
314
+ throw new Error(
315
+ `Unsafe ${label} from GitHub response: ${JSON.stringify(candidate)}`,
316
+ );
145
317
  }
146
318
  }
147
319
 
@@ -158,9 +330,12 @@ export async function installPlugin(
158
330
  deps: InstallPluginDeps,
159
331
  ): Promise<InstallPluginResult> {
160
332
  const name = sanitizePluginName(opts.name);
161
- const ref = opts.ref ?? DEFAULT_PLUGIN_REF;
333
+ const marketplaceRef = opts.ref ?? DEFAULT_PLUGIN_REF;
162
334
  const force = opts.force ?? false;
163
335
 
336
+ const source = await resolvePluginSource(name, marketplaceRef, deps.fetch);
337
+ const ref = source.ref;
338
+
164
339
  const pluginsDir = deps.workspacePluginsDir ?? getWorkspacePluginsDir();
165
340
  const target = join(pluginsDir, name);
166
341
 
@@ -178,13 +353,26 @@ export async function installPlugin(
178
353
  mkdirSync(stagingDir, { recursive: true });
179
354
 
180
355
  let fileCount: number;
356
+ let commit: string | null = null;
181
357
  try {
182
- fileCount = await copyDir(
183
- `${PLUGIN_SOURCE_PATH_PREFIX}/${name}`,
184
- ref,
185
- stagingDir,
186
- deps.fetch,
187
- );
358
+ if (source.kind === "external") {
359
+ const cloned = await copyExternalViaGit(
360
+ source,
361
+ stagingDir,
362
+ deps.runGit ?? defaultGitRunner,
363
+ );
364
+ fileCount = cloned.fileCount;
365
+ commit = cloned.commit;
366
+ } else {
367
+ fileCount = await copyDir(
368
+ source.owner,
369
+ source.repo,
370
+ source.rootPath,
371
+ ref,
372
+ stagingDir,
373
+ deps.fetch,
374
+ );
375
+ }
188
376
  } catch (err) {
189
377
  rmSync(stagingDir, { recursive: true, force: true });
190
378
  throw err;
@@ -192,9 +380,15 @@ export async function installPlugin(
192
380
 
193
381
  if (fileCount === 0) {
194
382
  rmSync(stagingDir, { recursive: true, force: true });
195
- throw new PluginNotFoundError(name, ref);
383
+ throw new PluginNotFoundError(name, ref, sourceLabel(source));
196
384
  }
197
385
 
386
+ // Record install provenance (source coordinates + resolved commit) as a
387
+ // hidden sidecar before the swap so it lands atomically with the files. The
388
+ // daemon loader enumerates plugin directories and reads each plugin's
389
+ // `package.json`, skipping dotfiles — so this never gets mistaken for code.
390
+ writeInstallManifest(stagingDir, name, source, ref, commit);
391
+
198
392
  // Atomic-ish swap: rmSync + renameSync. On POSIX the rename itself is
199
393
  // atomic, so the only window where the target is absent is between the
200
394
  // rm and the rename — and at that point the staging dir is fully populated.
@@ -203,54 +397,290 @@ export async function installPlugin(
203
397
  }
204
398
  renameSync(stagingDir, target);
205
399
 
206
- return { name, target, fileCount, ref };
400
+ return { name, target, fileCount, ref, commit };
207
401
  }
208
402
 
403
+ /** Cap on any single git invocation; a shallow fetch is well under this. */
404
+ const GIT_TIMEOUT_MS = 120_000;
405
+
406
+ /** Install-provenance sidecar written at the plugin root. */
407
+ const INSTALL_MANIFEST_FILENAME = ".vellum-plugin.json";
408
+
409
+ /**
410
+ * Materialize an external plugin by shallow-cloning its repo at the pinned ref.
411
+ *
412
+ * A single `git fetch --depth 1 <ref>` transfers the tree in one network
413
+ * operation regardless of how many directories the plugin spans, so it is
414
+ * immune to GitHub's 60/hr unauthenticated Contents-API rate limit — the
415
+ * failure mode a recursive per-directory walk hit on plugins like caveman
416
+ * (dozens of nested folders, one API request each). Cloning also resolves the
417
+ * exact commit the ref points at, recorded for version provenance.
418
+ *
419
+ * The clone lands in a sibling scratch dir; only the plugin root (the repo
420
+ * root, or `source.rootPath` within it) is copied into `destDir`, minus the
421
+ * `.git` metadata and any symlinks (the loader follows neither). Returns the
422
+ * file count and resolved commit; zero files means the ref exists but the
423
+ * declared sub-path doesn't, which the caller maps to not-found.
424
+ */
425
+ async function copyExternalViaGit(
426
+ source: PluginFetchSource,
427
+ destDir: string,
428
+ runGit: GitRunner,
429
+ ): Promise<{ fileCount: number; commit: string | null }> {
430
+ const cloneDir = `${destDir}.gitclone`;
431
+ rmSync(cloneDir, { recursive: true, force: true });
432
+ mkdirSync(cloneDir, { recursive: true });
433
+
434
+ try {
435
+ const repoUrl = `https://github.com/${source.owner}/${source.repo}.git`;
436
+ await runGit(["init", "--quiet"], { cwd: cloneDir });
437
+ await runGit(["remote", "add", "origin", repoUrl], { cwd: cloneDir });
438
+
439
+ try {
440
+ await runGit(["fetch", "--depth", "1", "--quiet", "origin", source.ref], {
441
+ cwd: cloneDir,
442
+ });
443
+ } catch (err) {
444
+ // A missing repo/ref (or a private one we can't reach) is a hard
445
+ // not-found, surfaced as zero files. Anything else — network loss, a
446
+ // transient GitHub outage — is retryable, so map it to a 503.
447
+ if (isGitRefNotFound(err)) return { fileCount: 0, commit: null };
448
+ throw new PluginSourceUnavailableError(
449
+ `git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${gitErrorText(err)}`,
450
+ 503,
451
+ );
452
+ }
453
+
454
+ await runGit(["checkout", "--quiet", "FETCH_HEAD"], { cwd: cloneDir });
455
+
456
+ let commit: string | null = null;
457
+ try {
458
+ const { stdout } = await runGit(["rev-parse", "HEAD"], { cwd: cloneDir });
459
+ commit = stdout.trim() || null;
460
+ } catch {
461
+ // Provenance is best-effort; a missing commit must not fail the install.
462
+ commit = null;
463
+ }
464
+
465
+ const srcRoot = source.rootPath
466
+ ? join(cloneDir, source.rootPath)
467
+ : cloneDir;
468
+ if (!existsSync(srcRoot) || !statSync(srcRoot).isDirectory()) {
469
+ return { fileCount: 0, commit };
470
+ }
471
+
472
+ const fileCount = copyTreeSkippingGit(srcRoot, destDir);
473
+ return { fileCount, commit };
474
+ } finally {
475
+ rmSync(cloneDir, { recursive: true, force: true });
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Recursively copy regular files from `srcRoot` into `destDir`, skipping the
481
+ * top-level `.git` directory and any symlinks. Returns the file count.
482
+ */
483
+ function copyTreeSkippingGit(srcRoot: string, destDir: string): number {
484
+ let count = 0;
485
+ const walk = (relDir: string): void => {
486
+ const absDir = relDir ? join(srcRoot, relDir) : srcRoot;
487
+ for (const entry of readdirSync(absDir, { withFileTypes: true })) {
488
+ // Drop git metadata and symlinks: the loader follows neither, and a
489
+ // symlink could otherwise point outside the staging tree.
490
+ if (relDir === "" && entry.name === ".git") continue;
491
+ if (entry.isSymbolicLink()) continue;
492
+
493
+ const rel = relDir ? join(relDir, entry.name) : entry.name;
494
+ if (entry.isDirectory()) {
495
+ walk(rel);
496
+ continue;
497
+ }
498
+ if (!entry.isFile()) continue;
499
+
500
+ const dest = join(destDir, rel);
501
+ mkdirSync(dirname(dest), { recursive: true });
502
+ copyFileSync(join(srcRoot, rel), dest);
503
+ count++;
504
+ }
505
+ };
506
+ walk("");
507
+ return count;
508
+ }
509
+
510
+ /** True when a git fetch failed because the repo or ref is unreachable. */
511
+ function isGitRefNotFound(err: unknown): boolean {
512
+ const text = gitErrorText(err).toLowerCase();
513
+ return [
514
+ "could not find remote ref",
515
+ "couldn't find remote ref",
516
+ "remote branch",
517
+ "repository not found",
518
+ "could not read from remote repository",
519
+ "could not read username",
520
+ "terminal prompts disabled",
521
+ "authentication failed",
522
+ ].some((needle) => text.includes(needle));
523
+ }
524
+
525
+ /** Extract a stderr/message blob from a spawn error for classification/logging. */
526
+ function gitErrorText(err: unknown): string {
527
+ if (err instanceof Error) {
528
+ const withStreams = err as Error & { stderr?: unknown };
529
+ const stderr =
530
+ typeof withStreams.stderr === "string" ? withStreams.stderr : "";
531
+ return `${err.message} ${stderr}`.trim();
532
+ }
533
+ return String(err);
534
+ }
535
+
536
+ /**
537
+ * Hardened `git` runner used in production. Strips inherited `GIT_*` vars
538
+ * (which could redirect config, hooks, or the object store), disables the
539
+ * credential prompt so a private/missing repo fails fast instead of hanging
540
+ * the daemon, and augments `PATH` so the real git is found when the daemon is
541
+ * launched from a macOS `.app` bundle with a minimal environment.
542
+ */
543
+ export const defaultGitRunner: GitRunner = async (args, opts) => {
544
+ const { stdout } = await execFileAsync("git", [...args], {
545
+ cwd: opts.cwd,
546
+ encoding: "utf8",
547
+ timeout: GIT_TIMEOUT_MS,
548
+ maxBuffer: 64 * 1024 * 1024,
549
+ env: pluginGitEnv(),
550
+ });
551
+ return { stdout };
552
+ };
553
+
554
+ function pluginGitEnv(): NodeJS.ProcessEnv {
555
+ const env: NodeJS.ProcessEnv = {};
556
+ for (const [key, value] of Object.entries(process.env)) {
557
+ if (value !== undefined && !key.startsWith("GIT_")) env[key] = value;
558
+ }
559
+ env.GIT_TERMINAL_PROMPT = "0";
560
+ const extraPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
561
+ const current = (env.PATH ?? "").split(":").filter(Boolean);
562
+ const missing = extraPaths.filter((p) => !current.includes(p));
563
+ if (missing.length > 0) {
564
+ env.PATH = [...current, ...missing].join(":");
565
+ }
566
+ return env;
567
+ }
568
+
569
+ /**
570
+ * Write the install-provenance sidecar into the staged plugin root, recording
571
+ * the resolved source coordinates and commit so we can later report or verify
572
+ * exactly what is installed. Hidden (dot-prefixed) so the daemon loader, which
573
+ * skips dotfiles, never mistakes it for plugin code.
574
+ */
575
+ function writeInstallManifest(
576
+ stagingDir: string,
577
+ name: string,
578
+ source: PluginFetchSource,
579
+ ref: string,
580
+ commit: string | null,
581
+ ): void {
582
+ const manifest = {
583
+ name,
584
+ source: {
585
+ kind: source.kind,
586
+ owner: source.owner,
587
+ repo: source.repo,
588
+ path: source.rootPath || undefined,
589
+ ref,
590
+ },
591
+ commit: commit ?? undefined,
592
+ installedAt: new Date().toISOString(),
593
+ };
594
+ writeFileSync(
595
+ join(stagingDir, INSTALL_MANIFEST_FILENAME),
596
+ `${JSON.stringify(manifest, null, 2)}\n`,
597
+ );
598
+ }
599
+
600
+ /**
601
+ * Recursively copy a first-party plugin directory via the GitHub Contents API.
602
+ *
603
+ * First-party plugins live in our own monorepo as a small handful of files, so
604
+ * the per-directory walk stays well within the unauthenticated rate limit —
605
+ * and avoids cloning the entire repository just to install one plugin. Returns
606
+ * the number of files written; zero means the directory doesn't exist at this
607
+ * ref, which the caller maps to a not-found error.
608
+ */
209
609
  async function copyDir(
610
+ owner: string,
611
+ repo: string,
210
612
  apiPath: string,
211
613
  ref: string,
212
614
  destDir: string,
213
615
  fetchFn: FetchLike,
214
616
  ): Promise<number> {
215
- const entries = await listDir(apiPath, ref, fetchFn);
617
+ const entries = await listDir(owner, repo, apiPath, ref, fetchFn);
216
618
  if (entries === null) return 0;
217
619
 
218
620
  let count = 0;
219
621
  for (const entry of entries) {
622
+ // The daemon loader follows neither symlinks nor submodules; skip them.
623
+ if (entry.type === "symlink" || entry.type === "submodule") continue;
220
624
  assertSafeFilename("entry name", entry.name);
625
+
221
626
  if (entry.type === "dir") {
222
627
  const subDest = join(destDir, entry.name);
223
628
  mkdirSync(subDest, { recursive: true });
224
- count += await copyDir(entry.path, ref, subDest, fetchFn);
629
+ count += await copyDir(owner, repo, entry.path, ref, subDest, fetchFn);
225
630
  continue;
226
631
  }
227
- if (entry.type === "file") {
228
- await copyFile(entry, destDir, fetchFn);
229
- count++;
230
- continue;
231
- }
232
- // Skip symlink + submodule deliberately. The daemon-side loader does not
233
- // follow either, so reproducing them in the install target adds risk
234
- // without value.
632
+
633
+ await copyFile(entry, join(destDir, entry.name), fetchFn);
634
+ count++;
235
635
  }
236
636
  return count;
237
637
  }
238
638
 
639
+ /** Download one file entry from the Contents API into `dest`. */
640
+ async function copyFile(
641
+ entry: GitHubContentEntry,
642
+ dest: string,
643
+ fetchFn: FetchLike,
644
+ ): Promise<void> {
645
+ if (!entry.download_url) {
646
+ throw new Error(`No download URL for ${entry.path}`);
647
+ }
648
+ const res = await githubFetch(
649
+ entry.download_url,
650
+ "application/octet-stream",
651
+ fetchFn,
652
+ );
653
+ if (!res.ok) {
654
+ const label = `Download failed for ${entry.path}: HTTP ${res.status}`;
655
+ if (isTransientUpstreamStatus(res)) {
656
+ throw new PluginSourceUnavailableError(label, res.status);
657
+ }
658
+ throw new Error(label);
659
+ }
660
+ const buf = Buffer.from(await res.arrayBuffer());
661
+ mkdirSync(dirname(dest), { recursive: true });
662
+ writeFileSync(dest, buf);
663
+ }
664
+
239
665
  async function listDir(
666
+ owner: string,
667
+ repo: string,
240
668
  apiPath: string,
241
669
  ref: string,
242
670
  fetchFn: FetchLike,
243
671
  ): Promise<readonly GitHubContentEntry[] | null> {
244
672
  const url =
245
- `https://api.github.com/repos/${PLUGIN_SOURCE_OWNER}/${PLUGIN_SOURCE_REPO}` +
673
+ `https://api.github.com/repos/${owner}/${repo}` +
246
674
  `/contents/${encodeURIComponent(apiPath).replaceAll("%2F", "/")}?ref=${encodeURIComponent(ref)}`;
247
675
 
248
676
  const res = await githubFetch(url, "application/vnd.github+json", fetchFn);
249
677
  if (res.status === 404) return null;
250
678
  if (!res.ok) {
251
- throw new Error(
252
- `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`,
253
- );
679
+ const label = `GitHub contents listing failed for ${apiPath} @ ${ref}: HTTP ${res.status}`;
680
+ if (isTransientUpstreamStatus(res)) {
681
+ throw new PluginSourceUnavailableError(label, res.status);
682
+ }
683
+ throw new Error(label);
254
684
  }
255
685
 
256
686
  const body = (await res.json()) as unknown;
@@ -263,27 +693,6 @@ async function listDir(
263
693
  return body as readonly GitHubContentEntry[];
264
694
  }
265
695
 
266
- async function copyFile(
267
- entry: GitHubContentEntry,
268
- destDir: string,
269
- fetchFn: FetchLike,
270
- ): Promise<void> {
271
- if (!entry.download_url) {
272
- throw new Error(`GitHub contents entry has no download_url: ${entry.path}`);
273
- }
274
- const res = await githubFetch(entry.download_url, "application/octet-stream", fetchFn);
275
- if (!res.ok) {
276
- throw new Error(`Download failed for ${entry.path}: HTTP ${res.status}`);
277
- }
278
- const buf = Buffer.from(await res.arrayBuffer());
279
- // entry.name was already validated by the caller; assert again as a
280
- // belt-and-braces guard so copyFile is safe to call from future paths.
281
- assertSafeFilename("file entry name", entry.name);
282
- const dest = join(destDir, entry.name);
283
- mkdirSync(dirname(dest), { recursive: true });
284
- writeFileSync(dest, buf);
285
- }
286
-
287
696
  /**
288
697
  * Wraps `fetchFn` with the headers we want to send to GitHub for every
289
698
  * request. Unauthenticated — the canonical source is a public repo, so