@vellumai/assistant 0.12.2 → 0.12.3-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -17,6 +17,7 @@ let handleRequestResult: {
17
17
  body: unknown;
18
18
  bodyEncoding?: "base64";
19
19
  account?: string | null;
20
+ hint?: string;
20
21
  } = {
21
22
  ok: true,
22
23
  status: 200,
@@ -71,18 +72,71 @@ afterEach(() => {
71
72
  process.exitCode = 0;
72
73
  });
73
74
 
74
- async function runRequestCommand(args: string[]): Promise<{
75
+ /**
76
+ * Runs the command with both streams captured and `process.exit` recorded.
77
+ * The stream stubs honour a write callback the way the real streams do, so an
78
+ * exit that rides the last write lands in `exitCalls`, and only after every
79
+ * chunk written before it has been captured. `holdStderr` keeps stderr
80
+ * callbacks back and returns them, for the case where stderr is still in
81
+ * flight when stdout has drained.
82
+ */
83
+ async function runRequestCommand(
84
+ args: string[],
85
+ options: { holdStderr?: boolean } = {},
86
+ ): Promise<{
75
87
  stdout: Buffer;
88
+ stderr: string;
76
89
  exitCode: number;
90
+ exitCalls: number[];
91
+ heldStderrCallbacks: Array<() => void>;
77
92
  }> {
78
93
  const chunks: Buffer[] = [];
94
+ const stderrChunks: string[] = [];
95
+ const exitCalls: number[] = [];
96
+ const heldStderrCallbacks: Array<() => void> = [];
79
97
  const originalWrite = process.stdout.write;
80
- process.stdout.write = ((chunk: string | Uint8Array) => {
98
+ const originalStderrWrite = process.stderr.write;
99
+ const originalExit = process.exit;
100
+ const callbackOf = (rest: unknown[]): (() => void) | undefined =>
101
+ rest.find((arg) => typeof arg === "function") as (() => void) | undefined;
102
+ process.stdout.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
81
103
  chunks.push(
82
104
  typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk),
83
105
  );
106
+ callbackOf(rest)?.();
84
107
  return true;
85
108
  }) as typeof process.stdout.write;
109
+ process.stderr.write = ((chunk: string | Uint8Array, ...rest: unknown[]) => {
110
+ stderrChunks.push(String(chunk));
111
+ const callback = callbackOf(rest);
112
+ if (callback) {
113
+ if (options.holdStderr) {
114
+ heldStderrCallbacks.push(holdCallback(callback));
115
+ } else {
116
+ callback();
117
+ }
118
+ }
119
+ return true;
120
+ }) as typeof process.stderr.write;
121
+ const recordExit = ((code?: number) => {
122
+ exitCalls.push(code ?? Number(process.exitCode ?? 0));
123
+ }) as typeof process.exit;
124
+ process.exit = recordExit;
125
+ // A held callback runs after this function has restored the real
126
+ // `process.exit`, so it reinstalls the recorder for the duration of the
127
+ // call; the exit it triggers must land in `exitCalls`, never end the test
128
+ // process.
129
+ const holdCallback = (callback: () => void): (() => void) => {
130
+ return () => {
131
+ const current = process.exit;
132
+ process.exit = recordExit;
133
+ try {
134
+ callback();
135
+ } finally {
136
+ process.exit = current;
137
+ }
138
+ };
139
+ };
86
140
 
87
141
  try {
88
142
  const program = new Command();
@@ -101,11 +155,16 @@ async function runRequestCommand(args: string[]): Promise<{
101
155
  // Commander may throw under exitOverride for parse errors.
102
156
  } finally {
103
157
  process.stdout.write = originalWrite;
158
+ process.stderr.write = originalStderrWrite;
159
+ process.exit = originalExit;
104
160
  }
105
161
 
106
162
  return {
107
163
  stdout: Buffer.concat(chunks),
164
+ stderr: stderrChunks.join(""),
108
165
  exitCode: Number(process.exitCode ?? 0),
166
+ exitCalls,
167
+ heldStderrCallbacks,
109
168
  };
110
169
  }
111
170
 
@@ -308,6 +367,124 @@ describe("oauth request body output", () => {
308
367
  expect(parsed.bodyEncoding).toBe("base64");
309
368
  });
310
369
 
370
+ test("exits non-zero when the route reports failure inside a 2xx", async () => {
371
+ // The exit code follows the route's verdict, so a caller that only checks
372
+ // the exit code sees a refused call as a failure; the body still prints,
373
+ // since it carries the provider's own error code.
374
+ handleRequestResult = {
375
+ ok: false,
376
+ status: 200,
377
+ headers: { "content-type": "application/json" },
378
+ body: { ok: false, error: "not_in_channel" },
379
+ account: "user@example.com",
380
+ };
381
+ const { stdout, exitCode } = await runRequestCommand([
382
+ "--provider",
383
+ "slack_channel",
384
+ "-s",
385
+ "/chat.postMessage",
386
+ ]);
387
+
388
+ expect(exitCode).toBe(1);
389
+ expect(stdout.toString("utf8")).toContain("not_in_channel");
390
+ });
391
+
392
+ test("exits itself once the whole body has drained", async () => {
393
+ // The in-process route leaves handles open, so the command has to exit;
394
+ // and it must do so on the write callback, because a bare exit after a
395
+ // large piped write drops everything past the first 64 KB.
396
+ const body = "x".repeat(200_000);
397
+ handleRequestResult = {
398
+ ok: true,
399
+ status: 200,
400
+ headers: { "content-type": "text/plain" },
401
+ body,
402
+ account: "user@example.com",
403
+ };
404
+ const { stdout, exitCode, exitCalls } = await runRequestCommand([
405
+ "--provider",
406
+ "google",
407
+ "-s",
408
+ "https://api.google.com/v1/big",
409
+ ]);
410
+
411
+ expect(stdout.toString("utf8")).toBe(body + "\n");
412
+ expect(exitCode).toBe(0);
413
+ expect(exitCalls).toEqual([0]);
414
+ });
415
+
416
+ test("exits after writing an empty body to a file", async () => {
417
+ const target = join(tempDir, "empty.bin");
418
+ handleRequestResult = {
419
+ ok: true,
420
+ status: 204,
421
+ headers: {},
422
+ body: null,
423
+ account: "user@example.com",
424
+ };
425
+ const { exitCalls } = await runRequestCommand([
426
+ "--provider",
427
+ "google",
428
+ "-s",
429
+ "-o",
430
+ target,
431
+ "https://api.google.com/v1/none",
432
+ ]);
433
+
434
+ expect(readFileSync(target)).toHaveLength(0);
435
+ expect(exitCalls).toEqual([0]);
436
+ });
437
+
438
+ test("waits for stderr to drain before exiting when stdout carried nothing", async () => {
439
+ // With `-o`, stdout carries nothing, so the exit cannot ride a stdout
440
+ // write, and the diagnostics on stderr (the account line, a hint) may
441
+ // still be in flight. The exit waits for the last of them.
442
+ const target = join(tempDir, "body.json");
443
+ handleRequestResult = {
444
+ ok: false,
445
+ status: 200,
446
+ headers: { "content-type": "application/json" },
447
+ body: { ok: false, error: "not_in_channel" },
448
+ account: "user@example.com",
449
+ hint: "slack_channel answered HTTP 200 but reported ok: false",
450
+ };
451
+ const { stderr, exitCalls, heldStderrCallbacks } = await runRequestCommand(
452
+ ["--provider", "slack_channel", "-o", target, "/chat.postMessage"],
453
+ { holdStderr: true },
454
+ );
455
+
456
+ expect(stderr).toContain("Account: user@example.com");
457
+ expect(stderr).toContain("ok: false");
458
+ expect(heldStderrCallbacks).toHaveLength(2);
459
+ expect(exitCalls).toEqual([]);
460
+
461
+ heldStderrCallbacks[0]();
462
+ expect(exitCalls).toEqual([]);
463
+ heldStderrCallbacks[1]();
464
+ expect(exitCalls).toEqual([1]);
465
+ });
466
+
467
+ test("a stdout body waits for stderr diagnostics written before it", async () => {
468
+ handleRequestResult = {
469
+ ok: true,
470
+ status: 200,
471
+ headers: { "content-type": "application/json" },
472
+ body: { hello: "world" },
473
+ account: "user@example.com",
474
+ };
475
+ const { stdout, exitCalls, heldStderrCallbacks } = await runRequestCommand(
476
+ ["--provider", "google", "https://api.google.com/v1/me"],
477
+ { holdStderr: true },
478
+ );
479
+
480
+ expect(stdout.toString("utf8")).toContain("hello");
481
+ expect(heldStderrCallbacks).toHaveLength(1);
482
+ expect(exitCalls).toEqual([]);
483
+
484
+ heldStderrCallbacks[0]();
485
+ expect(exitCalls).toEqual([0]);
486
+ });
487
+
311
488
  test("writes text bodies as UTF-8 and appends a newline on stdout", async () => {
312
489
  handleRequestResult = {
313
490
  ok: true,
@@ -142,11 +142,38 @@ export async function runAuthenticatedRequest(params: {
142
142
  const { providerKey, url, opts, cmd } = params;
143
143
  const jsonMode = shouldOutputJson(cmd);
144
144
 
145
+ // The command exits itself, once its last write on either stream has
146
+ // drained. Running the route in-process leaves handles open that the event
147
+ // loop waits on (the lazy CES connection the credential resolves through is
148
+ // never closed by the CLI), so returning from the action would leave the
149
+ // process alive after a complete response. A bare `process.exit()` is not
150
+ // the answer either: with a pipe on the other end, Bun flushes what the
151
+ // pipe accepts and drops the rest, so the exit rides write callbacks, which
152
+ // fire when the bytes are out. Stdout's last write carries `exit` itself;
153
+ // stderr's writes are counted, so an exit requested while one is still in
154
+ // flight waits for it. The pipe's capacity is not a bound to lean on: a
155
+ // busy host can shrink an unprivileged pipe to a single page.
156
+ let stderrInFlight = 0;
157
+ let exitRequested = false;
158
+ const exit = (): void => {
159
+ exitRequested = true;
160
+ if (stderrInFlight === 0) {
161
+ process.exit(Number(process.exitCode ?? 0));
162
+ }
163
+ };
164
+
145
165
  // Helper: write info to stderr (respects -s)
146
166
  const writeInfo = (msg: string): void => {
147
- if (!opts.silent) {
148
- process.stderr.write(msg + "\n");
167
+ if (opts.silent) {
168
+ return;
149
169
  }
170
+ stderrInFlight += 1;
171
+ process.stderr.write(msg + "\n", () => {
172
+ stderrInFlight -= 1;
173
+ if (exitRequested) {
174
+ exit();
175
+ }
176
+ });
150
177
  };
151
178
 
152
179
  try {
@@ -232,17 +259,18 @@ export async function runAuthenticatedRequest(params: {
232
259
  // reported the way every other failure here is, so `--json` gets its
233
260
  // envelope and the caller's diagnostics hint is not lost; only the
234
261
  // exit code comes from the route's status.
235
- writeError(cmd, `${err.message}\n\n${params.diagnosticsHint}`);
236
262
  process.exitCode = exitCodeFromIpcResult({
237
263
  statusCode: err.statusCode,
238
264
  });
265
+ writeError(cmd, `${err.message}\n\n${params.diagnosticsHint}`, exit);
239
266
  return;
240
267
  }
241
268
  throw err;
242
269
  }
243
270
 
244
- // Non-2xx exit code
245
- if (result.status < 200 || result.status >= 300) {
271
+ // The route's verdict, not the status code: a provider that reports
272
+ // failure inside a 2xx (Slack's `ok: false`) is a failed request.
273
+ if (!result.ok) {
246
274
  process.exitCode = 1;
247
275
  }
248
276
 
@@ -261,7 +289,7 @@ export async function runAuthenticatedRequest(params: {
261
289
 
262
290
  // JSON output mode
263
291
  if (jsonMode) {
264
- writeOutput(cmd, result);
292
+ writeOutput(cmd, result, exit);
265
293
  return;
266
294
  }
267
295
 
@@ -282,20 +310,23 @@ export async function runAuthenticatedRequest(params: {
282
310
  if (output) {
283
311
  if (opts.output) {
284
312
  writeFileSync(opts.output, output.bytes);
313
+ } else if (output.isBinary) {
314
+ process.stdout.write(output.bytes, exit);
315
+ return;
285
316
  } else {
286
317
  process.stdout.write(output.bytes);
287
- if (!output.isBinary) {
288
- process.stdout.write("\n");
289
- }
318
+ process.stdout.write("\n", exit);
319
+ return;
290
320
  }
291
321
  }
292
322
  } else if (opts.output) {
293
323
  writeFileSync(opts.output, Buffer.alloc(0));
294
324
  }
325
+ exit();
295
326
  } catch (err) {
296
327
  const message = err instanceof Error ? err.message : String(err);
297
- writeError(cmd, `${message}\n\n${params.diagnosticsHint}`);
298
328
  process.exitCode = 1;
329
+ writeError(cmd, `${message}\n\n${params.diagnosticsHint}`, exit);
299
330
  }
300
331
  }
301
332
 
@@ -238,11 +238,18 @@ export function registerPluginsCommand(program: Command): void {
238
238
  const platformEnabled =
239
239
  libs.catalogLocal.arePlatformFeaturesEnabled();
240
240
  if (platformEnabled) {
241
- const match = (
242
- await libs.catalogCache.getPluginCatalog(DEFAULT_PLUGIN_REF, {
243
- fetch: globalThis.fetch.bind(globalThis),
244
- })
245
- ).matches.find((candidate) => candidate.name === nameOrUrl);
241
+ let match;
242
+ try {
243
+ match = (
244
+ await libs.catalogCache.getPluginCatalog(
245
+ DEFAULT_PLUGIN_REF,
246
+ { fetch: globalThis.fetch.bind(globalThis) },
247
+ )
248
+ ).matches.find((candidate) => candidate.name === nameOrUrl);
249
+ } catch {
250
+ // The install endpoint remains authoritative and can succeed
251
+ // independently when catalog discovery is unavailable.
252
+ }
246
253
  if (match?.source.kind === "local") {
247
254
  result = await libs.installGitHub.installPlugin(
248
255
  {
@@ -35,6 +35,7 @@ interface ScheduleRecord {
35
35
  mode: string;
36
36
  status: string;
37
37
  routingIntent: string;
38
+ quiet: boolean;
38
39
  reuseConversation: boolean;
39
40
  wakeConversationId: string | null;
40
41
  sourceKey: string | null;
@@ -268,6 +269,7 @@ export function registerSchedulesCommand(program: Command): void {
268
269
  formatScheduleInferenceProfile(schedule.inferenceProfile),
269
270
  ],
270
271
  ["Routing intent", schedule.routingIntent],
272
+ ["Quiet", schedule.quiet ? "yes" : "no"],
271
273
  ["Reuse conversation", schedule.reuseConversation ? "yes" : "no"],
272
274
  ["Wake conversation", schedule.wakeConversationId ?? "—"],
273
275
  ["Retry count", String(schedule.retryCount)],
@@ -19,6 +19,7 @@ import {
19
19
  PluginInspectNotFoundError,
20
20
  } from "../inspect-plugin.js";
21
21
  import { computeFingerprint } from "../plugin-fingerprint.js";
22
+ import type { PluginCatalog } from "../search-plugins.js";
22
23
 
23
24
  const SHA_A = "a".repeat(40);
24
25
  const SHA_B = "b".repeat(40);
@@ -305,6 +306,59 @@ describe("inspectPlugin", () => {
305
306
  expect(result.remote?.commit).toBe(SHA_B);
306
307
  });
307
308
 
309
+ test("previews an uninstalled bundled provider offline without fetching GitHub", async () => {
310
+ const savedDisablePlatform = process.env.VELLUM_DISABLE_PLATFORM;
311
+ const savedIsPlatform = process.env.IS_PLATFORM;
312
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
313
+ delete process.env.IS_PLATFORM;
314
+ let fetchCalls = 0;
315
+ const fetch = (async () => {
316
+ fetchCalls += 1;
317
+ throw new Error("offline");
318
+ }) as FetchLike;
319
+ const localCatalog: PluginCatalog = {
320
+ ref: "bundled",
321
+ matches: [
322
+ {
323
+ name: "fathom",
324
+ path: "local:plugins/mcp-catalog/fathom@1.0.0",
325
+ category: "productivity",
326
+ source: {
327
+ kind: "local",
328
+ path: "plugins/mcp-catalog/fathom",
329
+ version: "1.0.0",
330
+ },
331
+ },
332
+ ],
333
+ };
334
+
335
+ try {
336
+ const result = await inspectPlugin(
337
+ { name: "fathom" },
338
+ { fetch, workspacePluginsDir: workspace, localCatalog },
339
+ );
340
+
341
+ expect(result.status).toBe("not-installed");
342
+ expect(result.remote).toMatchObject({
343
+ kind: "local",
344
+ path: "plugins/mcp-catalog/fathom",
345
+ version: "1.0.0",
346
+ });
347
+ expect(fetchCalls).toBe(0);
348
+ } finally {
349
+ if (savedDisablePlatform === undefined) {
350
+ delete process.env.VELLUM_DISABLE_PLATFORM;
351
+ } else {
352
+ process.env.VELLUM_DISABLE_PLATFORM = savedDisablePlatform;
353
+ }
354
+ if (savedIsPlatform === undefined) {
355
+ delete process.env.IS_PLATFORM;
356
+ } else {
357
+ process.env.IS_PLATFORM = savedIsPlatform;
358
+ }
359
+ }
360
+ });
361
+
308
362
  test("reports remote-unavailable when the marketplace cannot be reached", async () => {
309
363
  // GIVEN an installed plugin and a failing marketplace fetch
310
364
  installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
@@ -39,6 +39,7 @@ import {
39
39
  resolveTreeRefPath,
40
40
  sanitizePluginName,
41
41
  } from "../install-from-github.js";
42
+ import { readValidatedPluginIcon } from "../plugin-icon-file.js";
42
43
 
43
44
  const CANON_REPO = "vellum-ai/vellum-assistant";
44
45
  /** Synthetic host the fixtures use for Contents API `download_url`s. */
@@ -320,6 +321,46 @@ describe("installPlugin — install lifecycle", () => {
320
321
  ]);
321
322
  });
322
323
 
324
+ test("installs the reviewed Fathom package from the generated bundle", async () => {
325
+ const result = await installPlugin(
326
+ {
327
+ name: "fathom",
328
+ trustedSource: {
329
+ kind: "local",
330
+ path: "plugins/mcp-catalog/fathom",
331
+ version: "1.0.2",
332
+ },
333
+ },
334
+ {
335
+ fetch: (async () => {
336
+ throw new Error("local package install must not fetch");
337
+ }) as FetchLike,
338
+ runGit: unusedGitRunner,
339
+ workspacePluginsDir: pluginsDir,
340
+ },
341
+ );
342
+
343
+ expect(result).toMatchObject({
344
+ name: "fathom",
345
+ ref: "1.0.2",
346
+ commit: null,
347
+ });
348
+ expect(readValidatedPluginIcon(result.target).hasIcon).toBe(true);
349
+ expect(
350
+ readPluginMcpServers({ workspacePluginsDir: pluginsDir }).servers,
351
+ ).toEqual([
352
+ expect.objectContaining({
353
+ pluginName: "fathom",
354
+ serverKey: "fathom",
355
+ config: expect.objectContaining({
356
+ transport: expect.objectContaining({
357
+ url: "https://api.fathom.ai/mcp",
358
+ }),
359
+ }),
360
+ }),
361
+ ]);
362
+ });
363
+
323
364
  test("refuses to overwrite an existing install without --force", async () => {
324
365
  // GIVEN a plugin already installed at <pluginsDir>/caveman
325
366
  const target = join(pluginsDir, "caveman");