@vellumai/assistant 0.8.0 → 0.8.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 (692) hide show
  1. package/AGENTS.md +11 -0
  2. package/Dockerfile +5 -4
  3. package/README.md +2 -2
  4. package/docker-entrypoint.sh +16 -0
  5. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  6. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  7. package/eslint.config.mjs +12 -0
  8. package/knip.json +2 -1
  9. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  10. package/openapi.yaml +4847 -1698
  11. package/package.json +3 -1
  12. package/scripts/generate-openapi.ts +52 -4
  13. package/scripts/sync-llm-catalog.ts +165 -0
  14. package/scripts/sync-web-search-catalog.ts +107 -0
  15. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  17. package/src/__tests__/anthropic-provider.test.ts +92 -2
  18. package/src/__tests__/app-control-flow.test.ts +7 -0
  19. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  20. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  21. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  22. package/src/__tests__/btw-routes.test.ts +1 -0
  23. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  24. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  25. package/src/__tests__/channel-policy.test.ts +12 -0
  26. package/src/__tests__/checker.test.ts +89 -0
  27. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +35 -7
  28. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  29. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  30. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  31. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  32. package/src/__tests__/config-loader-platform-defaults.test.ts +77 -23
  33. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  34. package/src/__tests__/config-schema.test.ts +14 -3
  35. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  36. package/src/__tests__/config-set-route.test.ts +198 -0
  37. package/src/__tests__/config-watcher.test.ts +6 -0
  38. package/src/__tests__/contacts-tools.test.ts +51 -199
  39. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  40. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  41. package/src/__tests__/context-search-conversations-source.test.ts +42 -16
  42. package/src/__tests__/context-search-fanout.test.ts +20 -157
  43. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -3
  44. package/src/__tests__/context-search-types.test.ts +7 -2
  45. package/src/__tests__/context-window-manager.test.ts +389 -1
  46. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  47. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  48. package/src/__tests__/conversation-error.test.ts +38 -0
  49. package/src/__tests__/conversation-fork-crud.test.ts +241 -1
  50. package/src/__tests__/conversation-inference-profile-route.test.ts +14 -14
  51. package/src/__tests__/conversation-init.benchmark.test.ts +1 -0
  52. package/src/__tests__/conversation-lifecycle.test.ts +124 -0
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  54. package/src/__tests__/conversation-process-callsite.test.ts +21 -1
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +4 -4
  56. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  57. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  58. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  59. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  60. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  61. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  62. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  63. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  64. package/src/__tests__/external-plugin-loader.test.ts +458 -0
  65. package/src/__tests__/filing-service.test.ts +23 -3
  66. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  67. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  68. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  69. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -8
  70. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  71. package/src/__tests__/heartbeat-service.test.ts +50 -233
  72. package/src/__tests__/history-repair.test.ts +89 -0
  73. package/src/__tests__/host-app-control-proxy.test.ts +109 -1
  74. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  75. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  76. package/src/__tests__/host-browser-routes.test.ts +325 -33
  77. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  78. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  79. package/src/__tests__/inference-profile-reaper.test.ts +154 -0
  80. package/src/__tests__/inference-profile-session-handler.test.ts +398 -0
  81. package/src/__tests__/inference-profile-session-ipc.test.ts +236 -0
  82. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  83. package/src/__tests__/install-skill-routing.test.ts +2 -2
  84. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +15 -0
  85. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  86. package/src/__tests__/llm-catalog-parity.test.ts +146 -0
  87. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +188 -0
  88. package/src/__tests__/llm-request-log-source-factory.test.ts +124 -0
  89. package/src/__tests__/llm-resolver.test.ts +46 -0
  90. package/src/__tests__/managed-profile-guard.test.ts +131 -2
  91. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  92. package/src/__tests__/mcp-cli.test.ts +182 -220
  93. package/src/__tests__/mcp-health-check.test.ts +56 -27
  94. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  95. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  96. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  97. package/src/__tests__/oauth-cli.test.ts +38 -2009
  98. package/src/__tests__/oauth-commands-routes.test.ts +711 -0
  99. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  100. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +33 -12
  102. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  103. package/src/__tests__/plugin-bootstrap.test.ts +31 -2
  104. package/src/__tests__/plugin-route-contribution.test.ts +31 -3
  105. package/src/__tests__/plugin-tool-contribution.test.ts +31 -3
  106. package/src/__tests__/plugin-types.test.ts +13 -11
  107. package/src/__tests__/process-message-background-slack.test.ts +46 -0
  108. package/src/__tests__/profile-entry-status.test.ts +43 -0
  109. package/src/__tests__/provider-managed-proxy-integration.test.ts +12 -4
  110. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  111. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  112. package/src/__tests__/relay-server.test.ts +118 -0
  113. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  114. package/src/__tests__/schedule-retry.test.ts +56 -4
  115. package/src/__tests__/schedule-routes.test.ts +104 -0
  116. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  117. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  118. package/src/__tests__/scheduler-reuse-conversation.test.ts +161 -5
  119. package/src/__tests__/scheduler-wake.test.ts +0 -63
  120. package/src/__tests__/secret-allowlist.test.ts +1 -0
  121. package/src/__tests__/secret-routes-managed-proxy.test.ts +12 -4
  122. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  123. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  124. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  125. package/src/__tests__/skill-load-tool.test.ts +2 -4
  126. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  127. package/src/__tests__/suggestion-routes.test.ts +3 -3
  128. package/src/__tests__/sync-message-contract.test.ts +63 -0
  129. package/src/__tests__/task-scheduler.test.ts +88 -23
  130. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  131. package/src/__tests__/usage-cli.test.ts +11 -73
  132. package/src/__tests__/user-plugin-loader.test.ts +145 -0
  133. package/src/__tests__/vercel-config.test.ts +168 -0
  134. package/src/__tests__/web-search-catalog-parity.test.ts +86 -0
  135. package/src/__tests__/web-search.test.ts +303 -2
  136. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  137. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +58 -0
  138. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +53 -20
  139. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +191 -0
  140. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  141. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  142. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  143. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  144. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  145. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  146. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  147. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  148. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  149. package/src/acp/resolve-agent.test.ts +25 -0
  150. package/src/acp/resolve-agent.ts +13 -2
  151. package/src/acp/session-manager.ts +14 -0
  152. package/src/approvals/guardian-request-resolvers.ts +32 -87
  153. package/src/calls/relay-server.ts +35 -0
  154. package/src/calls/relay-setup-router.ts +36 -0
  155. package/src/calls/types.ts +1 -0
  156. package/src/calls/voice-session-bridge.ts +23 -4
  157. package/src/channels/config.ts +14 -1
  158. package/src/channels/types.ts +1 -0
  159. package/src/cli/AGENTS.md +164 -4
  160. package/src/cli/__tests__/notifications.test.ts +54 -0
  161. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  162. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  163. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  164. package/src/cli/commands/__tests__/changelog.test.ts +593 -0
  165. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  166. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  167. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  168. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  169. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  170. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  171. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  172. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  173. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  174. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  175. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  176. package/src/cli/commands/__tests__/status.test.ts +249 -0
  177. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  178. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  179. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  180. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  181. package/src/cli/commands/attachment.ts +8 -3
  182. package/src/cli/commands/audit.ts +95 -64
  183. package/src/cli/commands/auth.ts +61 -58
  184. package/src/cli/commands/avatar.ts +276 -390
  185. package/src/cli/commands/backup.ts +409 -505
  186. package/src/cli/commands/bash.ts +9 -5
  187. package/src/cli/commands/browser.ts +28 -9
  188. package/src/cli/commands/cache.ts +9 -4
  189. package/src/cli/commands/changelog.ts +414 -0
  190. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  191. package/src/cli/commands/clients.ts +8 -3
  192. package/src/cli/commands/completions.ts +9 -9
  193. package/src/cli/commands/config.ts +102 -72
  194. package/src/cli/commands/contacts.ts +575 -696
  195. package/src/cli/commands/conversations-defer.ts +17 -69
  196. package/src/cli/commands/conversations-import.ts +90 -253
  197. package/src/cli/commands/conversations.ts +346 -436
  198. package/src/cli/commands/credential-execution.ts +9 -6
  199. package/src/cli/commands/credentials.ts +456 -736
  200. package/src/cli/commands/domain.ts +128 -206
  201. package/src/cli/commands/email.ts +606 -794
  202. package/src/cli/commands/gateway.ts +8 -1
  203. package/src/cli/commands/image-generation.ts +157 -205
  204. package/src/cli/commands/inference-providers.ts +352 -0
  205. package/src/cli/commands/inference-session.ts +415 -0
  206. package/src/cli/commands/inference.ts +87 -65
  207. package/src/cli/commands/keys.ts +8 -3
  208. package/src/cli/commands/mcp.ts +103 -287
  209. package/src/cli/commands/memory-v2.ts +162 -516
  210. package/src/cli/commands/notifications.ts +33 -7
  211. package/src/cli/commands/oauth/apps.ts +292 -261
  212. package/src/cli/commands/oauth/connect.ts +176 -297
  213. package/src/cli/commands/oauth/disconnect.ts +16 -215
  214. package/src/cli/commands/oauth/index.ts +49 -45
  215. package/src/cli/commands/oauth/mode.ts +43 -199
  216. package/src/cli/commands/oauth/ping.ts +17 -125
  217. package/src/cli/commands/oauth/providers.ts +732 -921
  218. package/src/cli/commands/oauth/request.ts +60 -350
  219. package/src/cli/commands/oauth/shared.ts +11 -121
  220. package/src/cli/commands/oauth/status.ts +31 -121
  221. package/src/cli/commands/oauth/token.ts +13 -55
  222. package/src/cli/commands/pending.ts +19 -10
  223. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  224. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  225. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  226. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  227. package/src/cli/commands/platform/connect.ts +16 -80
  228. package/src/cli/commands/platform/disconnect.ts +14 -112
  229. package/src/cli/commands/platform/index.ts +177 -246
  230. package/src/cli/commands/routes.ts +153 -336
  231. package/src/cli/commands/sequence.ts +316 -360
  232. package/src/cli/commands/skills.ts +449 -671
  233. package/src/cli/commands/status.ts +58 -37
  234. package/src/cli/commands/stt.ts +94 -262
  235. package/src/cli/commands/task.ts +14 -40
  236. package/src/cli/commands/trust.ts +8 -3
  237. package/src/cli/commands/tts.ts +162 -167
  238. package/src/cli/commands/ui.ts +35 -42
  239. package/src/cli/commands/usage.ts +188 -126
  240. package/src/cli/commands/watchers.ts +8 -3
  241. package/src/cli/commands/webhooks.ts +99 -193
  242. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  243. package/src/cli/lib/daemon-credential-client.ts +4 -5
  244. package/src/cli/lib/nested-value.ts +44 -0
  245. package/src/cli/lib/open-browser.ts +36 -0
  246. package/src/cli/lib/register-command.ts +19 -0
  247. package/src/cli/lib/time-ago.ts +34 -0
  248. package/src/cli/program.ts +2 -4
  249. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  250. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  251. package/src/cli/utils/conversation-id.ts +30 -0
  252. package/src/cli/utils/parse-duration.ts +41 -0
  253. package/src/config/acp-defaults.test.ts +5 -1
  254. package/src/config/acp-defaults.ts +11 -4
  255. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  256. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  258. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  259. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  260. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  261. package/src/config/bundled-tool-registry.ts +0 -2
  262. package/src/config/feature-flag-registry.json +16 -0
  263. package/src/config/llm-resolver.ts +16 -1
  264. package/src/config/loader.ts +76 -14
  265. package/src/config/raw-config-utils.ts +2 -30
  266. package/src/config/schema.ts +4 -0
  267. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  268. package/src/config/schemas/call-site-catalog.ts +29 -7
  269. package/src/config/schemas/llm-request-logs.ts +57 -0
  270. package/src/config/schemas/llm.ts +52 -2
  271. package/src/config/schemas/memory-retrospective.ts +48 -0
  272. package/src/config/schemas/memory-v2.ts +32 -1
  273. package/src/config/schemas/memory.ts +4 -0
  274. package/src/config/schemas/services.ts +15 -12
  275. package/src/config/seed-inference-profiles.ts +195 -134
  276. package/src/contacts/contact-store.ts +0 -61
  277. package/src/context/window-manager.ts +191 -5
  278. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +79 -0
  279. package/src/daemon/__tests__/conversation-tool-setup.test.ts +109 -4
  280. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  281. package/src/daemon/approval-generators.ts +23 -29
  282. package/src/daemon/config-watcher.ts +2 -0
  283. package/src/daemon/conversation-agent-loop-handlers.ts +24 -0
  284. package/src/daemon/conversation-agent-loop.ts +127 -97
  285. package/src/daemon/conversation-error.ts +21 -0
  286. package/src/daemon/conversation-lifecycle.ts +46 -5
  287. package/src/daemon/conversation-process.ts +36 -19
  288. package/src/daemon/conversation-runtime-assembly.ts +14 -5
  289. package/src/daemon/conversation-slash.ts +175 -23
  290. package/src/daemon/conversation-store.ts +17 -10
  291. package/src/daemon/conversation-surfaces.ts +76 -12
  292. package/src/daemon/conversation-tool-setup.ts +24 -14
  293. package/src/daemon/conversation.ts +48 -9
  294. package/src/daemon/external-plugins-bootstrap.ts +18 -8
  295. package/src/daemon/guardian-action-generators.ts +7 -22
  296. package/src/daemon/handlers/config-model.ts +8 -126
  297. package/src/daemon/handlers/config-slack-channel.ts +10 -7
  298. package/src/daemon/handlers/config-vercel.ts +3 -1
  299. package/src/daemon/handlers/skills.ts +84 -5
  300. package/src/daemon/history-repair.ts +33 -6
  301. package/src/daemon/host-app-control-proxy.ts +44 -19
  302. package/src/daemon/host-bash-proxy.ts +85 -158
  303. package/src/daemon/host-browser-proxy.ts +96 -35
  304. package/src/daemon/host-proxy-base.ts +13 -1
  305. package/src/daemon/host-proxy-preactivation.ts +25 -1
  306. package/src/daemon/identity-helpers.ts +19 -0
  307. package/src/daemon/lifecycle.ts +42 -43
  308. package/src/daemon/meet-host-supervisor.ts +15 -15
  309. package/src/daemon/memory-v2-startup.ts +9 -2
  310. package/src/daemon/message-protocol.ts +6 -0
  311. package/src/daemon/message-types/bookmarks.ts +18 -0
  312. package/src/daemon/message-types/conversations.ts +12 -9
  313. package/src/daemon/message-types/messages.ts +9 -1
  314. package/src/daemon/message-types/sync.ts +60 -0
  315. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  316. package/src/daemon/pkb-reminder-builder.ts +21 -7
  317. package/src/daemon/process-message.ts +56 -23
  318. package/src/daemon/server.ts +23 -18
  319. package/src/daemon/shutdown-handlers.ts +0 -2
  320. package/src/daemon/tool-setup-types.ts +9 -0
  321. package/src/daemon/tool-side-effects.ts +6 -4
  322. package/src/daemon/wake-target-adapter.ts +11 -0
  323. package/src/export/transcript-formatter.ts +61 -2
  324. package/src/filing/filing-service.ts +40 -53
  325. package/src/heartbeat/__tests__/heartbeat-service.test.ts +359 -0
  326. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  327. package/src/heartbeat/heartbeat-service.ts +148 -127
  328. package/src/home/__tests__/feed-types.test.ts +63 -131
  329. package/src/home/__tests__/feed-writer.test.ts +77 -278
  330. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  331. package/src/home/feed-types.ts +19 -73
  332. package/src/home/feed-writer.ts +25 -156
  333. package/src/home/post-connect-feed.ts +1 -3
  334. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  335. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  336. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  337. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  338. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  339. package/src/ipc/assistant-server.ts +55 -6
  340. package/src/ipc/cli-client.ts +370 -50
  341. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  342. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  343. package/src/ipc/skill-routes/events.ts +30 -3
  344. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  345. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  346. package/src/live-voice/live-voice-session-manager.ts +11 -4
  347. package/src/live-voice/live-voice-session.ts +14 -6
  348. package/src/memory/__tests__/bookmark-crud.test.ts +258 -0
  349. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  350. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  351. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  352. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  353. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  354. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +213 -0
  355. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  356. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  357. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  358. package/src/memory/bookmark-crud.ts +179 -0
  359. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  360. package/src/memory/context-search/agent-protocol.ts +5 -1
  361. package/src/memory/context-search/agent-runner.ts +60 -85
  362. package/src/memory/context-search/limits.ts +1 -4
  363. package/src/memory/context-search/search.ts +23 -113
  364. package/src/memory/context-search/sources/conversations.ts +18 -6
  365. package/src/memory/context-search/sources/memory-v2.ts +39 -14
  366. package/src/memory/context-search/sources/memory.ts +7 -0
  367. package/src/memory/context-search/sources/workspace.ts +13 -10
  368. package/src/memory/context-search/types.ts +1 -1
  369. package/src/memory/conversation-bootstrap.ts +11 -0
  370. package/src/memory/conversation-crud.ts +312 -10
  371. package/src/memory/conversation-queries.ts +9 -5
  372. package/src/memory/conversation-title-service.ts +1 -0
  373. package/src/memory/conversation-types.ts +16 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +2 -1
  376. package/src/memory/embedding-runtime-manager.ts +1 -2
  377. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  378. package/src/memory/graph/conversation-graph-memory.ts +76 -5
  379. package/src/memory/graph/extraction.ts +4 -0
  380. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  381. package/src/memory/graph/tool-handlers.ts +17 -7
  382. package/src/memory/graph/tools.ts +44 -5
  383. package/src/memory/indexer.ts +17 -0
  384. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +13 -15
  385. package/src/memory/jobs/embed-concept-page.ts +45 -9
  386. package/src/memory/jobs-store.ts +51 -1
  387. package/src/memory/jobs-worker.ts +52 -3
  388. package/src/memory/llm-request-log-source-clickhouse.ts +317 -0
  389. package/src/memory/llm-request-log-source-local.ts +26 -0
  390. package/src/memory/llm-request-log-source.ts +97 -0
  391. package/src/memory/llm-request-log-store.ts +1 -1
  392. package/src/memory/memory-retrospective-constants.ts +13 -0
  393. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  394. package/src/memory/memory-retrospective-job.ts +351 -0
  395. package/src/memory/memory-retrospective-startup-cleanup.ts +108 -0
  396. package/src/memory/memory-retrospective-state.ts +162 -0
  397. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  398. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  399. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  400. package/src/memory/message-content.ts +38 -1
  401. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  402. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  403. package/src/memory/migrations/229-delete-private-conversations.test.ts +70 -1
  404. package/src/memory/migrations/229-delete-private-conversations.ts +12 -0
  405. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  406. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  407. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  408. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  409. package/src/memory/migrations/243-provider-connections.ts +68 -0
  410. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  411. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  412. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  413. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  414. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  415. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  416. package/src/memory/migrations/index.ts +7 -0
  417. package/src/memory/published-pages-store.ts +16 -0
  418. package/src/memory/schema/bookmarks.ts +38 -0
  419. package/src/memory/schema/conversations.ts +2 -0
  420. package/src/memory/schema/index.ts +2 -0
  421. package/src/memory/schema/inference.ts +29 -0
  422. package/src/memory/schema/memory-core.ts +9 -0
  423. package/src/memory/search/semantic.ts +1 -4
  424. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  425. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  426. package/src/memory/v2/__tests__/activation.test.ts +11 -4
  427. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  428. package/src/memory/v2/__tests__/consolidation-job.test.ts +123 -135
  429. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  430. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  431. package/src/memory/v2/__tests__/injection.test.ts +628 -10
  432. package/src/memory/v2/__tests__/migration.test.ts +7 -3
  433. package/src/memory/v2/__tests__/page-index.test.ts +277 -0
  434. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  435. package/src/memory/v2/__tests__/prompts-router.test.ts +257 -0
  436. package/src/memory/v2/__tests__/qdrant.test.ts +72 -0
  437. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  438. package/src/memory/v2/__tests__/router.test.ts +516 -0
  439. package/src/memory/v2/__tests__/sim.test.ts +45 -1
  440. package/src/memory/v2/__tests__/skill-store.test.ts +58 -3
  441. package/src/memory/v2/__tests__/static-context.test.ts +7 -22
  442. package/src/memory/v2/__tests__/sweep-job.test.ts +95 -0
  443. package/src/memory/v2/activation-store.ts +34 -5
  444. package/src/memory/v2/activation.ts +40 -27
  445. package/src/memory/v2/backfill-jobs.ts +17 -84
  446. package/src/memory/v2/consolidation-job.ts +85 -78
  447. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  448. package/src/memory/v2/injection.ts +440 -109
  449. package/src/memory/v2/migration.ts +117 -20
  450. package/src/memory/v2/page-index.ts +191 -0
  451. package/src/memory/v2/page-store.ts +3 -0
  452. package/src/memory/v2/prompts/consolidation.ts +9 -7
  453. package/src/memory/v2/prompts/router.ts +192 -0
  454. package/src/memory/v2/qdrant.ts +100 -87
  455. package/src/memory/v2/reranker.ts +14 -7
  456. package/src/memory/v2/router.ts +322 -0
  457. package/src/memory/v2/sim.ts +25 -12
  458. package/src/memory/v2/skill-store.ts +118 -29
  459. package/src/memory/v2/static-context.ts +16 -9
  460. package/src/memory/v2/sweep-job.ts +122 -96
  461. package/src/memory/v2/types.ts +10 -6
  462. package/src/memory/validation.ts +13 -0
  463. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  464. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  465. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  466. package/src/notifications/adapters/platform.ts +171 -0
  467. package/src/notifications/conversation-pairing.ts +2 -2
  468. package/src/notifications/copy-composer.ts +15 -0
  469. package/src/notifications/destination-resolver.ts +21 -0
  470. package/src/notifications/emit-signal.ts +28 -1
  471. package/src/notifications/home-feed-side-effect.ts +111 -0
  472. package/src/notifications/signal.ts +5 -0
  473. package/src/permissions/checker.ts +12 -0
  474. package/src/permissions/ipc-risk-types.ts +2 -0
  475. package/src/plugin-api/index.ts +13 -0
  476. package/src/plugin-api/package.json +12 -0
  477. package/src/plugin-api/types.ts +62 -0
  478. package/src/plugins/defaults/injectors.ts +19 -3
  479. package/src/plugins/external-plugin-loader.ts +294 -0
  480. package/src/plugins/types.ts +46 -30
  481. package/src/plugins/user-loader.ts +64 -41
  482. package/src/proactive-artifact/job.test.ts +12 -4
  483. package/src/proactive-artifact/job.ts +4 -0
  484. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  485. package/src/proactive-artifact/trigger-state.ts +4 -0
  486. package/src/prompts/__tests__/system-prompt.test.ts +105 -0
  487. package/src/prompts/system-prompt.ts +22 -1
  488. package/src/prompts/update-bulletin-job.ts +61 -73
  489. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  490. package/src/providers/__tests__/inference.test.ts +288 -0
  491. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  492. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  493. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  494. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  495. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  496. package/src/providers/anthropic/client.ts +95 -26
  497. package/src/providers/call-site-routing.ts +94 -16
  498. package/src/providers/connection-resolution.ts +163 -0
  499. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  500. package/src/providers/inference/adapter-factory.ts +173 -0
  501. package/src/providers/inference/auth.ts +112 -0
  502. package/src/providers/inference/backfill.ts +196 -0
  503. package/src/providers/inference/connections.ts +356 -0
  504. package/src/providers/inference/resolve-auth.ts +65 -0
  505. package/src/providers/model-catalog.ts +104 -6
  506. package/src/providers/openai/responses-provider.ts +4 -2
  507. package/src/providers/provider-env-vars.ts +17 -7
  508. package/src/providers/provider-secret-catalog.ts +49 -30
  509. package/src/providers/provider-send-message.ts +41 -20
  510. package/src/providers/registry.ts +143 -159
  511. package/src/providers/retry.ts +18 -10
  512. package/src/providers/search-provider-catalog.ts +121 -0
  513. package/src/runtime/AGENTS.md +18 -5
  514. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  515. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  516. package/src/runtime/actor-trust-resolver.ts +32 -10
  517. package/src/runtime/agent-wake.ts +35 -6
  518. package/src/runtime/assistant-event-hub.ts +3 -85
  519. package/src/runtime/auth/route-policy.ts +303 -8
  520. package/src/runtime/auth/same-actor.ts +2 -0
  521. package/src/runtime/background-job-runner.ts +339 -0
  522. package/src/runtime/btw-sidechain.ts +1 -0
  523. package/src/runtime/http-router.ts +36 -1
  524. package/src/runtime/http-server.ts +31 -5
  525. package/src/runtime/http-types.ts +2 -0
  526. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  527. package/src/runtime/middleware/request-logger.ts +62 -1
  528. package/src/runtime/pre-first-message-gate.ts +83 -0
  529. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  530. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +251 -0
  531. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  532. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +315 -0
  533. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +189 -0
  534. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  535. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  536. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +4 -4
  537. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  538. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  539. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  540. package/src/runtime/routes/acp-routes.ts +10 -8
  541. package/src/runtime/routes/app-management-routes.ts +228 -3
  542. package/src/runtime/routes/approval-routes.ts +0 -18
  543. package/src/runtime/routes/audit-routes.ts +43 -0
  544. package/src/runtime/routes/auth-routes.ts +72 -0
  545. package/src/runtime/routes/avatar-routes.ts +273 -20
  546. package/src/runtime/routes/backup-routes.ts +406 -2
  547. package/src/runtime/routes/bookmark-routes.ts +154 -0
  548. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  549. package/src/runtime/routes/contact-routes.ts +0 -160
  550. package/src/runtime/routes/conversation-cli-routes.ts +192 -0
  551. package/src/runtime/routes/conversation-management-routes.ts +30 -43
  552. package/src/runtime/routes/conversation-query-routes.ts +334 -86
  553. package/src/runtime/routes/conversation-routes.ts +31 -10
  554. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  555. package/src/runtime/routes/credential-routes.ts +540 -0
  556. package/src/runtime/routes/debug-routes.ts +2 -2
  557. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  558. package/src/runtime/routes/domain-routes.ts +167 -0
  559. package/src/runtime/routes/email-routes.ts +603 -0
  560. package/src/runtime/routes/errors.ts +2 -2
  561. package/src/runtime/routes/events-routes.ts +192 -0
  562. package/src/runtime/routes/home-feed-routes.ts +6 -78
  563. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  564. package/src/runtime/routes/host-browser-routes.ts +103 -22
  565. package/src/runtime/routes/http-adapter.ts +2 -0
  566. package/src/runtime/routes/identity-routes.ts +5 -0
  567. package/src/runtime/routes/image-generation-routes.ts +99 -0
  568. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +137 -1
  569. package/src/runtime/routes/inbound-stages/background-dispatch.ts +87 -7
  570. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  571. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -4
  572. package/src/runtime/routes/index.ts +36 -0
  573. package/src/runtime/routes/inference-profile-session-handler.ts +312 -0
  574. package/src/runtime/routes/inference-profile-session-reaper.ts +98 -0
  575. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  576. package/src/runtime/routes/inference-provider-connection-routes.ts +317 -0
  577. package/src/runtime/routes/inference-send-routes.ts +115 -0
  578. package/src/runtime/routes/integrations/twilio.ts +1 -0
  579. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  580. package/src/runtime/routes/memory-v2-routes.ts +13 -398
  581. package/src/runtime/routes/notification-routes.ts +2 -0
  582. package/src/runtime/routes/oauth-apps.ts +112 -7
  583. package/src/runtime/routes/oauth-commands-routes.ts +1007 -0
  584. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  585. package/src/runtime/routes/oauth-providers.ts +298 -8
  586. package/src/runtime/routes/platform-routes.ts +336 -0
  587. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  588. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  589. package/src/runtime/routes/playground/state.ts +2 -1
  590. package/src/runtime/routes/publish-routes.ts +221 -0
  591. package/src/runtime/routes/schedule-routes.ts +82 -0
  592. package/src/runtime/routes/sequence-routes.ts +291 -0
  593. package/src/runtime/routes/settings-routes.ts +2 -10
  594. package/src/runtime/routes/skills-routes.ts +31 -1
  595. package/src/runtime/routes/stt-routes.ts +240 -3
  596. package/src/runtime/routes/surface-action-routes.ts +43 -7
  597. package/src/runtime/routes/tts-routes.ts +67 -0
  598. package/src/runtime/routes/types.ts +32 -0
  599. package/src/runtime/routes/user-routes-cli.ts +243 -0
  600. package/src/runtime/routes/webhook-routes.ts +165 -0
  601. package/src/runtime/sync/resource-sync-events.ts +25 -0
  602. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  603. package/src/runtime/sync/sync-publisher.ts +21 -0
  604. package/src/schedule/scheduler.ts +200 -123
  605. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  606. package/src/security/secret-patterns.ts +3 -0
  607. package/src/sequence/engine.ts +38 -40
  608. package/src/subagent/manager.ts +20 -15
  609. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  610. package/src/tools/browser/browser-execution.ts +15 -4
  611. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  612. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  613. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  614. package/src/tools/browser/cdp-client/factory.ts +66 -5
  615. package/src/tools/browser/runtime-check.ts +77 -0
  616. package/src/tools/memory/register.test.ts +3 -3
  617. package/src/tools/memory/register.ts +9 -1
  618. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  619. package/src/tools/network/web-search.ts +280 -37
  620. package/src/tools/permission-checker.ts +13 -5
  621. package/src/tools/subagent/spawn.ts +3 -3
  622. package/src/tools/terminal/shell.ts +44 -0
  623. package/src/usage/attribution.ts +3 -2
  624. package/src/util/pricing.ts +86 -160
  625. package/src/watcher/__tests__/engine.test.ts +301 -0
  626. package/src/watcher/constants.ts +7 -0
  627. package/src/watcher/engine.ts +90 -90
  628. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  629. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  630. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +28 -4
  631. package/src/workspace/migrations/069-seed-onboarding-threads.ts +8 -2
  632. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +104 -0
  633. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +93 -0
  634. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  635. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  636. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  637. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  638. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  639. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  640. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  641. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  642. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  643. package/src/workspace/migrations/registry.ts +22 -0
  644. package/src/workspace/migrations/runner.ts +13 -2
  645. package/src/workspace/migrations/types.ts +13 -3
  646. package/src/workspace/provider-commit-message-generator.ts +3 -2
  647. package/src/__tests__/context-search-pkb-source.test.ts +0 -498
  648. package/src/__tests__/credentials-cli.test.ts +0 -1225
  649. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  650. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  651. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  652. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  653. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  654. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  655. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  656. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  657. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  658. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  659. package/src/cli/commands/autonomy.ts +0 -365
  660. package/src/cli/commands/memory.ts +0 -424
  661. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -947
  662. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  663. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  664. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  665. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  666. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  667. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  668. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  669. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  670. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  671. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  672. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  673. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  674. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  675. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  676. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  677. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  678. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  679. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  680. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  681. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  682. package/src/home/assistant-feed-authoring.ts +0 -135
  683. package/src/home/emit-feed-event.ts +0 -169
  684. package/src/home/feed-scheduler.ts +0 -281
  685. package/src/home/platform-gmail-digest.ts +0 -163
  686. package/src/home/rewrite-command-preview.ts +0 -66
  687. package/src/home/rewrite-feed-title.ts +0 -58
  688. package/src/home/rollup-producer.ts +0 -426
  689. package/src/memory/admin.ts +0 -326
  690. package/src/memory/context-search/sources/pkb.ts +0 -476
  691. package/src/memory/graph/compaction.ts +0 -299
  692. /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
@@ -5,31 +5,62 @@ import {
5
5
  writeFileSync,
6
6
  } from "node:fs";
7
7
  import { basename, join } from "node:path";
8
- import type { Readable } from "node:stream";
9
- import { pipeline } from "node:stream/promises";
10
8
 
11
9
  import type { Command } from "commander";
12
10
 
13
11
  import { getAssistantDomain } from "../../config/env.js";
14
- import { markdownToEmailHtml } from "../../email/html-renderer.js";
15
- import { VellumPlatformClient } from "../../platform/client.js";
12
+ import {
13
+ cliIpcCall,
14
+ cliIpcCallStream,
15
+ exitFromIpcResult,
16
+ } from "../../ipc/cli-client.js";
17
+ import { registerCommand } from "../lib/register-command.js";
16
18
  import { getCliLogger } from "../logger.js";
17
19
  import { shouldOutputJson, writeOutput } from "../output.js";
18
20
 
19
21
  const log = getCliLogger("email");
20
22
 
23
+ /**
24
+ * Handle an IPC error in the email command. In --json mode, writes a
25
+ * `{"error": "..."}` envelope to stdout so callers can parse it. In all
26
+ * modes, sets a non-zero exit code without calling process.exit() so tests
27
+ * using runAssistantCommandFull can inspect the exit code after the call.
28
+ */
29
+ function handleEmailIpcError(
30
+ r: { ok: false; error?: string; statusCode?: number },
31
+ cmd: Command,
32
+ ): void {
33
+ const exitCode =
34
+ r.statusCode == null
35
+ ? 10
36
+ : r.statusCode >= 500
37
+ ? 3
38
+ : r.statusCode >= 400
39
+ ? 2
40
+ : 1;
41
+ if (shouldOutputJson(cmd)) {
42
+ process.stdout.write(
43
+ JSON.stringify({ error: r.error ?? "Unknown error" }) + "\n",
44
+ );
45
+ process.exitCode = exitCode;
46
+ return;
47
+ }
48
+ exitFromIpcResult(r, cmd);
49
+ }
50
+
21
51
  export function registerEmailCommand(program: Command): void {
22
52
  const domain = getAssistantDomain();
23
- const email = program
24
- .command("email")
25
- .description(
26
- `Get your own email address (@${domain}) — register, send, receive, and manage email natively`,
27
- )
28
- .option("--json", "Machine-readable compact JSON output");
29
-
30
- email.addHelpText(
31
- "after",
32
- `
53
+ registerCommand(program, {
54
+ name: "email",
55
+ transport: "ipc",
56
+ description: `Get your own email address (@${domain}) — register, send, receive, and manage email natively`,
57
+ build: (email) => {
58
+ // Keep the --json option at the email namespace level
59
+ email.option("--json", "Machine-readable compact JSON output");
60
+
61
+ email.addHelpText(
62
+ "after",
63
+ `
33
64
  Set up and manage this assistant's native email address on the Vellum
34
65
  platform. No third-party email provider or browser sign-up needed.
35
66
 
@@ -42,14 +73,14 @@ Examples:
42
73
  $ assistant email attachment msg_abc1 --list
43
74
  $ assistant email attachment msg_abc1 att_xyz1
44
75
  $ assistant email register mybot --json`,
45
- );
46
-
47
- email
48
- .command("register <username>")
49
- .description(`Register an @${domain} email address for this assistant`)
50
- .addHelpText(
51
- "after",
52
- `
76
+ );
77
+
78
+ email
79
+ .command("register <username>")
80
+ .description(`Register an @${domain} email address for this assistant`)
81
+ .addHelpText(
82
+ "after",
83
+ `
53
84
  Arguments:
54
85
  username The local part of the email address (e.g. "mybot" → mybot@${domain})
55
86
 
@@ -63,74 +94,32 @@ Examples:
63
94
 
64
95
  $ assistant email register support --json
65
96
  {"address":"support@${domain}","id":"...","created_at":"..."}`,
66
- )
67
- .action(async (username: string, _opts: unknown, cmd: Command) => {
68
- try {
69
- const client = await VellumPlatformClient.create();
70
- if (!client) {
71
- throw new Error(
72
- "Platform credentials not configured. Run: assistant platform connect",
73
- );
74
- }
75
- if (!client.platformAssistantId) {
76
- throw new Error(
77
- "Assistant ID not configured. Run: assistant platform connect",
78
- );
79
- }
80
-
81
- const response = await client.fetch(
82
- `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
83
- {
84
- method: "POST",
85
- headers: { "Content-Type": "application/json" },
86
- body: JSON.stringify({ username }),
87
- },
88
- );
97
+ )
98
+ .action(async (username: string, _opts: unknown, cmd: Command) => {
99
+ const r = await cliIpcCall<{
100
+ id: string;
101
+ address: string;
102
+ created_at: string;
103
+ }>("email_register", { body: { username } });
104
+ if (!r.ok)
105
+ return handleEmailIpcError(
106
+ { ok: false, error: r.error, statusCode: r.statusCode },
107
+ cmd,
108
+ );
109
+ if (shouldOutputJson(cmd)) {
110
+ writeOutput(cmd, r.result);
111
+ } else {
112
+ log.info(`✓ Registered ${r.result!.address}`);
113
+ }
114
+ });
89
115
 
90
- if (!response.ok) {
91
- const body = (await response.json().catch(() => ({}))) as Record<
92
- string,
93
- unknown
94
- >;
95
- const detail =
96
- body.detail ??
97
- (Array.isArray(body.username) ? body.username[0] : undefined) ??
98
- (Array.isArray(body.assistant_id)
99
- ? body.assistant_id[0]
100
- : undefined) ??
101
- `HTTP ${response.status}`;
102
- throw new Error(String(detail));
103
- }
104
-
105
- const data = (await response.json()) as {
106
- id: string;
107
- address: string;
108
- created_at: string;
109
- };
110
-
111
- if (shouldOutputJson(cmd)) {
112
- writeOutput(cmd, data);
113
- } else {
114
- log.info(`✓ Registered ${data.address}`);
115
- }
116
- } catch (err) {
117
- const message = err instanceof Error ? err.message : String(err);
118
- if (shouldOutputJson(cmd)) {
119
- writeOutput(cmd, { error: message });
120
- } else {
121
- log.error(`Error: ${message}`);
122
- }
123
- process.exitCode = 1;
124
- }
125
- });
126
-
127
- email
128
- .command("unregister")
129
- .description("Remove the email address registered for this assistant")
130
- .option("--confirm", "Skip confirmation prompt")
131
- .addHelpText(
132
- "after",
133
- `
116
+ email
117
+ .command("unregister")
118
+ .description("Remove the email address registered for this assistant")
119
+ .option("--confirm", "Skip confirmation prompt")
120
+ .addHelpText(
121
+ "after",
122
+ `
134
123
  Removes the email address currently registered for this assistant.
135
124
  The address is deactivated immediately — inbound email will no longer
136
125
  be delivered. The username enters a cooldown period and is not
@@ -146,93 +135,50 @@ Examples:
146
135
 
147
136
  $ assistant email unregister --json
148
137
  {"unregistered":"mybot@${domain}"}`,
149
- )
150
- .action(async (_opts: { confirm?: boolean }, cmd: Command) => {
151
- try {
152
- const client = await VellumPlatformClient.create();
153
- if (!client) {
154
- throw new Error(
155
- "Platform credentials not configured. Run: assistant platform connect",
156
- );
157
- }
158
- if (!client.platformAssistantId) {
159
- throw new Error(
160
- "Assistant ID not configured. Run: assistant platform connect",
161
- );
162
- }
163
-
164
- const listResponse = await client.fetch(
165
- `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
166
- );
167
-
168
- if (!listResponse.ok) {
169
- throw new Error(
170
- `Failed to list email addresses: HTTP ${listResponse.status}`,
138
+ )
139
+ .action(async (_opts: { confirm?: boolean }, cmd: Command) => {
140
+ if (!_opts.confirm && !shouldOutputJson(cmd)) {
141
+ const rl = await import("node:readline");
142
+ // We need to get the address to show in the prompt, but we can't
143
+ // know it without making an IPC call. Use a generic prompt here.
144
+ const iface = rl.createInterface({
145
+ input: process.stdin,
146
+ output: process.stderr,
147
+ });
148
+ const answer = await new Promise<string>((resolve) => {
149
+ iface.question(
150
+ `Remove registered email address? (y/N) `,
151
+ resolve,
152
+ );
153
+ });
154
+ iface.close();
155
+ if (answer.trim().toLowerCase() !== "y") {
156
+ log.info("Cancelled.");
157
+ return;
158
+ }
159
+ }
160
+ const r = await cliIpcCall<{ unregistered: string }>(
161
+ "email_unregister",
162
+ {},
171
163
  );
172
- }
173
-
174
- const listData = (await listResponse.json()) as {
175
- results: { id: string; address: string }[];
176
- };
177
-
178
- const addresses = listData.results ?? [];
179
- if (addresses.length === 0) {
180
- throw new Error("No email address registered for this assistant.");
181
- }
182
-
183
- const target = addresses[0];
184
-
185
- if (!_opts.confirm && !shouldOutputJson(cmd)) {
186
- const rl = await import("node:readline");
187
- const iface = rl.createInterface({
188
- input: process.stdin,
189
- output: process.stderr,
190
- });
191
- const answer = await new Promise<string>((resolve) => {
192
- iface.question(`Remove ${target.address}? (y/N) `, resolve);
193
- });
194
- iface.close();
195
- if (answer.trim().toLowerCase() !== "y") {
196
- log.info("Cancelled.");
197
- return;
164
+ if (!r.ok)
165
+ return handleEmailIpcError(
166
+ { ok: false, error: r.error, statusCode: r.statusCode },
167
+ cmd,
168
+ );
169
+ if (shouldOutputJson(cmd)) {
170
+ writeOutput(cmd, r.result);
171
+ } else {
172
+ log.info(`✓ Unregistered ${r.result!.unregistered}`);
198
173
  }
199
- }
200
-
201
- const deleteResponse = await client.fetch(
202
- `/v1/assistants/${client.platformAssistantId}/email-addresses/${target.id}/`,
203
- { method: "DELETE" },
204
- );
174
+ });
205
175
 
206
- if (!deleteResponse.ok) {
207
- const body = (await deleteResponse
208
- .json()
209
- .catch(() => ({}))) as Record<string, unknown>;
210
- const detail = body.detail ?? `HTTP ${deleteResponse.status}`;
211
- throw new Error(String(detail));
212
- }
213
-
214
- if (shouldOutputJson(cmd)) {
215
- writeOutput(cmd, { unregistered: target.address });
216
- } else {
217
- log.info(`✓ Unregistered ${target.address}`);
218
- }
219
- } catch (err) {
220
- const message = err instanceof Error ? err.message : String(err);
221
- if (shouldOutputJson(cmd)) {
222
- writeOutput(cmd, { error: message });
223
- } else {
224
- log.error(`Error: ${message}`);
225
- }
226
- process.exitCode = 1;
227
- }
228
- });
229
-
230
- email
231
- .command("status")
232
- .description("Show email address info and usage for this assistant")
233
- .addHelpText(
234
- "after",
235
- `
176
+ email
177
+ .command("status")
178
+ .description("Show email address info and usage for this assistant")
179
+ .addHelpText(
180
+ "after",
181
+ `
236
182
  Shows the email address registered for this assistant along with
237
183
  current usage and quota information from the platform.
238
184
 
@@ -247,111 +193,60 @@ Examples:
247
193
 
248
194
  $ assistant email status --json
249
195
  {"address":"hi@mybot.${domain}","status":"active","created_at":"2026-04-15T...","usage":{...}}`,
250
- )
251
- .action(async (_opts: unknown, cmd: Command) => {
252
- try {
253
- const client = await VellumPlatformClient.create();
254
- if (!client) {
255
- throw new Error(
256
- "Platform credentials not configured. Run: assistant platform connect",
257
- );
258
- }
259
- if (!client.platformAssistantId) {
260
- throw new Error(
261
- "Assistant ID not configured. Run: assistant platform connect",
262
- );
263
- }
264
-
265
- // 1. List addresses to find the registered one
266
- const listResponse = await client.fetch(
267
- `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
268
- );
269
-
270
- if (!listResponse.ok) {
271
- throw new Error(
272
- `Failed to list email addresses: HTTP ${listResponse.status}`,
273
- );
274
- }
275
-
276
- const listData = (await listResponse.json()) as {
277
- results: { id: string; address: string }[];
278
- };
279
-
280
- const addresses = listData.results ?? [];
281
- if (addresses.length === 0) {
282
- throw new Error(
283
- "No email address registered for this assistant. Run: assistant email register <username>",
284
- );
285
- }
286
-
287
- const target = addresses[0];
288
-
289
- // 2. Fetch status/usage for this address
290
- const statusResponse = await client.fetch(
291
- `/v1/assistants/${client.platformAssistantId}/email-addresses/${target.id}/status/`,
292
- );
293
-
294
- if (!statusResponse.ok) {
295
- const body = (await statusResponse
296
- .json()
297
- .catch(() => ({}))) as Record<string, unknown>;
298
- const detail = body.detail ?? `HTTP ${statusResponse.status}`;
299
- throw new Error(String(detail));
300
- }
301
-
302
- const statusData = (await statusResponse.json()) as {
303
- address: string;
304
- status: string;
305
- created_at: string;
306
- usage: {
307
- sent_today: number;
308
- daily_limit: number;
309
- received_today: number;
310
- sent_this_month: number;
311
- received_this_month: number;
312
- };
313
- };
314
-
315
- if (shouldOutputJson(cmd)) {
316
- writeOutput(cmd, statusData);
317
- } else {
318
- log.info(`Address: ${statusData.address}`);
319
- log.info(`Status: ${statusData.status}`);
320
- log.info(`Since: ${statusData.created_at.split("T")[0]}`);
321
- if (statusData.usage) {
322
- log.info(
323
- `Sent: ${statusData.usage.sent_today} / ${statusData.usage.daily_limit} (daily)`,
324
- );
325
- log.info(`Received: ${statusData.usage.received_today} (today)`);
326
- log.info(
327
- `Monthly: ${statusData.usage.sent_this_month} sent, ${statusData.usage.received_this_month} received`,
196
+ )
197
+ .action(async (_opts: unknown, cmd: Command) => {
198
+ const r = await cliIpcCall<{
199
+ address: string;
200
+ status: string;
201
+ created_at: string;
202
+ usage: {
203
+ sent_today: number;
204
+ daily_limit: number;
205
+ received_today: number;
206
+ sent_this_month: number;
207
+ received_this_month: number;
208
+ };
209
+ }>("email_status", {});
210
+ if (!r.ok)
211
+ return handleEmailIpcError(
212
+ { ok: false, error: r.error, statusCode: r.statusCode },
213
+ cmd,
328
214
  );
215
+ const statusData = r.result!;
216
+ if (shouldOutputJson(cmd)) {
217
+ writeOutput(cmd, statusData);
218
+ } else {
219
+ log.info(`Address: ${statusData.address}`);
220
+ log.info(`Status: ${statusData.status}`);
221
+ log.info(`Since: ${statusData.created_at.split("T")[0]}`);
222
+ if (statusData.usage) {
223
+ log.info(
224
+ `Sent: ${statusData.usage.sent_today} / ${statusData.usage.daily_limit} (daily)`,
225
+ );
226
+ log.info(`Received: ${statusData.usage.received_today} (today)`);
227
+ log.info(
228
+ `Monthly: ${statusData.usage.sent_this_month} sent, ${statusData.usage.received_this_month} received`,
229
+ );
230
+ }
329
231
  }
330
- }
331
- } catch (err) {
332
- const message = err instanceof Error ? err.message : String(err);
333
- if (shouldOutputJson(cmd)) {
334
- writeOutput(cmd, { error: message });
335
- } else {
336
- log.error(`Error: ${message}`);
337
- }
338
- process.exitCode = 1;
339
- }
340
- });
341
-
342
- email
343
- .command("list")
344
- .description("List received and sent emails for this assistant")
345
- .option(
346
- "-d, --direction <direction>",
347
- "Filter by direction: inbound, outbound, or all",
348
- "all",
349
- )
350
- .option("-l, --limit <count>", "Maximum number of results", "20")
351
- .option("--since <date>", "Only show messages since this date (ISO 8601)")
352
- .addHelpText(
353
- "after",
354
- `
232
+ });
233
+
234
+ email
235
+ .command("list")
236
+ .description("List received and sent emails for this assistant")
237
+ .option(
238
+ "-d, --direction <direction>",
239
+ "Filter by direction: inbound, outbound, or all",
240
+ "all",
241
+ )
242
+ .option("-l, --limit <count>", "Maximum number of results", "20")
243
+ .option(
244
+ "--since <date>",
245
+ "Only show messages since this date (ISO 8601)",
246
+ )
247
+ .addHelpText(
248
+ "after",
249
+ `
355
250
  Lists email messages for this assistant. Shows subject, from, to,
356
251
  direction, and timestamp for each message.
357
252
 
@@ -359,109 +254,79 @@ Examples:
359
254
  $ assistant email list
360
255
  $ assistant email list --direction inbound --limit 5
361
256
  $ assistant email list --since 2026-04-01 --json`,
362
- )
363
- .action(
364
- async (
365
- opts: {
366
- direction?: string;
367
- limit?: string;
368
- since?: string;
369
- },
370
- cmd: Command,
371
- ) => {
372
- try {
373
- const client = await VellumPlatformClient.create();
374
- if (!client) {
375
- throw new Error(
376
- "Platform credentials not configured. Run: assistant platform connect",
377
- );
378
- }
379
- if (!client.platformAssistantId) {
380
- throw new Error(
381
- "Assistant ID not configured. Run: assistant platform connect",
382
- );
383
- }
384
-
385
- const params = new URLSearchParams();
386
- if (opts.direction && opts.direction !== "all") {
387
- params.set("direction", opts.direction);
388
- }
389
- if (opts.limit) {
390
- params.set("limit", opts.limit);
391
- }
392
- if (opts.since) {
393
- params.set("since", opts.since);
394
- }
395
-
396
- const qs = params.toString();
397
- const path = `/v1/assistants/${client.platformAssistantId}/emails/${qs ? `?${qs}` : ""}`;
398
- const response = await client.fetch(path);
399
-
400
- if (!response.ok) {
401
- const body = (await response.json().catch(() => ({}))) as Record<
402
- string,
403
- unknown
404
- >;
405
- const detail = body.detail ?? `HTTP ${response.status}`;
406
- throw new Error(String(detail));
407
- }
408
-
409
- const data = (await response.json()) as {
410
- results: {
411
- id: string;
412
- direction: string;
413
- from_address: string;
414
- to_addresses: string[];
415
- subject: string;
416
- created_at: string;
417
- }[];
418
- count: number;
419
- };
257
+ )
258
+ .action(
259
+ async (
260
+ opts: {
261
+ direction?: string;
262
+ limit?: string;
263
+ since?: string;
264
+ },
265
+ cmd: Command,
266
+ ) => {
267
+ const params: Record<string, string> = {};
268
+ if (opts.direction && opts.direction !== "all") {
269
+ params.direction = opts.direction;
270
+ }
271
+ if (opts.limit) {
272
+ params.limit = opts.limit;
273
+ }
274
+ if (opts.since) {
275
+ params.since = opts.since;
276
+ }
420
277
 
421
- if (shouldOutputJson(cmd)) {
422
- writeOutput(cmd, data);
423
- } else {
424
- const messages = data.results ?? [];
425
- if (messages.length === 0) {
426
- log.info("No email messages found.");
278
+ const r = await cliIpcCall<{
279
+ results: {
280
+ id: string;
281
+ direction: string;
282
+ from_address: string;
283
+ to_addresses: string[];
284
+ subject: string;
285
+ created_at: string;
286
+ }[];
287
+ count: number;
288
+ }>("email_list", { queryParams: params });
289
+ if (!r.ok)
290
+ return handleEmailIpcError(
291
+ { ok: false, error: r.error, statusCode: r.statusCode },
292
+ cmd,
293
+ );
294
+ const data = r.result!;
295
+ if (shouldOutputJson(cmd)) {
296
+ writeOutput(cmd, data);
427
297
  } else {
428
- for (const msg of messages) {
429
- const dir = msg.direction === "inbound" ? "←" : "→";
430
- const to = Array.isArray(msg.to_addresses)
431
- ? msg.to_addresses.join(", ")
432
- : "";
433
- const date = new Date(msg.created_at).toLocaleString();
434
- log.info(
435
- `${dir} ${date} ${msg.from_address} → ${to} "${msg.subject || "(no subject)"}"`,
436
- );
298
+ const messages = data.results ?? [];
299
+ if (messages.length === 0) {
300
+ log.info("No email messages found.");
301
+ } else {
302
+ for (const msg of messages) {
303
+ const dir = msg.direction === "inbound" ? "←" : "→";
304
+ const to = Array.isArray(msg.to_addresses)
305
+ ? msg.to_addresses.join(", ")
306
+ : "";
307
+ const date = new Date(msg.created_at).toLocaleString();
308
+ log.info(
309
+ `${dir} ${date} ${msg.from_address} → ${to} "${msg.subject || "(no subject)"}"`,
310
+ );
311
+ }
312
+ log.info(`\n${data.count} total message(s)`);
437
313
  }
438
- log.info(`\n${data.count} total message(s)`);
439
314
  }
440
- }
441
- } catch (err) {
442
- const message = err instanceof Error ? err.message : String(err);
443
- if (shouldOutputJson(cmd)) {
444
- writeOutput(cmd, { error: message });
445
- } else {
446
- log.error(`Error: ${message}`);
447
- }
448
- process.exitCode = 1;
449
- }
450
- },
451
- );
315
+ },
316
+ );
452
317
 
453
- email
454
- .command("download <message-id>")
455
- .description("Download a specific email message")
456
- .option(
457
- "--format <type>",
458
- "Output format: text, html, json (default: text)",
459
- "text",
460
- )
461
- .option("-o, --output <path>", "Write to file instead of stdout")
462
- .addHelpText(
463
- "after",
464
- `
318
+ email
319
+ .command("download <message-id>")
320
+ .description("Download a specific email message")
321
+ .option(
322
+ "--format <type>",
323
+ "Output format: text, html, json (default: text)",
324
+ "text",
325
+ )
326
+ .option("-o, --output <path>", "Write to file instead of stdout")
327
+ .addHelpText(
328
+ "after",
329
+ `
465
330
  Arguments:
466
331
  message-id Email message ID (from \`assistant email list --json\`)
467
332
 
@@ -483,132 +348,113 @@ Examples:
483
348
 
484
349
  $ assistant email download msg_abc123 -o email.txt
485
350
  ✓ Saved to email.txt`,
486
- )
487
- .action(
488
- async (
489
- messageId: string,
490
- opts: {
491
- format?: string;
492
- output?: string;
493
- },
494
- cmd: Command,
495
- ) => {
496
- try {
497
- const client = await VellumPlatformClient.create();
498
- if (!client) {
499
- throw new Error(
500
- "Platform credentials not configured. Run: assistant platform connect",
501
- );
502
- }
503
- if (!client.platformAssistantId) {
504
- throw new Error(
505
- "Assistant ID not configured. Run: assistant platform connect",
506
- );
507
- }
508
-
509
- const response = await client.fetch(
510
- `/v1/assistants/${client.platformAssistantId}/emails/${messageId}/`,
511
- );
512
-
513
- if (!response.ok) {
514
- const body = (await response.json().catch(() => ({}))) as Record<
515
- string,
516
- unknown
517
- >;
518
- const detail = body.detail ?? `HTTP ${response.status}`;
519
- throw new Error(String(detail));
520
- }
521
-
522
- const msg = (await response.json()) as {
523
- id: string;
524
- direction: string;
525
- from_address: string;
526
- to_addresses: string[];
527
- subject: string;
528
- body_text: string;
529
- body_html: string;
530
- in_reply_to: string;
531
- references: string[];
532
- created_at: string;
533
- };
534
-
535
- const fmt = opts.format ?? "text";
536
-
537
- let content: string;
538
- if (fmt === "json" || shouldOutputJson(cmd)) {
539
- content = JSON.stringify(msg, null, 2) + "\n";
540
- } else if (fmt === "html") {
541
- if (!msg.body_html) {
542
- throw new Error("No HTML body available for this message.");
543
- }
544
- content = msg.body_html;
545
- } else {
546
- // text format: headers + body
547
- const to = Array.isArray(msg.to_addresses)
548
- ? msg.to_addresses.join(", ")
549
- : "";
550
- const date = new Date(msg.created_at).toLocaleString();
551
- const lines = [
552
- `From: ${msg.from_address}`,
553
- `To: ${to}`,
554
- `Subject: ${msg.subject || "(no subject)"}`,
555
- `Date: ${date}`,
556
- ];
557
- if (msg.in_reply_to) {
558
- lines.push(`In-Reply-To: ${msg.in_reply_to}`);
351
+ )
352
+ .action(
353
+ async (
354
+ messageId: string,
355
+ opts: {
356
+ format?: string;
357
+ output?: string;
358
+ },
359
+ cmd: Command,
360
+ ) => {
361
+ const r = await cliIpcCall<{
362
+ id: string;
363
+ direction: string;
364
+ from_address: string;
365
+ to_addresses: string[];
366
+ subject: string;
367
+ body_text: string;
368
+ body_html: string;
369
+ in_reply_to: string;
370
+ references: string[];
371
+ created_at: string;
372
+ }>("email_download", { queryParams: { messageId } });
373
+ if (!r.ok)
374
+ return handleEmailIpcError(
375
+ { ok: false, error: r.error, statusCode: r.statusCode },
376
+ cmd,
377
+ );
378
+ const msg = r.result!;
379
+
380
+ const fmt = opts.format ?? "text";
381
+
382
+ let content: string;
383
+ if (fmt === "json" || shouldOutputJson(cmd)) {
384
+ content = JSON.stringify(msg, null, 2) + "\n";
385
+ } else if (fmt === "html") {
386
+ if (!msg.body_html) {
387
+ log.error("No HTML body available for this message.");
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+ content = msg.body_html;
392
+ } else {
393
+ // text format: headers + body
394
+ const to = Array.isArray(msg.to_addresses)
395
+ ? msg.to_addresses.join(", ")
396
+ : "";
397
+ const date = new Date(msg.created_at).toLocaleString();
398
+ const lines = [
399
+ `From: ${msg.from_address}`,
400
+ `To: ${to}`,
401
+ `Subject: ${msg.subject || "(no subject)"}`,
402
+ `Date: ${date}`,
403
+ ];
404
+ if (msg.in_reply_to) {
405
+ lines.push(`In-Reply-To: ${msg.in_reply_to}`);
406
+ }
407
+ lines.push("", msg.body_text || "(no plain-text body)");
408
+ content = lines.join("\n") + "\n";
559
409
  }
560
- lines.push("", msg.body_text || "(no plain-text body)");
561
- content = lines.join("\n") + "\n";
562
- }
563
410
 
564
- if (opts.output) {
565
- writeFileSync(opts.output, content, "utf-8");
566
- if (!shouldOutputJson(cmd)) {
567
- log.info(`✓ Saved to ${opts.output}`);
411
+ if (opts.output) {
412
+ try {
413
+ writeFileSync(opts.output, content, "utf-8");
414
+ } catch (err) {
415
+ log.error(
416
+ `Failed to write --output ${opts.output}: ${err instanceof Error ? err.message : String(err)}`,
417
+ );
418
+ process.exitCode = 1;
419
+ return;
420
+ }
421
+ if (!shouldOutputJson(cmd)) {
422
+ log.info(`✓ Saved to ${opts.output}`);
423
+ } else {
424
+ writeOutput(cmd, { saved: opts.output, bytes: content.length });
425
+ }
568
426
  } else {
569
- writeOutput(cmd, { saved: opts.output, bytes: content.length });
427
+ process.stdout.write(content);
570
428
  }
571
- } else {
572
- process.stdout.write(content);
573
- }
574
- } catch (err) {
575
- const message = err instanceof Error ? err.message : String(err);
576
- if (shouldOutputJson(cmd)) {
577
- writeOutput(cmd, { error: message });
578
- } else {
579
- log.error(`Error: ${message}`);
580
- }
581
- process.exitCode = 1;
582
- }
583
- },
584
- );
429
+ },
430
+ );
585
431
 
586
- email
587
- .command("send <to...>")
588
- .description("Send an email from this assistant")
589
- .option("-s, --subject <text>", "Subject line")
590
- .option("-b, --body <text>", "Email body (plain text)")
591
- .option("-f, --file <path>", "Read body from file")
592
- .option("--html <path>", "HTML body file (optional)")
593
- .option(
594
- "--cc <address>",
595
- "CC recipient (repeatable)",
596
- (val: string, prev: string[]) => [...prev, val],
597
- [] as string[],
598
- )
599
- .option(
600
- "--bcc <address>",
601
- "BCC recipient (repeatable)",
602
- (val: string, prev: string[]) => [...prev, val],
603
- [] as string[],
604
- )
605
- .option(
606
- "--reply-to <email_id>",
607
- "Reply to an email by its ID (auto-resolves threading headers and subject)",
608
- )
609
- .addHelpText(
610
- "after",
611
- `
432
+ email
433
+ .command("send <to...>")
434
+ .description("Send an email from this assistant")
435
+ .option("-s, --subject <text>", "Subject line")
436
+ .option("-b, --body <text>", "Email body (plain text)")
437
+ .option("-f, --file <path>", "Read body from file")
438
+ .option("--html <path>", "HTML body file (optional)")
439
+ .option(
440
+ "--cc <address>",
441
+ "CC recipient (repeatable)",
442
+ (val: string, prev: string[]) => [...prev, val],
443
+ [] as string[],
444
+ )
445
+ .option(
446
+ "--bcc <address>",
447
+ "BCC recipient (repeatable)",
448
+ (val: string, prev: string[]) => [...prev, val],
449
+ [] as string[],
450
+ )
451
+ .option(
452
+ "--reply-to <email_id>",
453
+ "Reply to an email by its ID (auto-resolves threading headers and subject)",
454
+ )
455
+ .addHelpText(
456
+ "after",
457
+ `
612
458
  Arguments:
613
459
  to Recipient email address(es) — one or more
614
460
 
@@ -637,151 +483,107 @@ Examples:
637
483
 
638
484
  $ assistant email send user@example.com -s "Hello" -b "Hi" --json
639
485
  {"delivery_id":"abc123","status":"accepted"}`,
640
- )
641
- .action(
642
- async (
643
- to: string[],
644
- opts: {
645
- subject?: string;
646
- body?: string;
647
- file?: string;
648
- html?: string;
649
- cc?: string[];
650
- bcc?: string[];
651
- replyTo?: string;
652
- },
653
- cmd: Command,
654
- ) => {
655
- try {
656
- const client = await VellumPlatformClient.create();
657
- if (!client) {
658
- throw new Error(
659
- "Platform credentials not configured. Run: assistant platform connect",
660
- );
661
- }
662
- if (!client.platformAssistantId) {
663
- throw new Error(
664
- "Assistant ID not configured. Run: assistant platform connect",
665
- );
666
- }
667
-
668
- // 1. Resolve the assistant's registered email address (the "from").
669
- const listResponse = await client.fetch(
670
- `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
671
- );
672
-
673
- if (!listResponse.ok) {
674
- throw new Error(
675
- `Failed to list email addresses: HTTP ${listResponse.status}`,
676
- );
677
- }
678
-
679
- const listData = (await listResponse.json()) as {
680
- results: { id: string; address: string }[];
681
- };
486
+ )
487
+ .action(
488
+ async (
489
+ to: string[],
490
+ opts: {
491
+ subject?: string;
492
+ body?: string;
493
+ file?: string;
494
+ html?: string;
495
+ cc?: string[];
496
+ bcc?: string[];
497
+ replyTo?: string;
498
+ },
499
+ cmd: Command,
500
+ ) => {
501
+ // Resolve body text: --body > --file > stdin
502
+ let text = opts.body;
503
+ if (!text && opts.file) {
504
+ try {
505
+ text = readFileSync(opts.file, "utf-8");
506
+ } catch (err) {
507
+ log.error(
508
+ `Failed to read --file ${opts.file}: ${err instanceof Error ? err.message : String(err)}`,
509
+ );
510
+ process.exitCode = 1;
511
+ return;
512
+ }
513
+ }
514
+ if (!text && !process.stdin.isTTY) {
515
+ try {
516
+ text = readFileSync("/dev/stdin", "utf-8");
517
+ } catch (err) {
518
+ log.error(
519
+ `Failed to read body from stdin: ${err instanceof Error ? err.message : String(err)}`,
520
+ );
521
+ process.exitCode = 1;
522
+ return;
523
+ }
524
+ }
525
+ if (!text) {
526
+ log.error(
527
+ "Email body is required. Use --body, --file, or pipe via stdin.",
528
+ );
529
+ process.exitCode = 1;
530
+ return;
531
+ }
682
532
 
683
- const addresses = listData.results ?? [];
684
- if (addresses.length === 0) {
685
- throw new Error(
686
- "No email address registered for this assistant. Run: assistant email register <username>",
687
- );
688
- }
533
+ // Read HTML file if --html given; pass raw content to route
534
+ let html: string | undefined;
535
+ if (opts.html) {
536
+ try {
537
+ html = readFileSync(opts.html, "utf-8");
538
+ } catch (err) {
539
+ log.error(
540
+ `Failed to read --html ${opts.html}: ${err instanceof Error ? err.message : String(err)}`,
541
+ );
542
+ process.exitCode = 1;
543
+ return;
544
+ }
545
+ }
689
546
 
690
- const fromAddress = addresses[0].address;
547
+ const params: Record<string, unknown> = { to, text };
548
+ if (opts.subject) params.subject = opts.subject;
549
+ if (html) params.html = html;
550
+ if (opts.cc && opts.cc.length > 0) params.cc = opts.cc;
551
+ if (opts.bcc && opts.bcc.length > 0) params.bcc = opts.bcc;
552
+ if (opts.replyTo) params.reply_to = opts.replyTo;
691
553
 
692
- // 2. Resolve body text: --body > --file > stdin
693
- let text = opts.body;
694
- if (!text && opts.file) {
695
- text = readFileSync(opts.file, "utf-8");
696
- }
697
- if (!text && !process.stdin.isTTY) {
698
- text = readFileSync("/dev/stdin", "utf-8");
699
- }
700
- if (!text) {
701
- throw new Error(
702
- "Email body is required. Use --body, --file, or pipe via stdin.",
554
+ const r = await cliIpcCall<{ delivery_id: string; status: string }>(
555
+ "email_send",
556
+ { body: params },
703
557
  );
704
- }
705
-
706
- // 3. Resolve HTML body: explicit file > auto-generate from text
707
- let html: string | undefined;
708
- if (opts.html) {
709
- html = readFileSync(opts.html, "utf-8");
710
- } else {
711
- // Auto-generate HTML from the text body (markdown → email HTML).
712
- html = markdownToEmailHtml(text);
713
- }
714
-
715
- // 4. Build payload
716
- const payload: Record<string, unknown> = {
717
- to,
718
- from_address: fromAddress,
719
- text,
720
- };
721
- if (opts.subject) payload.subject = opts.subject;
722
- if (html) payload.html = html;
723
- if (opts.cc && opts.cc.length > 0) payload.cc = opts.cc;
724
- if (opts.bcc && opts.bcc.length > 0) payload.bcc = opts.bcc;
725
- if (opts.replyTo) payload.reply_to = opts.replyTo;
726
-
727
- // 5. Send via runtime proxy
728
- const response = await client.fetch("/v1/runtime-proxy/email/send/", {
729
- method: "POST",
730
- headers: { "Content-Type": "application/json" },
731
- body: JSON.stringify(payload),
732
- });
733
-
734
- if (!response.ok) {
735
- const body = (await response.json().catch(() => ({}))) as Record<
736
- string,
737
- unknown
738
- >;
739
- if (response.status === 402) {
740
- throw new Error(
741
- "Insufficient balance to send email. Add credits at https://platform.vellum.ai/billing",
558
+ if (!r.ok)
559
+ return handleEmailIpcError(
560
+ { ok: false, error: r.error, statusCode: r.statusCode },
561
+ cmd,
562
+ );
563
+ const data = r.result!;
564
+ if (shouldOutputJson(cmd)) {
565
+ writeOutput(cmd, data);
566
+ } else {
567
+ log.info(
568
+ `✓ Sent to ${to.join(", ")} (delivery_id: ${data.delivery_id})`,
742
569
  );
743
570
  }
744
- const detail = body.detail ?? `HTTP ${response.status}`;
745
- throw new Error(String(detail));
746
- }
747
-
748
- const data = (await response.json()) as {
749
- delivery_id: string;
750
- status: string;
751
- };
752
-
753
- if (shouldOutputJson(cmd)) {
754
- writeOutput(cmd, data);
755
- } else {
756
- log.info(
757
- `✓ Sent to ${to.join(", ")} (delivery_id: ${data.delivery_id})`,
758
- );
759
- }
760
- } catch (err) {
761
- const message = err instanceof Error ? err.message : String(err);
762
- if (shouldOutputJson(cmd)) {
763
- writeOutput(cmd, { error: message });
764
- } else {
765
- log.error(`Error: ${message}`);
766
- }
767
- process.exitCode = 1;
768
- }
769
- },
770
- );
571
+ },
572
+ );
771
573
 
772
- email
773
- .command("attachment <message-id> [attachment-id]")
774
- .description("Download email attachments")
775
- .option("--all", "Download all attachments for the message")
776
- .option(
777
- "-o, --output <dir>",
778
- "Output directory (default: current directory)",
779
- ".",
780
- )
781
- .option("--list", "List attachments without downloading")
782
- .addHelpText(
783
- "after",
784
- `
574
+ email
575
+ .command("attachment <message-id> [attachment-id]")
576
+ .description("Download email attachments")
577
+ .option("--all", "Download all attachments for the message")
578
+ .option(
579
+ "-o, --output <dir>",
580
+ "Output directory (default: current directory)",
581
+ ".",
582
+ )
583
+ .option("--list", "List attachments without downloading")
584
+ .addHelpText(
585
+ "after",
586
+ `
785
587
  Arguments:
786
588
  message-id Email message ID (from \`assistant email list --json\`)
787
589
  attachment-id Attachment ID (optional — required unless --all or --list)
@@ -796,162 +598,167 @@ $ assistant email attachment msg_abc1 att_xyz1 -o ./downloads/
796
598
  $ assistant email attachment msg_abc1 --all
797
599
  $ assistant email attachment msg_abc1 --all -o ./attachments/
798
600
  $ assistant email attachment msg_abc1 --list --json`,
799
- )
800
- .action(
801
- async (
802
- messageId: string,
803
- attachmentId: string | undefined,
804
- opts: {
805
- all?: boolean;
806
- output?: string;
807
- list?: boolean;
808
- },
809
- cmd: Command,
810
- ) => {
811
- try {
812
- const client = await VellumPlatformClient.create();
813
- if (!client) {
814
- throw new Error(
815
- "Platform credentials not configured. Run: assistant platform connect",
816
- );
817
- }
818
- if (!client.platformAssistantId) {
819
- throw new Error(
820
- "Assistant ID not configured. Run: assistant platform connect",
821
- );
822
- }
823
-
824
- const assistantId = client.platformAssistantId;
825
- const basePath = `/v1/assistants/${assistantId}/emails/${messageId}/attachments`;
826
-
827
- if (opts.list) {
828
- // List mode — show attachment metadata without downloading
829
- const response = await client.fetch(`${basePath}/`);
830
- if (!response.ok) {
831
- const body = (await response.json().catch(() => ({}))) as Record<
832
- string,
833
- unknown
834
- >;
835
- const detail = body.detail ?? `HTTP ${response.status}`;
836
- throw new Error(String(detail));
837
- }
838
-
839
- const data = (await response.json()) as {
840
- results: AttachmentMeta[];
841
- };
842
-
843
- if (shouldOutputJson(cmd)) {
844
- writeOutput(cmd, data);
845
- } else {
846
- const attachments = data.results ?? [];
847
- if (attachments.length === 0) {
848
- log.info("No attachments for this message.");
601
+ )
602
+ .action(
603
+ async (
604
+ messageId: string,
605
+ attachmentId: string | undefined,
606
+ opts: {
607
+ all?: boolean;
608
+ output?: string;
609
+ list?: boolean;
610
+ },
611
+ cmd: Command,
612
+ ) => {
613
+ if (opts.list) {
614
+ // List mode show attachment metadata without downloading
615
+ const r = await cliIpcCall<{ results: AttachmentMeta[] }>(
616
+ "email_attachment_list",
617
+ { queryParams: { messageId } },
618
+ );
619
+ if (!r.ok)
620
+ return handleEmailIpcError(
621
+ { ok: false, error: r.error, statusCode: r.statusCode },
622
+ cmd,
623
+ );
624
+ const data = r.result!;
625
+ if (shouldOutputJson(cmd)) {
626
+ writeOutput(cmd, data);
849
627
  } else {
850
- for (const att of attachments) {
851
- log.info(
852
- ` ${att.id} ${att.filename} (${att.content_type}, ${formatBytes(att.size_bytes)})`,
853
- );
628
+ const attachments = data.results ?? [];
629
+ if (attachments.length === 0) {
630
+ log.info("No attachments for this message.");
631
+ } else {
632
+ for (const att of attachments) {
633
+ log.info(
634
+ ` ${att.id} ${att.filename} (${att.content_type}, ${formatBytes(att.size_bytes)})`,
635
+ );
636
+ }
637
+ log.info(`\n${attachments.length} attachment(s)`);
854
638
  }
855
- log.info(`\n${attachments.length} attachment(s)`);
856
639
  }
857
- }
858
- return;
859
- }
860
-
861
- if (!opts.all && !attachmentId) {
862
- throw new Error(
863
- "Specify an attachment ID, or use --all to download all attachments. Use --list to see available attachments.",
864
- );
865
- }
866
-
867
- // Ensure output directory exists
868
- const outDir = opts.output ?? ".";
869
- mkdirSync(outDir, { recursive: true });
870
-
871
- if (opts.all) {
872
- // Download all attachments
873
- const listResponse = await client.fetch(`${basePath}/`);
874
- if (!listResponse.ok) {
875
- const body = (await listResponse
876
- .json()
877
- .catch(() => ({}))) as Record<string, unknown>;
878
- const detail = body.detail ?? `HTTP ${listResponse.status}`;
879
- throw new Error(String(detail));
640
+ return;
880
641
  }
881
642
 
882
- const listData = (await listResponse.json()) as {
883
- results: AttachmentMeta[];
884
- };
885
-
886
- const attachments = listData.results ?? [];
887
- if (attachments.length === 0) {
888
- throw new Error("No attachments for this message.");
889
- }
890
-
891
- const downloaded: { filename: string; size_bytes: number }[] = [];
892
- for (const att of attachments) {
893
- const dest = join(outDir, safeFilename(att.filename));
894
- await downloadAttachment(client, basePath, att.id, dest);
895
- downloaded.push({
896
- filename: att.filename,
897
- size_bytes: att.size_bytes,
898
- });
643
+ if (!opts.all && !attachmentId) {
644
+ log.error(
645
+ "Specify an attachment ID, or use --all to download all. Use --list to see available.",
646
+ );
647
+ process.exitCode = 1;
648
+ return;
899
649
  }
900
650
 
901
- if (shouldOutputJson(cmd)) {
902
- writeOutput(cmd, {
903
- downloaded: downloaded.length,
904
- directory: outDir,
905
- files: downloaded,
906
- });
907
- } else {
908
- log.info(
909
- `✓ Downloaded ${downloaded.length} attachment(s) to ${outDir}`,
651
+ // Ensure output directory exists and download attachment(s)
652
+ const outDir = opts.output ?? ".";
653
+ try {
654
+ mkdirSync(outDir, { recursive: true });
655
+ } catch (err) {
656
+ log.error(
657
+ `Failed to create output directory ${outDir}: ${err instanceof Error ? err.message : String(err)}`,
910
658
  );
911
- for (const f of downloaded) {
912
- log.info(` - ${f.filename} (${formatBytes(f.size_bytes)})`);
913
- }
914
- }
915
- } else {
916
- // Download single attachment — first get metadata for the filename
917
- const metaResponse = await client.fetch(
918
- `${basePath}/${attachmentId}/`,
919
- );
920
- if (!metaResponse.ok) {
921
- const body = (await metaResponse
922
- .json()
923
- .catch(() => ({}))) as Record<string, unknown>;
924
- const detail = body.detail ?? `HTTP ${metaResponse.status}`;
925
- throw new Error(String(detail));
659
+ process.exitCode = 1;
660
+ return;
926
661
  }
927
662
 
928
- const meta = (await metaResponse.json()) as AttachmentMeta;
929
- const dest = join(outDir, safeFilename(meta.filename));
930
- await downloadAttachment(client, basePath, meta.id, dest);
663
+ try {
664
+ if (opts.all) {
665
+ // Download all attachments — list first to get filenames
666
+ const listR = await cliIpcCall<{ results: AttachmentMeta[] }>(
667
+ "email_attachment_list",
668
+ { queryParams: { messageId } },
669
+ );
670
+ if (!listR.ok)
671
+ return handleEmailIpcError(
672
+ {
673
+ ok: false,
674
+ error: listR.error,
675
+ statusCode: listR.statusCode,
676
+ },
677
+ cmd,
678
+ );
679
+ const attachments = listR.result!.results ?? [];
680
+ if (attachments.length === 0) {
681
+ log.error("No attachments for this message.");
682
+ process.exitCode = 1;
683
+ return;
684
+ }
931
685
 
932
- if (shouldOutputJson(cmd)) {
933
- writeOutput(cmd, {
934
- filename: meta.filename,
935
- size_bytes: meta.size_bytes,
936
- saved: dest,
937
- });
938
- } else {
939
- log.info(
940
- `✓ Downloaded ${meta.filename} (${formatBytes(meta.size_bytes)})`,
686
+ const downloaded: { filename: string; size_bytes: number }[] =
687
+ [];
688
+ for (const att of attachments) {
689
+ const dest = join(outDir, safeFilename(att.filename));
690
+ await streamDownloadAttachment(att.id, messageId, dest);
691
+ downloaded.push({
692
+ filename: att.filename,
693
+ size_bytes: att.size_bytes,
694
+ });
695
+ }
696
+
697
+ if (shouldOutputJson(cmd)) {
698
+ writeOutput(cmd, {
699
+ downloaded: downloaded.length,
700
+ directory: outDir,
701
+ files: downloaded,
702
+ });
703
+ } else {
704
+ log.info(
705
+ `✓ Downloaded ${downloaded.length} attachment(s) to ${outDir}`,
706
+ );
707
+ for (const f of downloaded) {
708
+ log.info(
709
+ ` - ${f.filename} (${formatBytes(f.size_bytes)})`,
710
+ );
711
+ }
712
+ }
713
+ } else {
714
+ // Download single attachment — look up metadata from the list first
715
+ const listR = await cliIpcCall<{ results: AttachmentMeta[] }>(
716
+ "email_attachment_list",
717
+ { queryParams: { messageId } },
718
+ );
719
+ if (!listR.ok)
720
+ return handleEmailIpcError(
721
+ {
722
+ ok: false,
723
+ error: listR.error,
724
+ statusCode: listR.statusCode,
725
+ },
726
+ cmd,
727
+ );
728
+ const meta = (listR.result!.results ?? []).find(
729
+ (a) => a.id === attachmentId,
730
+ );
731
+ if (!meta) {
732
+ log.error(`Attachment not found: ${attachmentId}`);
733
+ process.exitCode = 2;
734
+ return;
735
+ }
736
+ const dest = join(outDir, safeFilename(meta.filename));
737
+ await streamDownloadAttachment(attachmentId!, messageId, dest);
738
+
739
+ if (shouldOutputJson(cmd)) {
740
+ writeOutput(cmd, {
741
+ filename: meta.filename,
742
+ size_bytes: meta.size_bytes,
743
+ saved: dest,
744
+ });
745
+ } else {
746
+ log.info(
747
+ `✓ Downloaded ${meta.filename} (${formatBytes(meta.size_bytes)})`,
748
+ );
749
+ }
750
+ }
751
+ } catch (err) {
752
+ log.error(
753
+ `Failed to download attachment: ${err instanceof Error ? err.message : String(err)}`,
941
754
  );
755
+ process.exitCode = 1;
756
+ return;
942
757
  }
943
- }
944
- } catch (err) {
945
- const message = err instanceof Error ? err.message : String(err);
946
- if (shouldOutputJson(cmd)) {
947
- writeOutput(cmd, { error: message });
948
- } else {
949
- log.error(`Error: ${message}`);
950
- }
951
- process.exitCode = 1;
952
- }
953
- },
954
- );
758
+ },
759
+ );
760
+ },
761
+ });
955
762
  }
956
763
 
957
764
  interface AttachmentMeta {
@@ -974,27 +781,32 @@ function safeFilename(name: string): string {
974
781
  return basename(name).replace(/[\x00/\\]/g, "_") || "attachment";
975
782
  }
976
783
 
977
- async function downloadAttachment(
978
- client: VellumPlatformClient,
979
- basePath: string,
784
+ async function streamDownloadAttachment(
980
785
  attachmentId: string,
786
+ messageId: string,
981
787
  dest: string,
982
788
  ): Promise<void> {
983
- const response = await client.fetch(`${basePath}/${attachmentId}/download/`);
984
-
985
- if (!response.ok) {
986
- const body = (await response.json().catch(() => ({}))) as Record<
987
- string,
988
- unknown
989
- >;
990
- const detail = body.detail ?? `HTTP ${response.status}`;
991
- throw new Error(`Failed to download attachment: ${detail}`);
992
- }
993
-
994
- if (!response.body) {
995
- throw new Error("Empty response body from download endpoint.");
996
- }
789
+ const r = await cliIpcCallStream("email_attachment_get", {
790
+ queryParams: { messageId, attachmentId },
791
+ });
792
+ if (!r.ok) throw new Error(r.error ?? "Stream failed");
997
793
 
998
794
  const fileStream = createWriteStream(dest);
999
- await pipeline(response.body as unknown as Readable, fileStream);
795
+ const reader = r.body.getReader();
796
+ try {
797
+ while (true) {
798
+ const { done, value } = await reader.read();
799
+ if (done) break;
800
+ await new Promise<void>((resolve, reject) =>
801
+ fileStream.write(value, (err) => (err ? reject(err) : resolve())),
802
+ );
803
+ }
804
+ await new Promise<void>((resolve, reject) =>
805
+ fileStream.close((err) => (err ? reject(err) : resolve())),
806
+ );
807
+ } catch (err) {
808
+ r.abort();
809
+ fileStream.destroy();
810
+ throw err;
811
+ }
1000
812
  }