@vellumai/assistant 0.7.3 → 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 (778) hide show
  1. package/AGENTS.md +11 -0
  2. package/ARCHITECTURE.md +29 -28
  3. package/Dockerfile +6 -4
  4. package/README.md +2 -2
  5. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  6. package/bun.lock +3 -0
  7. package/docker-entrypoint.sh +16 -0
  8. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  9. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  10. package/eslint.config.mjs +12 -0
  11. package/knip.json +3 -1
  12. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  13. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  14. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  15. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  16. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  17. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  19. package/openapi.yaml +4126 -959
  20. package/package.json +5 -1
  21. package/scripts/generate-openapi.ts +52 -4
  22. package/scripts/sync-llm-catalog.ts +165 -0
  23. package/scripts/sync-web-search-catalog.ts +107 -0
  24. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  25. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  26. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  27. package/src/__tests__/anthropic-provider.test.ts +92 -2
  28. package/src/__tests__/app-control-flow.test.ts +7 -0
  29. package/src/__tests__/approval-cascade.test.ts +8 -16
  30. package/src/__tests__/approval-routes-http.test.ts +6 -0
  31. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  32. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  33. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  35. package/src/__tests__/btw-routes.test.ts +1 -0
  36. package/src/__tests__/call-constants.test.ts +10 -1
  37. package/src/__tests__/call-controller.test.ts +127 -0
  38. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  39. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  40. package/src/__tests__/channel-policy.test.ts +12 -0
  41. package/src/__tests__/checker.test.ts +89 -0
  42. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +88 -30
  43. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  44. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  45. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  46. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  47. package/src/__tests__/config-loader-platform-defaults.test.ts +345 -8
  48. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  49. package/src/__tests__/config-schema.test.ts +14 -3
  50. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  51. package/src/__tests__/config-set-route.test.ts +198 -0
  52. package/src/__tests__/config-watcher.test.ts +6 -0
  53. package/src/__tests__/contacts-tools.test.ts +51 -199
  54. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  55. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  56. package/src/__tests__/context-search-conversations-source.test.ts +42 -16
  57. package/src/__tests__/context-search-fanout.test.ts +20 -157
  58. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  59. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -3
  60. package/src/__tests__/context-search-types.test.ts +7 -2
  61. package/src/__tests__/context-window-manager.test.ts +389 -1
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  65. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  67. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  68. package/src/__tests__/conversation-error.test.ts +38 -0
  69. package/src/__tests__/conversation-fork-crud.test.ts +241 -1
  70. package/src/__tests__/conversation-inference-profile-route.test.ts +14 -14
  71. package/src/__tests__/conversation-init.benchmark.test.ts +2 -1
  72. package/src/__tests__/conversation-lifecycle.test.ts +124 -0
  73. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  74. package/src/__tests__/conversation-process-callsite.test.ts +22 -7
  75. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  76. package/src/__tests__/conversation-runtime-assembly.test.ts +19 -10
  77. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  78. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  79. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  81. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  82. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  83. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  84. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  85. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  86. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  87. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  88. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  89. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  90. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  91. package/src/__tests__/external-plugin-loader.test.ts +458 -0
  92. package/src/__tests__/filing-service.test.ts +25 -22
  93. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  94. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  95. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  96. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -34
  97. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  98. package/src/__tests__/heartbeat-service.test.ts +50 -233
  99. package/src/__tests__/history-repair.test.ts +89 -0
  100. package/src/__tests__/host-app-control-proxy.test.ts +109 -1
  101. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  102. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  103. package/src/__tests__/host-browser-routes.test.ts +325 -33
  104. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  105. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  106. package/src/__tests__/inference-profile-reaper.test.ts +154 -0
  107. package/src/__tests__/inference-profile-session-handler.test.ts +398 -0
  108. package/src/__tests__/inference-profile-session-ipc.test.ts +236 -0
  109. package/src/__tests__/injector-chain.test.ts +24 -16
  110. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  111. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  112. package/src/__tests__/install-skill-routing.test.ts +2 -2
  113. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +169 -67
  114. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  115. package/src/__tests__/llm-catalog-parity.test.ts +146 -0
  116. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +188 -0
  117. package/src/__tests__/llm-request-log-source-factory.test.ts +124 -0
  118. package/src/__tests__/llm-resolver.test.ts +46 -0
  119. package/src/__tests__/managed-profile-guard.test.ts +131 -2
  120. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  121. package/src/__tests__/mcp-cli.test.ts +182 -220
  122. package/src/__tests__/mcp-health-check.test.ts +56 -27
  123. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  124. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  125. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  126. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  127. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  128. package/src/__tests__/oauth-cli.test.ts +38 -1888
  129. package/src/__tests__/oauth-commands-routes.test.ts +711 -0
  130. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  131. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  132. package/src/__tests__/openai-responses-cutover-guard.test.ts +33 -12
  133. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  134. package/src/__tests__/plugin-bootstrap.test.ts +31 -2
  135. package/src/__tests__/plugin-route-contribution.test.ts +31 -3
  136. package/src/__tests__/plugin-tool-contribution.test.ts +31 -3
  137. package/src/__tests__/plugin-types.test.ts +13 -11
  138. package/src/__tests__/process-message-background-slack.test.ts +46 -0
  139. package/src/__tests__/profile-entry-status.test.ts +43 -0
  140. package/src/__tests__/provider-managed-proxy-integration.test.ts +12 -4
  141. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  142. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  143. package/src/__tests__/relay-server.test.ts +164 -2
  144. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  145. package/src/__tests__/schedule-retry.test.ts +56 -4
  146. package/src/__tests__/schedule-routes.test.ts +104 -0
  147. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  148. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  149. package/src/__tests__/scheduler-reuse-conversation.test.ts +161 -5
  150. package/src/__tests__/scheduler-wake.test.ts +0 -63
  151. package/src/__tests__/secret-allowlist.test.ts +1 -0
  152. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  153. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  154. package/src/__tests__/secret-response-routing.test.ts +7 -5
  155. package/src/__tests__/secret-routes-managed-proxy.test.ts +12 -4
  156. package/src/__tests__/server-history-render.test.ts +82 -0
  157. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  158. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  159. package/src/__tests__/skill-include-graph.test.ts +31 -0
  160. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  161. package/src/__tests__/skill-load-tool.test.ts +42 -16
  162. package/src/__tests__/skills.test.ts +39 -0
  163. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  164. package/src/__tests__/suggestion-routes.test.ts +3 -3
  165. package/src/__tests__/sync-message-contract.test.ts +63 -0
  166. package/src/__tests__/task-scheduler.test.ts +88 -23
  167. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  168. package/src/__tests__/tool-executor.test.ts +155 -0
  169. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  170. package/src/__tests__/usage-cli.test.ts +11 -73
  171. package/src/__tests__/user-plugin-loader.test.ts +145 -0
  172. package/src/__tests__/vercel-config.test.ts +168 -0
  173. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  174. package/src/__tests__/web-search-catalog-parity.test.ts +86 -0
  175. package/src/__tests__/web-search.test.ts +303 -2
  176. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  177. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +58 -0
  178. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +153 -0
  179. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  180. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +191 -0
  181. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  182. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  183. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  184. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  185. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  186. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  187. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  188. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  189. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  190. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  191. package/src/acp/resolve-agent.test.ts +25 -0
  192. package/src/acp/resolve-agent.ts +13 -2
  193. package/src/acp/session-manager.ts +14 -0
  194. package/src/agent/loop.ts +11 -0
  195. package/src/approvals/guardian-decision-primitive.ts +0 -13
  196. package/src/approvals/guardian-request-resolvers.ts +19 -102
  197. package/src/calls/call-constants.ts +5 -8
  198. package/src/calls/call-controller.ts +130 -67
  199. package/src/calls/relay-server.ts +42 -1
  200. package/src/calls/relay-setup-router.ts +36 -0
  201. package/src/calls/types.ts +1 -0
  202. package/src/calls/voice-session-bridge.ts +24 -5
  203. package/src/channels/config.ts +14 -1
  204. package/src/channels/types.ts +1 -0
  205. package/src/cli/AGENTS.md +164 -4
  206. package/src/cli/__tests__/notifications.test.ts +54 -0
  207. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  208. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  209. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  210. package/src/cli/commands/__tests__/changelog.test.ts +593 -0
  211. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  212. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  213. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  214. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  215. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  216. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  217. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  218. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  219. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  220. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  221. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  222. package/src/cli/commands/__tests__/status.test.ts +249 -0
  223. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  224. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  225. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  226. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  227. package/src/cli/commands/attachment.ts +8 -3
  228. package/src/cli/commands/audit.ts +95 -64
  229. package/src/cli/commands/auth.ts +61 -58
  230. package/src/cli/commands/avatar.ts +276 -390
  231. package/src/cli/commands/backup.ts +409 -505
  232. package/src/cli/commands/bash.ts +9 -5
  233. package/src/cli/commands/browser.ts +28 -9
  234. package/src/cli/commands/cache.ts +9 -4
  235. package/src/cli/commands/changelog.ts +414 -0
  236. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  237. package/src/cli/commands/clients.ts +8 -3
  238. package/src/cli/commands/completions.ts +9 -9
  239. package/src/cli/commands/config.ts +102 -72
  240. package/src/cli/commands/contacts.ts +575 -696
  241. package/src/cli/commands/conversations-defer.ts +17 -69
  242. package/src/cli/commands/conversations-import.ts +90 -253
  243. package/src/cli/commands/conversations.ts +346 -436
  244. package/src/cli/commands/credential-execution.ts +9 -6
  245. package/src/cli/commands/credentials.ts +456 -736
  246. package/src/cli/commands/domain.ts +128 -206
  247. package/src/cli/commands/email.ts +606 -794
  248. package/src/cli/commands/gateway.ts +8 -1
  249. package/src/cli/commands/image-generation.ts +157 -205
  250. package/src/cli/commands/inference-providers.ts +352 -0
  251. package/src/cli/commands/inference-session.ts +415 -0
  252. package/src/cli/commands/inference.ts +87 -65
  253. package/src/cli/commands/keys.ts +8 -3
  254. package/src/cli/commands/mcp.ts +103 -287
  255. package/src/cli/commands/memory-v2.ts +163 -517
  256. package/src/cli/commands/notifications.ts +33 -7
  257. package/src/cli/commands/oauth/apps.ts +292 -261
  258. package/src/cli/commands/oauth/connect.ts +182 -345
  259. package/src/cli/commands/oauth/disconnect.ts +16 -215
  260. package/src/cli/commands/oauth/index.ts +49 -45
  261. package/src/cli/commands/oauth/mode.ts +43 -199
  262. package/src/cli/commands/oauth/ping.ts +17 -125
  263. package/src/cli/commands/oauth/providers.ts +732 -921
  264. package/src/cli/commands/oauth/request.ts +60 -350
  265. package/src/cli/commands/oauth/shared.ts +11 -121
  266. package/src/cli/commands/oauth/status.ts +31 -121
  267. package/src/cli/commands/oauth/token.ts +13 -55
  268. package/src/cli/commands/pending.ts +19 -10
  269. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  270. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  271. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  272. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  273. package/src/cli/commands/platform/connect.ts +16 -80
  274. package/src/cli/commands/platform/disconnect.ts +14 -112
  275. package/src/cli/commands/platform/index.ts +177 -246
  276. package/src/cli/commands/routes.ts +153 -336
  277. package/src/cli/commands/sequence.ts +316 -360
  278. package/src/cli/commands/skills.ts +449 -671
  279. package/src/cli/commands/status.ts +58 -37
  280. package/src/cli/commands/stt.ts +94 -262
  281. package/src/cli/commands/task.ts +14 -40
  282. package/src/cli/commands/trust.ts +8 -3
  283. package/src/cli/commands/tts.ts +162 -167
  284. package/src/cli/commands/ui.ts +35 -42
  285. package/src/cli/commands/usage.ts +188 -126
  286. package/src/cli/commands/watchers.ts +8 -3
  287. package/src/cli/commands/webhooks.ts +99 -193
  288. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  289. package/src/cli/lib/daemon-credential-client.ts +4 -5
  290. package/src/cli/lib/nested-value.ts +44 -0
  291. package/src/cli/lib/open-browser.ts +36 -0
  292. package/src/cli/lib/register-command.ts +19 -0
  293. package/src/cli/lib/time-ago.ts +34 -0
  294. package/src/cli/program.ts +2 -4
  295. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  296. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  297. package/src/cli/utils/conversation-id.ts +30 -0
  298. package/src/cli/utils/parse-duration.ts +41 -0
  299. package/src/config/acp-defaults.test.ts +5 -1
  300. package/src/config/acp-defaults.ts +11 -4
  301. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  303. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  304. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  305. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  306. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  307. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  308. package/src/config/bundled-tool-registry.ts +0 -2
  309. package/src/config/feature-flag-registry.json +17 -17
  310. package/src/config/llm-resolver.ts +16 -1
  311. package/src/config/loader.ts +148 -33
  312. package/src/config/raw-config-utils.ts +2 -30
  313. package/src/config/schema.ts +4 -0
  314. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  315. package/src/config/schemas/call-site-catalog.ts +29 -7
  316. package/src/config/schemas/llm-request-logs.ts +57 -0
  317. package/src/config/schemas/llm.ts +52 -2
  318. package/src/config/schemas/memory-retrospective.ts +48 -0
  319. package/src/config/schemas/memory-v2.ts +33 -2
  320. package/src/config/schemas/memory.ts +4 -0
  321. package/src/config/schemas/services.ts +15 -12
  322. package/src/config/seed-inference-profiles.ts +195 -134
  323. package/src/contacts/contact-store.ts +0 -61
  324. package/src/context/window-manager.ts +191 -5
  325. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +111 -0
  326. package/src/daemon/__tests__/conversation-tool-setup.test.ts +109 -4
  327. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  328. package/src/daemon/approval-generators.ts +23 -29
  329. package/src/daemon/config-watcher.ts +2 -0
  330. package/src/daemon/conversation-agent-loop-handlers.ts +56 -0
  331. package/src/daemon/conversation-agent-loop.ts +140 -107
  332. package/src/daemon/conversation-error.ts +21 -0
  333. package/src/daemon/conversation-lifecycle.ts +68 -13
  334. package/src/daemon/conversation-process.ts +36 -19
  335. package/src/daemon/conversation-runtime-assembly.ts +14 -5
  336. package/src/daemon/conversation-slash.ts +175 -23
  337. package/src/daemon/conversation-store.ts +17 -10
  338. package/src/daemon/conversation-surfaces.ts +92 -26
  339. package/src/daemon/conversation-tool-setup.ts +33 -19
  340. package/src/daemon/conversation.ts +49 -10
  341. package/src/daemon/external-plugins-bootstrap.ts +18 -8
  342. package/src/daemon/guardian-action-generators.ts +7 -22
  343. package/src/daemon/handlers/config-model.ts +8 -126
  344. package/src/daemon/handlers/config-slack-channel.ts +10 -7
  345. package/src/daemon/handlers/config-vercel.ts +3 -1
  346. package/src/daemon/handlers/shared.ts +26 -0
  347. package/src/daemon/handlers/skills.ts +84 -5
  348. package/src/daemon/history-repair.ts +33 -6
  349. package/src/daemon/host-app-control-proxy.ts +44 -19
  350. package/src/daemon/host-bash-proxy.ts +85 -158
  351. package/src/daemon/host-browser-proxy.ts +97 -36
  352. package/src/daemon/host-cu-proxy.ts +1 -1
  353. package/src/daemon/host-file-proxy.ts +1 -1
  354. package/src/daemon/host-proxy-base.ts +13 -1
  355. package/src/daemon/host-proxy-preactivation.ts +25 -1
  356. package/src/daemon/host-transfer-proxy.ts +2 -2
  357. package/src/daemon/identity-helpers.ts +19 -0
  358. package/src/daemon/lifecycle.ts +128 -114
  359. package/src/daemon/meet-host-supervisor.ts +15 -15
  360. package/src/daemon/memory-v2-startup.ts +62 -14
  361. package/src/daemon/message-protocol.ts +6 -0
  362. package/src/daemon/message-types/bookmarks.ts +18 -0
  363. package/src/daemon/message-types/conversations.ts +12 -9
  364. package/src/daemon/message-types/messages.ts +28 -2
  365. package/src/daemon/message-types/sync.ts +60 -0
  366. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  367. package/src/daemon/pkb-reminder-builder.ts +21 -7
  368. package/src/daemon/process-message.ts +56 -23
  369. package/src/daemon/server.ts +23 -18
  370. package/src/daemon/shutdown-handlers.ts +0 -2
  371. package/src/daemon/tool-setup-types.ts +9 -0
  372. package/src/daemon/tool-side-effects.ts +6 -4
  373. package/src/daemon/wake-target-adapter.ts +11 -0
  374. package/src/documents/document-store.ts +35 -1
  375. package/src/export/transcript-formatter.ts +61 -2
  376. package/src/filing/filing-service.ts +42 -56
  377. package/src/heartbeat/__tests__/heartbeat-service.test.ts +359 -0
  378. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  379. package/src/heartbeat/heartbeat-service.ts +149 -128
  380. package/src/home/__tests__/feed-types.test.ts +63 -131
  381. package/src/home/__tests__/feed-writer.test.ts +77 -278
  382. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  383. package/src/home/feed-types.ts +19 -73
  384. package/src/home/feed-writer.ts +25 -156
  385. package/src/home/post-connect-feed.ts +1 -3
  386. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  387. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  388. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  389. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  390. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  391. package/src/ipc/assistant-server.ts +148 -42
  392. package/src/ipc/cli-client.ts +370 -50
  393. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  394. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  395. package/src/ipc/skill-routes/events.ts +30 -3
  396. package/src/ipc/skill-server.ts +99 -42
  397. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  398. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  399. package/src/live-voice/live-voice-session-manager.ts +11 -4
  400. package/src/live-voice/live-voice-session.ts +14 -6
  401. package/src/memory/__tests__/bookmark-crud.test.ts +258 -0
  402. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  403. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  404. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  405. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  406. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  407. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  408. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +213 -0
  409. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  410. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  411. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  412. package/src/memory/bookmark-crud.ts +179 -0
  413. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  414. package/src/memory/context-search/agent-protocol.ts +5 -1
  415. package/src/memory/context-search/agent-runner.ts +60 -85
  416. package/src/memory/context-search/limits.ts +1 -4
  417. package/src/memory/context-search/search.ts +23 -113
  418. package/src/memory/context-search/sources/conversations.ts +18 -6
  419. package/src/memory/context-search/sources/memory-v2.ts +40 -31
  420. package/src/memory/context-search/sources/memory.ts +9 -2
  421. package/src/memory/context-search/sources/workspace.ts +13 -10
  422. package/src/memory/context-search/types.ts +1 -1
  423. package/src/memory/conversation-bootstrap.ts +11 -0
  424. package/src/memory/conversation-crud.ts +312 -10
  425. package/src/memory/conversation-queries.ts +9 -5
  426. package/src/memory/conversation-title-service.ts +1 -0
  427. package/src/memory/conversation-types.ts +16 -0
  428. package/src/memory/db-init.ts +14 -0
  429. package/src/memory/embedding-backend.ts +2 -1
  430. package/src/memory/embedding-runtime-manager.ts +1 -2
  431. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  432. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  433. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  434. package/src/memory/graph/conversation-graph-memory.ts +108 -14
  435. package/src/memory/graph/extraction.ts +4 -0
  436. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  437. package/src/memory/graph/graph-search.test.ts +6 -5
  438. package/src/memory/graph/graph-search.ts +3 -4
  439. package/src/memory/graph/retriever.test.ts +12 -7
  440. package/src/memory/graph/retriever.ts +4 -5
  441. package/src/memory/graph/tool-handlers.ts +20 -11
  442. package/src/memory/graph/tools.ts +48 -9
  443. package/src/memory/indexer.ts +18 -2
  444. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +120 -6
  445. package/src/memory/jobs/embed-concept-page.ts +261 -89
  446. package/src/memory/jobs-store.ts +51 -1
  447. package/src/memory/jobs-worker.ts +60 -7
  448. package/src/memory/llm-request-log-source-clickhouse.ts +317 -0
  449. package/src/memory/llm-request-log-source-local.ts +26 -0
  450. package/src/memory/llm-request-log-source.ts +97 -0
  451. package/src/memory/llm-request-log-store.ts +1 -1
  452. package/src/memory/memory-retrospective-constants.ts +13 -0
  453. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  454. package/src/memory/memory-retrospective-job.ts +351 -0
  455. package/src/memory/memory-retrospective-startup-cleanup.ts +108 -0
  456. package/src/memory/memory-retrospective-state.ts +162 -0
  457. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  458. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  459. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  460. package/src/memory/message-content.ts +38 -1
  461. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  462. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  463. package/src/memory/migrations/229-delete-private-conversations.test.ts +70 -1
  464. package/src/memory/migrations/229-delete-private-conversations.ts +12 -0
  465. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  466. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  467. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  468. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  469. package/src/memory/migrations/243-provider-connections.ts +68 -0
  470. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  471. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  472. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  473. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  474. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  475. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  476. package/src/memory/migrations/index.ts +7 -0
  477. package/src/memory/pkb/pkb-search.test.ts +6 -5
  478. package/src/memory/pkb/pkb-search.ts +4 -5
  479. package/src/memory/published-pages-store.ts +16 -0
  480. package/src/memory/qdrant-client.ts +3 -0
  481. package/src/memory/schema/bookmarks.ts +38 -0
  482. package/src/memory/schema/conversations.ts +2 -0
  483. package/src/memory/schema/index.ts +2 -0
  484. package/src/memory/schema/inference.ts +29 -0
  485. package/src/memory/schema/memory-core.ts +9 -0
  486. package/src/memory/search/semantic.ts +5 -9
  487. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  488. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  489. package/src/memory/v2/__tests__/activation.test.ts +46 -9
  490. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  491. package/src/memory/v2/__tests__/consolidation-job.test.ts +140 -163
  492. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  493. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  494. package/src/memory/v2/__tests__/injection.test.ts +768 -33
  495. package/src/memory/v2/__tests__/migration.test.ts +7 -3
  496. package/src/memory/v2/__tests__/page-index.test.ts +277 -0
  497. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  498. package/src/memory/v2/__tests__/prompts-router.test.ts +257 -0
  499. package/src/memory/v2/__tests__/qdrant.test.ts +382 -9
  500. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  501. package/src/memory/v2/__tests__/router.test.ts +516 -0
  502. package/src/memory/v2/__tests__/sim.test.ts +163 -8
  503. package/src/memory/v2/__tests__/skill-store.test.ts +58 -3
  504. package/src/memory/v2/__tests__/static-context.test.ts +8 -35
  505. package/src/memory/v2/__tests__/sweep-job.test.ts +114 -33
  506. package/src/memory/v2/activation-store.ts +34 -5
  507. package/src/memory/v2/activation.ts +40 -27
  508. package/src/memory/v2/backfill-jobs.ts +17 -84
  509. package/src/memory/v2/consolidation-job.ts +92 -86
  510. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  511. package/src/memory/v2/injection.ts +466 -115
  512. package/src/memory/v2/migration.ts +117 -20
  513. package/src/memory/v2/page-index.ts +191 -0
  514. package/src/memory/v2/page-store.ts +42 -0
  515. package/src/memory/v2/prompts/consolidation.ts +14 -7
  516. package/src/memory/v2/prompts/router.ts +192 -0
  517. package/src/memory/v2/qdrant.ts +307 -133
  518. package/src/memory/v2/reranker.ts +14 -7
  519. package/src/memory/v2/router.ts +322 -0
  520. package/src/memory/v2/sim.ts +88 -34
  521. package/src/memory/v2/skill-store.ts +118 -29
  522. package/src/memory/v2/static-context.ts +20 -17
  523. package/src/memory/v2/sweep-job.ts +127 -102
  524. package/src/memory/v2/types.ts +16 -5
  525. package/src/memory/validation.ts +13 -0
  526. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  527. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  528. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  529. package/src/notifications/adapters/platform.ts +171 -0
  530. package/src/notifications/conversation-pairing.ts +2 -2
  531. package/src/notifications/copy-composer.ts +61 -12
  532. package/src/notifications/decision-engine.ts +46 -0
  533. package/src/notifications/destination-resolver.ts +21 -0
  534. package/src/notifications/emit-signal.ts +28 -1
  535. package/src/notifications/home-feed-side-effect.ts +111 -0
  536. package/src/notifications/signal.ts +5 -0
  537. package/src/permissions/checker.ts +12 -0
  538. package/src/permissions/gateway-threshold-reader.ts +116 -8
  539. package/src/permissions/ipc-risk-types.ts +2 -0
  540. package/src/permissions/prompter.ts +86 -96
  541. package/src/permissions/secret-prompter.ts +31 -31
  542. package/src/plugin-api/index.ts +13 -0
  543. package/src/plugin-api/package.json +12 -0
  544. package/src/plugin-api/types.ts +62 -0
  545. package/src/plugins/defaults/injectors.ts +20 -5
  546. package/src/plugins/external-plugin-loader.ts +294 -0
  547. package/src/plugins/types.ts +46 -30
  548. package/src/plugins/user-loader.ts +64 -41
  549. package/src/proactive-artifact/job.test.ts +63 -8
  550. package/src/proactive-artifact/job.ts +20 -2
  551. package/src/proactive-artifact/message-copy.ts +18 -1
  552. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  553. package/src/proactive-artifact/trigger-state.ts +4 -0
  554. package/src/prompts/__tests__/system-prompt.test.ts +105 -0
  555. package/src/prompts/system-prompt.ts +22 -1
  556. package/src/prompts/templates/SOUL.md +13 -28
  557. package/src/prompts/update-bulletin-job.ts +61 -73
  558. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  559. package/src/providers/__tests__/inference.test.ts +288 -0
  560. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  561. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  562. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  563. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  564. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  565. package/src/providers/anthropic/client.ts +95 -26
  566. package/src/providers/call-site-routing.ts +94 -16
  567. package/src/providers/connection-resolution.ts +163 -0
  568. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  569. package/src/providers/inference/adapter-factory.ts +173 -0
  570. package/src/providers/inference/auth.ts +112 -0
  571. package/src/providers/inference/backfill.ts +196 -0
  572. package/src/providers/inference/connections.ts +356 -0
  573. package/src/providers/inference/resolve-auth.ts +65 -0
  574. package/src/providers/model-catalog.ts +104 -6
  575. package/src/providers/openai/responses-provider.ts +4 -2
  576. package/src/providers/provider-env-vars.ts +17 -7
  577. package/src/providers/provider-secret-catalog.ts +49 -30
  578. package/src/providers/provider-send-message.ts +41 -20
  579. package/src/providers/registry.ts +143 -159
  580. package/src/providers/retry.ts +18 -10
  581. package/src/providers/search-provider-catalog.ts +121 -0
  582. package/src/runtime/AGENTS.md +18 -5
  583. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  584. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  585. package/src/runtime/actor-trust-resolver.ts +32 -10
  586. package/src/runtime/agent-wake.ts +35 -6
  587. package/src/runtime/assistant-event-hub.ts +3 -85
  588. package/src/runtime/auth/route-policy.ts +304 -8
  589. package/src/runtime/auth/same-actor.ts +2 -0
  590. package/src/runtime/background-job-runner.ts +339 -0
  591. package/src/runtime/btw-sidechain.ts +1 -0
  592. package/src/runtime/channel-approvals.ts +3 -2
  593. package/src/runtime/guardian-reply-router.ts +0 -10
  594. package/src/runtime/http-router.ts +36 -1
  595. package/src/runtime/http-server.ts +31 -5
  596. package/src/runtime/http-types.ts +2 -0
  597. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  598. package/src/runtime/middleware/request-logger.ts +62 -1
  599. package/src/runtime/pending-interactions.ts +19 -15
  600. package/src/runtime/pre-first-message-gate.ts +83 -0
  601. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  602. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +251 -0
  603. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  604. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +315 -0
  605. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +189 -0
  606. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  607. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  608. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  609. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  610. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  611. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  612. package/src/runtime/routes/acp-routes.ts +10 -8
  613. package/src/runtime/routes/app-management-routes.ts +228 -3
  614. package/src/runtime/routes/approval-routes.ts +7 -21
  615. package/src/runtime/routes/audit-routes.ts +43 -0
  616. package/src/runtime/routes/auth-routes.ts +72 -0
  617. package/src/runtime/routes/avatar-routes.ts +273 -20
  618. package/src/runtime/routes/backup-routes.ts +406 -2
  619. package/src/runtime/routes/bookmark-routes.ts +154 -0
  620. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  621. package/src/runtime/routes/consolidation-routes.ts +8 -9
  622. package/src/runtime/routes/contact-routes.ts +0 -160
  623. package/src/runtime/routes/conversation-cli-routes.ts +192 -0
  624. package/src/runtime/routes/conversation-management-routes.ts +30 -43
  625. package/src/runtime/routes/conversation-query-routes.ts +373 -82
  626. package/src/runtime/routes/conversation-routes.ts +31 -10
  627. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  628. package/src/runtime/routes/credential-routes.ts +540 -0
  629. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  630. package/src/runtime/routes/debug-routes.ts +2 -2
  631. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  632. package/src/runtime/routes/domain-routes.ts +167 -0
  633. package/src/runtime/routes/email-routes.ts +603 -0
  634. package/src/runtime/routes/errors.ts +2 -2
  635. package/src/runtime/routes/events-routes.ts +192 -0
  636. package/src/runtime/routes/filing-routes.ts +2 -3
  637. package/src/runtime/routes/home-feed-routes.ts +6 -78
  638. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  639. package/src/runtime/routes/host-browser-routes.ts +103 -22
  640. package/src/runtime/routes/http-adapter.ts +2 -0
  641. package/src/runtime/routes/identity-routes.ts +5 -0
  642. package/src/runtime/routes/image-generation-routes.ts +99 -0
  643. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +137 -1
  644. package/src/runtime/routes/inbound-stages/background-dispatch.ts +87 -7
  645. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  646. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -7
  647. package/src/runtime/routes/index.ts +36 -0
  648. package/src/runtime/routes/inference-profile-session-handler.ts +312 -0
  649. package/src/runtime/routes/inference-profile-session-reaper.ts +98 -0
  650. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  651. package/src/runtime/routes/inference-provider-connection-routes.ts +317 -0
  652. package/src/runtime/routes/inference-send-routes.ts +115 -0
  653. package/src/runtime/routes/integrations/twilio.ts +1 -0
  654. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  655. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  656. package/src/runtime/routes/memory-item-routes.ts +5 -6
  657. package/src/runtime/routes/memory-v2-routes.ts +105 -404
  658. package/src/runtime/routes/notification-routes.ts +2 -0
  659. package/src/runtime/routes/oauth-apps.ts +112 -7
  660. package/src/runtime/routes/oauth-commands-routes.ts +1007 -0
  661. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  662. package/src/runtime/routes/oauth-providers.ts +298 -8
  663. package/src/runtime/routes/platform-routes.ts +336 -0
  664. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  665. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  666. package/src/runtime/routes/playground/state.ts +2 -1
  667. package/src/runtime/routes/publish-routes.ts +221 -0
  668. package/src/runtime/routes/schedule-routes.ts +82 -0
  669. package/src/runtime/routes/sequence-routes.ts +291 -0
  670. package/src/runtime/routes/settings-routes.ts +2 -10
  671. package/src/runtime/routes/skills-routes.ts +31 -1
  672. package/src/runtime/routes/stt-routes.ts +240 -3
  673. package/src/runtime/routes/surface-action-routes.ts +43 -7
  674. package/src/runtime/routes/tts-routes.ts +67 -0
  675. package/src/runtime/routes/types.ts +32 -0
  676. package/src/runtime/routes/user-routes-cli.ts +243 -0
  677. package/src/runtime/routes/webhook-routes.ts +165 -0
  678. package/src/runtime/sync/resource-sync-events.ts +25 -0
  679. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  680. package/src/runtime/sync/sync-publisher.ts +21 -0
  681. package/src/schedule/scheduler.ts +200 -123
  682. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  683. package/src/security/secret-patterns.ts +3 -0
  684. package/src/sequence/engine.ts +38 -40
  685. package/src/skills/include-graph.ts +35 -13
  686. package/src/subagent/manager.ts +20 -15
  687. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  688. package/src/tools/browser/browser-execution.ts +15 -4
  689. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  690. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  691. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  692. package/src/tools/browser/cdp-client/factory.ts +66 -5
  693. package/src/tools/browser/runtime-check.ts +77 -0
  694. package/src/tools/document/document-tool.ts +20 -0
  695. package/src/tools/executor.ts +18 -2
  696. package/src/tools/memory/register.test.ts +10 -8
  697. package/src/tools/memory/register.ts +9 -1
  698. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  699. package/src/tools/network/web-search.ts +280 -37
  700. package/src/tools/permission-checker.ts +28 -5
  701. package/src/tools/skills/load.ts +24 -20
  702. package/src/tools/subagent/spawn.ts +3 -3
  703. package/src/tools/terminal/shell.ts +44 -0
  704. package/src/tools/tool-name-aliases.ts +19 -0
  705. package/src/tools/types.ts +19 -1
  706. package/src/usage/attribution.ts +3 -2
  707. package/src/util/pricing.ts +86 -160
  708. package/src/watcher/__tests__/engine.test.ts +301 -0
  709. package/src/watcher/constants.ts +7 -0
  710. package/src/watcher/engine.ts +90 -90
  711. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  712. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  713. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +28 -4
  714. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  715. package/src/workspace/migrations/069-seed-onboarding-threads.ts +34 -0
  716. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  717. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  718. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +104 -0
  719. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +93 -0
  720. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  721. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  722. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  723. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  724. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  725. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  726. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  727. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  728. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  729. package/src/workspace/migrations/registry.ts +28 -0
  730. package/src/workspace/migrations/runner.ts +13 -2
  731. package/src/workspace/migrations/types.ts +13 -3
  732. package/src/workspace/provider-commit-message-generator.ts +3 -2
  733. package/src/__tests__/context-search-pkb-source.test.ts +0 -492
  734. package/src/__tests__/credentials-cli.test.ts +0 -1225
  735. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  736. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  737. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  738. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  739. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  740. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  741. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  742. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  743. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  744. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  745. package/src/cli/commands/autonomy.ts +0 -365
  746. package/src/cli/commands/memory.ts +0 -424
  747. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -1201
  748. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  749. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  750. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  751. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  752. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  753. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  754. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  755. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  756. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  757. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  758. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  759. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  760. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  761. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  762. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  763. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  764. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  765. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  766. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  767. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  768. package/src/home/assistant-feed-authoring.ts +0 -135
  769. package/src/home/emit-feed-event.ts +0 -169
  770. package/src/home/feed-scheduler.ts +0 -281
  771. package/src/home/platform-gmail-digest.ts +0 -163
  772. package/src/home/rewrite-command-preview.ts +0 -66
  773. package/src/home/rewrite-feed-title.ts +0 -58
  774. package/src/home/rollup-producer.ts +0 -426
  775. package/src/memory/admin.ts +0 -326
  776. package/src/memory/context-search/sources/pkb.ts +0 -477
  777. package/src/memory/graph/compaction.ts +0 -299
  778. /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
  }