@vellumai/assistant 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -1,954 +1,703 @@
1
- /**
2
- * CLI command group: `assistant email`
3
- *
4
- * Provider-agnostic email operations routed through the service facade.
5
- * All commands output JSON to stdout. Use --json for machine-readable output.
6
- * Exit codes: 0 = success, 1 = error, 2 = guardrail blocked.
7
- */
8
-
9
- import { Command } from "commander";
10
-
11
- import {
12
- SUPPORTED_PROVIDERS,
13
- type SupportedProvider,
14
- } from "../../email/providers/index.js";
15
- import { getEmailService, GuardrailError } from "../../email/service.js";
16
-
17
- // ---------------------------------------------------------------------------
18
- // Helpers
19
- // ---------------------------------------------------------------------------
20
-
21
- function output(data: unknown, json: boolean): void {
22
- process.stdout.write(
23
- json ? JSON.stringify(data) + "\n" : JSON.stringify(data, null, 2) + "\n",
24
- );
25
- }
1
+ import { readFileSync, writeFileSync } from "node:fs";
26
2
 
27
- function outputError(data: unknown, code: number): void {
28
- output(data, true);
29
- process.exitCode = code;
30
- }
3
+ import type { Command } from "commander";
31
4
 
32
- function exitError(message: string, code = 1): void {
33
- outputError({ ok: false, error: message }, code);
34
- }
5
+ import { VellumPlatformClient } from "../../platform/client.js";
6
+ import { getCliLogger } from "../logger.js";
7
+ import { shouldOutputJson, writeOutput } from "../output.js";
35
8
 
36
- function getJson(cmd: Command): boolean {
37
- let c: Command | null = cmd;
38
- while (c) {
39
- if ((c.opts() as { json?: boolean }).json) return true;
40
- c = c.parent;
41
- }
42
- return false;
43
- }
44
-
45
- async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
46
- try {
47
- const result = await fn();
48
- output({ ok: true, ...(result as Record<string, unknown>) }, getJson(cmd));
49
- } catch (err) {
50
- if (err instanceof GuardrailError) {
51
- outputError({ ok: false, error: err.code, ...err.details }, 2);
52
- return;
53
- }
54
- outputError(
55
- { ok: false, error: err instanceof Error ? err.message : String(err) },
56
- 1,
57
- );
58
- }
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Command registration
63
- // ---------------------------------------------------------------------------
9
+ const log = getCliLogger("email");
64
10
 
65
11
  export function registerEmailCommand(program: Command): void {
66
12
  const email = program
67
13
  .command("email")
68
- .description("Email operations (provider-agnostic)")
69
- .option("--json", "Machine-readable JSON output");
14
+ .description("Email channel operations")
15
+ .option("--json", "Machine-readable compact JSON output");
70
16
 
71
17
  email.addHelpText(
72
18
  "after",
73
19
  `
74
- Email commands are provider-agnostic the same CLI works regardless of
75
- the configured email provider (e.g. agentmail, resend). Use "email provider"
76
- to switch between providers.
77
-
78
- Outbound emails follow a draft-based sending model:
79
- 1. Create a draft with "email draft create"
80
- 2. Approve and send with "email draft approve-send"
81
- 3. Optionally reject with "email draft reject"
82
-
83
- Guardrails (outbound pause, daily send cap, address allow/block rules) are
84
- enforced at send time. If a guardrail blocks sending, exit code 2 is returned.
85
-
86
- Exit codes: 0 = success, 1 = error, 2 = guardrail blocked.
20
+ Manage the assistant's email channel on the Vellum platform.
87
21
 
88
22
  Examples:
23
+ $ assistant email register mybot
24
+ $ assistant email unregister --confirm
25
+ $ assistant email send user@example.com -s "Hello" -b "Hi there"
89
26
  $ assistant email status
90
- $ assistant email draft create --from hello@example.com --to user@test.com --subject "Hello" --body "Hi there"
91
- $ assistant email draft approve-send --draft-id d_abc123 --confirm
92
- $ assistant email guardrails set --daily-cap 50
93
- $ assistant email setup domain --domain example.com`,
94
- );
95
-
96
- const svc = getEmailService();
97
-
98
- // =========================================================================
99
- // Provider subcommands
100
- // =========================================================================
101
- const provider = email
102
- .command("provider")
103
- .description("Manage email provider");
104
-
105
- provider.addHelpText(
106
- "after",
107
- `
108
- Switch between email providers without changing the rest of your email
109
- configuration. The active provider determines which backend handles domain
110
- setup, inbox creation, DNS records, and message delivery.
111
-
112
- Examples:
113
- $ assistant email provider get
114
- $ assistant email provider set agentmail`,
27
+ $ assistant email list
28
+ $ assistant email register mybot --json`,
115
29
  );
116
30
 
117
- provider
118
- .command("get")
119
- .description("Show the active email provider")
120
- .addHelpText(
121
- "after",
122
- `
123
- Returns the name of the currently active email provider (e.g. agentmail,
124
- resend). Use this to confirm which backend is handling email operations
125
- before making changes.
126
-
127
- Examples:
128
- $ assistant email provider get
129
- $ assistant email provider get --json`,
130
- )
131
- .action((_opts: unknown, cmd: Command) => {
132
- output({ ok: true, provider: svc.getProviderName() }, getJson(cmd));
133
- });
134
-
135
- provider
136
- .command("set <provider>")
137
- .description(
138
- `Set the active email provider (${SUPPORTED_PROVIDERS.join(", ")})`,
139
- )
31
+ email
32
+ .command("register <username>")
33
+ .description("Register an @vellum.me email address for this assistant")
140
34
  .addHelpText(
141
35
  "after",
142
36
  `
143
37
  Arguments:
144
- provider The email provider to activate. Supported values: ${SUPPORTED_PROVIDERS.join(", ")}
38
+ username The local part of the email address (e.g. "mybot" → mybot@vellum.me)
145
39
 
146
- Persists the provider selection to config. All subsequent email commands
147
- (setup, inbox, draft, guardrails) will route through the selected provider.
40
+ Registers a new email address on the Vellum platform for the current
41
+ assistant. Each assistant can have one email address. The address is
42
+ immediately active for receiving inbound email.
148
43
 
149
44
  Examples:
150
- $ assistant email provider set agentmail`,
151
- )
152
- .action((name: string, _opts: unknown, cmd: Command) => {
153
- if (!SUPPORTED_PROVIDERS.includes(name as SupportedProvider)) {
154
- exitError(
155
- `Unknown provider: ${name}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`,
156
- );
157
- return;
158
- }
159
- svc.setProvider(name as SupportedProvider);
160
- output({ ok: true, provider: name }, getJson(cmd));
161
- });
162
-
163
- // =========================================================================
164
- // Status
165
- // =========================================================================
166
- email
167
- .command("status")
168
- .description("Show provider health, inboxes, and guardrail state")
169
- .addHelpText(
170
- "after",
171
- `
172
- Returns a combined view of the email subsystem: active provider and its health
173
- status, configured inboxes with their addresses, and current guardrail state
174
- (paused flag, daily send cap, today's send count).
175
-
176
- Use this to verify the email stack is fully configured before sending.
45
+ $ assistant email register mybot
46
+ ✓ Registered mybot@vellum.me
177
47
 
178
- Examples:
179
- $ assistant email status
180
- $ assistant email status --json`,
48
+ $ assistant email register support --json
49
+ {"address":"support@vellum.me","id":"...","created_at":"..."}`,
181
50
  )
182
- .action(async (_opts: unknown, cmd: Command) => {
183
- await run(cmd, async () => {
184
- const status = await svc.status();
185
- return status;
186
- });
187
- });
51
+ .action(async (username: string, _opts: unknown, cmd: Command) => {
52
+ try {
53
+ const client = await VellumPlatformClient.create();
54
+ if (!client) {
55
+ throw new Error(
56
+ "Platform credentials not configured. Run: assistant platform connect",
57
+ );
58
+ }
59
+ if (!client.platformAssistantId) {
60
+ throw new Error(
61
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
62
+ );
63
+ }
188
64
 
189
- // =========================================================================
190
- // Setup subcommands
191
- // =========================================================================
192
- const setup = email
193
- .command("setup")
194
- .description("Domain, inbox, and webhook setup");
65
+ const response = await client.fetch(
66
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
67
+ {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ username }),
71
+ },
72
+ );
195
73
 
196
- setup.addHelpText(
197
- "after",
198
- `
199
- The setup workflow configures the email stack in order:
200
- 1. domain — Register a sending domain with the provider
201
- 2. dns — Retrieve SPF, DKIM, and DMARC records to add to your DNS
202
- 3. verify — Check that DNS records have propagated and are valid
203
- 4. inboxes — Create standard inboxes (hello@, support@, ops@)
204
- 5. webhook — Register an inbound webhook for receiving email
74
+ if (!response.ok) {
75
+ const body = (await response.json().catch(() => ({}))) as Record<
76
+ string,
77
+ unknown
78
+ >;
79
+ const detail =
80
+ body.detail ??
81
+ (Array.isArray(body.username) ? body.username[0] : undefined) ??
82
+ (Array.isArray(body.assistant_id)
83
+ ? body.assistant_id[0]
84
+ : undefined) ??
85
+ `HTTP ${response.status}`;
86
+ throw new Error(String(detail));
87
+ }
205
88
 
206
- Run each step in sequence. "verify" will fail until DNS records propagate.
89
+ const data = (await response.json()) as {
90
+ id: string;
91
+ address: string;
92
+ created_at: string;
93
+ };
207
94
 
208
- Examples:
209
- $ assistant email setup domain --domain example.com
210
- $ assistant email setup dns --domain example.com
211
- $ assistant email setup verify --domain example.com`,
212
- );
95
+ if (shouldOutputJson(cmd)) {
96
+ writeOutput(cmd, data);
97
+ } else {
98
+ log.info(`✓ Registered ${data.address}`);
99
+ }
100
+ } catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ if (shouldOutputJson(cmd)) {
103
+ writeOutput(cmd, { error: message });
104
+ } else {
105
+ log.error(`Error: ${message}`);
106
+ }
107
+ process.exitCode = 1;
108
+ }
109
+ });
213
110
 
214
- setup
215
- .command("domain")
216
- .description("Create/register a domain")
217
- .requiredOption("--domain <domain>", "Domain name")
218
- .option("--dry-run", "Preview without creating")
111
+ email
112
+ .command("unregister")
113
+ .description("Remove the email address registered for this assistant")
114
+ .option("--confirm", "Skip confirmation prompt")
219
115
  .addHelpText(
220
116
  "after",
221
117
  `
222
- Registers a sending domain with the active email provider. The domain must
223
- be a valid, publicly resolvable domain you control.
224
-
225
- Use --dry-run to preview the registration without creating it. This returns
226
- the DNS records that would need to be configured without committing changes.
118
+ Removes the email address currently registered for this assistant.
119
+ The address is deactivated immediately inbound email will no longer
120
+ be delivered. The username enters a cooldown period and is not
121
+ immediately available for reuse.
227
122
 
228
123
  Examples:
229
- $ assistant email setup domain --domain example.com
230
- $ assistant email setup domain --domain example.com --dry-run`,
231
- )
232
- .action(
233
- async (opts: { domain: string; dryRun?: boolean }, cmd: Command) => {
234
- await run(cmd, async () => {
235
- const domain = await svc.setupDomain(opts.domain, opts.dryRun);
236
- return { domain };
237
- });
238
- },
239
- );
240
-
241
- setup
242
- .command("dns")
243
- .description("Get DNS records (SPF/DKIM/DMARC) for a domain")
244
- .requiredOption("--domain <domain>", "Domain name")
245
- .addHelpText(
246
- "after",
247
- `
248
- Returns the SPF, DKIM, and DMARC DNS records that must be added to your
249
- domain's DNS zone. These records authorize the email provider to send on
250
- behalf of your domain and improve deliverability.
124
+ $ assistant email unregister
125
+ Remove mybot@vellum.me? (y/N) y
126
+ ✓ Unregistered mybot@vellum.me
251
127
 
252
- Add all returned records to your DNS provider before running "setup verify".
128
+ $ assistant email unregister --confirm
129
+ ✓ Unregistered mybot@vellum.me
253
130
 
254
- Examples:
255
- $ assistant email setup dns --domain example.com
256
- $ assistant email setup dns --domain example.com --json`,
131
+ $ assistant email unregister --json
132
+ {"unregistered":"mybot@vellum.me"}`,
257
133
  )
258
- .action(async (opts: { domain: string }, cmd: Command) => {
259
- await run(cmd, async () => {
260
- const records = await svc.getDomainDnsRecords(opts.domain);
261
- return { domain: opts.domain, records };
262
- });
263
- });
264
-
265
- setup
266
- .command("verify")
267
- .description("Verify domain after DNS is configured")
268
- .requiredOption("--domain <domain>", "Domain name")
269
- .addHelpText(
270
- "after",
271
- `
272
- Checks that the required DNS records (SPF, DKIM, DMARC) have propagated and
273
- are correctly configured. DNS propagation can take minutes to hours depending
274
- on your DNS provider's TTL settings.
134
+ .action(async (_opts: { confirm?: boolean }, cmd: Command) => {
135
+ try {
136
+ const client = await VellumPlatformClient.create();
137
+ if (!client) {
138
+ throw new Error(
139
+ "Platform credentials not configured. Run: assistant platform connect",
140
+ );
141
+ }
142
+ if (!client.platformAssistantId) {
143
+ throw new Error(
144
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
145
+ );
146
+ }
275
147
 
276
- Run this after adding all records returned by "setup dns". If verification
277
- fails, wait for propagation and retry.
148
+ const listResponse = await client.fetch(
149
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
150
+ );
278
151
 
279
- Examples:
280
- $ assistant email setup verify --domain example.com`,
281
- )
282
- .action(async (opts: { domain: string }, cmd: Command) => {
283
- await run(cmd, async () => {
284
- const domain = await svc.verifyDomain(opts.domain);
285
- return { domain };
286
- });
287
- });
152
+ if (!listResponse.ok) {
153
+ throw new Error(
154
+ `Failed to list email addresses: HTTP ${listResponse.status}`,
155
+ );
156
+ }
288
157
 
289
- setup
290
- .command("inboxes")
291
- .description("Create standard inboxes (hello@, support@, ops@)")
292
- .requiredOption("--domain <domain>", "Domain name")
293
- .addHelpText(
294
- "after",
295
- `
296
- Creates the standard set of inboxes (hello@, support@, ops@) on the
297
- specified domain. Idempotent — re-running skips already existing inboxes.
158
+ const listData = (await listResponse.json()) as {
159
+ results: { id: string; address: string }[];
160
+ };
298
161
 
299
- The domain must be registered and verified before creating inboxes.
162
+ const addresses = listData.results ?? [];
163
+ if (addresses.length === 0) {
164
+ throw new Error("No email address registered for this assistant.");
165
+ }
300
166
 
301
- Examples:
302
- $ assistant email setup inboxes --domain example.com`,
303
- )
304
- .action(async (opts: { domain: string }, cmd: Command) => {
305
- await run(cmd, async () => {
306
- const inboxes = await svc.ensureInboxes(opts.domain);
307
- return { domain: opts.domain, inboxes };
308
- });
309
- });
167
+ const target = addresses[0];
168
+
169
+ if (!_opts.confirm && !shouldOutputJson(cmd)) {
170
+ const rl = await import("node:readline");
171
+ const iface = rl.createInterface({
172
+ input: process.stdin,
173
+ output: process.stderr,
174
+ });
175
+ const answer = await new Promise<string>((resolve) => {
176
+ iface.question(`Remove ${target.address}? (y/N) `, resolve);
177
+ });
178
+ iface.close();
179
+ if (answer.trim().toLowerCase() !== "y") {
180
+ log.info("Cancelled.");
181
+ return;
182
+ }
183
+ }
310
184
 
311
- setup
312
- .command("webhook")
313
- .description("Register inbound webhook")
314
- .requiredOption("--url <url>", "Webhook URL")
315
- .option("--secret <secret>", "Webhook signing secret")
316
- .addHelpText(
317
- "after",
318
- `
319
- Registers a webhook URL with the email provider to receive inbound messages.
320
- The provider will POST incoming emails to this URL as JSON payloads.
185
+ const deleteResponse = await client.fetch(
186
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/${target.id}/`,
187
+ { method: "DELETE" },
188
+ );
321
189
 
322
- If --secret is provided, the provider signs each webhook payload with the
323
- secret so you can verify authenticity. If omitted, the provider may generate
324
- one automatically (provider-dependent).
190
+ if (!deleteResponse.ok) {
191
+ const body = (await deleteResponse
192
+ .json()
193
+ .catch(() => ({}))) as Record<string, unknown>;
194
+ const detail = body.detail ?? `HTTP ${deleteResponse.status}`;
195
+ throw new Error(String(detail));
196
+ }
325
197
 
326
- Examples:
327
- $ assistant email setup webhook --url https://example.com/api/email/inbound
328
- $ assistant email setup webhook --url https://example.com/api/email/inbound --secret whsec_abc123`,
329
- )
330
- .action(async (opts: { url: string; secret?: string }, cmd: Command) => {
331
- await run(cmd, async () => {
332
- const webhook = await svc.setupWebhook(opts.url, opts.secret);
333
- return { webhook };
334
- });
198
+ if (shouldOutputJson(cmd)) {
199
+ writeOutput(cmd, { unregistered: target.address });
200
+ } else {
201
+ log.info(`✓ Unregistered ${target.address}`);
202
+ }
203
+ } catch (err) {
204
+ const message = err instanceof Error ? err.message : String(err);
205
+ if (shouldOutputJson(cmd)) {
206
+ writeOutput(cmd, { error: message });
207
+ } else {
208
+ log.error(`Error: ${message}`);
209
+ }
210
+ process.exitCode = 1;
211
+ }
335
212
  });
336
213
 
337
- // =========================================================================
338
- // Inbox subcommands
339
- // =========================================================================
340
- const inbox = email.command("inbox").description("Manage inboxes");
341
-
342
- inbox.addHelpText(
343
- "after",
344
- `
345
- Inboxes are email addresses that can send and receive messages through the
346
- configured provider. Each inbox has a username (local part), domain, and
347
- optional display name.
348
-
349
- Examples:
350
- $ assistant email inbox list
351
- $ assistant email inbox create --username sam --domain example.com --display-name "Samwise"`,
352
- );
353
-
354
- inbox
355
- .command("create")
356
- .description("Create a new inbox")
357
- .requiredOption("--username <username>", 'Local part (e.g. "sam")')
358
- .option(
359
- "--domain <domain>",
360
- 'Domain (e.g. "agentmail.to"). Omit for provider default.',
361
- )
362
- .option("--display-name <name>", 'Display name (e.g. "Samwise")')
214
+ email
215
+ .command("status")
216
+ .description("Show email address info and usage for this assistant")
363
217
  .addHelpText(
364
218
  "after",
365
219
  `
366
- Creates a new inbox with the given username (local part) on the specified
367
- domain. If --domain is omitted, the provider's default domain is used.
368
-
369
- The --display-name sets the friendly name shown in the "From" header
370
- (e.g. "Samwise <sam@example.com>").
220
+ Shows the email address registered for this assistant along with
221
+ current usage and quota information from the platform.
371
222
 
372
223
  Examples:
373
- $ assistant email inbox create --username sam --domain example.com
374
- $ assistant email inbox create --username support --domain example.com --display-name "Support Team"
375
- $ assistant email inbox create --username hello`,
224
+ $ assistant email status
225
+ Address: mybot@vellum.me
226
+ Status: active
227
+ Sent: 12 / 100 (daily)
228
+
229
+ $ assistant email status --json
230
+ {"address":"mybot@vellum.me","status":"active","usage":{"sent_today":12,"daily_limit":100}}`,
376
231
  )
377
- .action(
378
- async (
379
- opts: { username: string; domain?: string; displayName?: string },
380
- cmd: Command,
381
- ) => {
382
- await run(cmd, async () => {
383
- const created = await svc.createInbox(
384
- opts.username,
385
- opts.domain,
386
- opts.displayName,
232
+ .action(async (_opts: unknown, cmd: Command) => {
233
+ try {
234
+ const client = await VellumPlatformClient.create();
235
+ if (!client) {
236
+ throw new Error(
237
+ "Platform credentials not configured. Run: assistant platform connect",
387
238
  );
388
- return { inbox: created };
389
- });
390
- },
391
- );
239
+ }
240
+ if (!client.platformAssistantId) {
241
+ throw new Error(
242
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
243
+ );
244
+ }
392
245
 
393
- inbox
394
- .command("list")
395
- .description("List all inboxes")
396
- .addHelpText(
397
- "after",
398
- `
399
- Lists all inboxes configured on the active email provider. Each inbox
400
- entry includes its address, display name, and inbox ID.
246
+ // 1. List addresses to find the registered one
247
+ const listResponse = await client.fetch(
248
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
249
+ );
401
250
 
402
- Use this to verify which inboxes are available before creating drafts or
403
- configuring inbound webhooks.
251
+ if (!listResponse.ok) {
252
+ throw new Error(
253
+ `Failed to list email addresses: HTTP ${listResponse.status}`,
254
+ );
255
+ }
404
256
 
405
- Examples:
406
- $ assistant email inbox list
407
- $ assistant email inbox list --json`,
408
- )
409
- .action(async (_opts: unknown, cmd: Command) => {
410
- await run(cmd, async () => {
411
- const inboxes = await svc.listInboxes();
412
- return { inboxes };
413
- });
414
- });
257
+ const listData = (await listResponse.json()) as {
258
+ results: { id: string; address: string }[];
259
+ };
415
260
 
416
- // =========================================================================
417
- // Draft subcommands
418
- // =========================================================================
419
- const draft = email.command("draft").description("Manage email drafts");
261
+ const addresses = listData.results ?? [];
262
+ if (addresses.length === 0) {
263
+ throw new Error(
264
+ "No email address registered for this assistant. Run: assistant email register <username>",
265
+ );
266
+ }
420
267
 
421
- draft.addHelpText(
422
- "after",
423
- `
424
- Drafts follow a lifecycle: create -> approve-send or reject.
268
+ const target = addresses[0];
425
269
 
426
- 1. "draft create" stages an outbound email without sending it
427
- 2. "draft approve-send" runs guardrail checks and sends if allowed
428
- 3. "draft reject" permanently deletes a draft (will not be sent)
270
+ // 2. Fetch status/usage for this address
271
+ const statusResponse = await client.fetch(
272
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/${target.id}/status/`,
273
+ );
429
274
 
430
- Drafts can also be listed, inspected by ID, or deleted. Use "draft list
431
- --status pending" to see drafts awaiting approval.
275
+ if (!statusResponse.ok) {
276
+ const body = (await statusResponse
277
+ .json()
278
+ .catch(() => ({}))) as Record<string, unknown>;
279
+ const detail = body.detail ?? `HTTP ${statusResponse.status}`;
280
+ throw new Error(String(detail));
281
+ }
432
282
 
433
- Examples:
434
- $ assistant email draft create --from hello@example.com --to user@test.com --subject "Hi" --body "Hello"
435
- $ assistant email draft list --status pending
436
- $ assistant email draft approve-send --draft-id d_abc123 --confirm`,
437
- );
283
+ const statusData = (await statusResponse.json()) as {
284
+ address: string;
285
+ status: string;
286
+ usage: {
287
+ sent_today: number;
288
+ daily_limit: number;
289
+ received_today: number;
290
+ };
291
+ };
292
+
293
+ if (shouldOutputJson(cmd)) {
294
+ writeOutput(cmd, statusData);
295
+ } else {
296
+ log.info(`Address: ${statusData.address}`);
297
+ log.info(`Status: ${statusData.status}`);
298
+ if (statusData.usage) {
299
+ log.info(
300
+ `Sent: ${statusData.usage.sent_today} / ${statusData.usage.daily_limit} (daily)`,
301
+ );
302
+ log.info(`Received today: ${statusData.usage.received_today}`);
303
+ }
304
+ }
305
+ } catch (err) {
306
+ const message = err instanceof Error ? err.message : String(err);
307
+ if (shouldOutputJson(cmd)) {
308
+ writeOutput(cmd, { error: message });
309
+ } else {
310
+ log.error(`Error: ${message}`);
311
+ }
312
+ process.exitCode = 1;
313
+ }
314
+ });
438
315
 
439
- draft
440
- .command("create")
441
- .description("Create a new draft")
442
- .requiredOption("--from <address>", "Sender address or inbox ID")
443
- .requiredOption("--to <address>", "Recipient email address")
444
- .requiredOption("--subject <subject>", "Email subject")
445
- .requiredOption("--body <body>", "Email body (plain text)")
446
- .option("--cc <address>", "CC address")
447
- .option("--in-reply-to <messageId>", "Message ID to reply to")
316
+ email
317
+ .command("list")
318
+ .description("List received and sent emails for this assistant")
319
+ .option(
320
+ "-d, --direction <direction>",
321
+ "Filter by direction: inbound, outbound, or all",
322
+ "all",
323
+ )
324
+ .option("-l, --limit <count>", "Maximum number of results", "20")
325
+ .option("--since <date>", "Only show messages since this date (ISO 8601)")
448
326
  .addHelpText(
449
327
  "after",
450
328
  `
451
- Creates an outbound email draft without sending it. The draft must be
452
- explicitly approved via "draft approve-send" before it is delivered.
453
-
454
- Required fields:
455
- --from Sender address (must match a configured inbox) or inbox ID
456
- --to Recipient email address
457
- --subject Email subject line
458
- --body Email body in plain text
459
-
460
- Optional fields:
461
- --cc CC recipient address
462
- --in-reply-to Message ID of the email being replied to (for threading)
329
+ Lists email messages for this assistant. Shows subject, from, to,
330
+ direction, and timestamp for each message.
463
331
 
464
332
  Examples:
465
- $ assistant email draft create --from hello@example.com --to user@test.com --subject "Hello" --body "Hi there"
466
- $ assistant email draft create --from hello@example.com --to user@test.com --subject "Re: Question" --body "Sure thing" --in-reply-to msg_abc123
467
- $ assistant email draft create --from hello@example.com --to user@test.com --subject "Update" --body "FYI" --cc manager@test.com`,
333
+ $ assistant email list
334
+ $ assistant email list --direction inbound --limit 5
335
+ $ assistant email list --since 2026-04-01 --json`,
468
336
  )
469
337
  .action(
470
338
  async (
471
339
  opts: {
472
- from: string;
473
- to: string;
474
- subject: string;
475
- body: string;
476
- cc?: string;
477
- inReplyTo?: string;
340
+ direction?: string;
341
+ limit?: string;
342
+ since?: string;
478
343
  },
479
344
  cmd: Command,
480
345
  ) => {
481
- await run(cmd, async () => {
482
- const d = await svc.createDraft(opts);
483
- return { draft: d };
484
- });
346
+ try {
347
+ const client = await VellumPlatformClient.create();
348
+ if (!client) {
349
+ throw new Error(
350
+ "Platform credentials not configured. Run: assistant platform connect",
351
+ );
352
+ }
353
+ if (!client.platformAssistantId) {
354
+ throw new Error(
355
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
356
+ );
357
+ }
358
+
359
+ const params = new URLSearchParams();
360
+ if (opts.direction && opts.direction !== "all") {
361
+ params.set("direction", opts.direction);
362
+ }
363
+ if (opts.limit) {
364
+ params.set("limit", opts.limit);
365
+ }
366
+ if (opts.since) {
367
+ params.set("since", opts.since);
368
+ }
369
+
370
+ const qs = params.toString();
371
+ const path = `/v1/assistants/${client.platformAssistantId}/emails/${qs ? `?${qs}` : ""}`;
372
+ const response = await client.fetch(path);
373
+
374
+ if (!response.ok) {
375
+ const body = (await response.json().catch(() => ({}))) as Record<
376
+ string,
377
+ unknown
378
+ >;
379
+ const detail = body.detail ?? `HTTP ${response.status}`;
380
+ throw new Error(String(detail));
381
+ }
382
+
383
+ const data = (await response.json()) as {
384
+ results: {
385
+ id: string;
386
+ direction: string;
387
+ from_address: string;
388
+ to_addresses: string[];
389
+ subject: string;
390
+ created_at: string;
391
+ }[];
392
+ count: number;
393
+ };
394
+
395
+ if (shouldOutputJson(cmd)) {
396
+ writeOutput(cmd, data);
397
+ } else {
398
+ const messages = data.results ?? [];
399
+ if (messages.length === 0) {
400
+ log.info("No email messages found.");
401
+ } else {
402
+ for (const msg of messages) {
403
+ const dir = msg.direction === "inbound" ? "←" : "→";
404
+ const to = Array.isArray(msg.to_addresses)
405
+ ? msg.to_addresses.join(", ")
406
+ : "";
407
+ const date = new Date(msg.created_at).toLocaleString();
408
+ log.info(
409
+ `${dir} ${date} ${msg.from_address} → ${to} "${msg.subject || "(no subject)"}"`,
410
+ );
411
+ }
412
+ log.info(`\n${data.count} total message(s)`);
413
+ }
414
+ }
415
+ } catch (err) {
416
+ const message = err instanceof Error ? err.message : String(err);
417
+ if (shouldOutputJson(cmd)) {
418
+ writeOutput(cmd, { error: message });
419
+ } else {
420
+ log.error(`Error: ${message}`);
421
+ }
422
+ process.exitCode = 1;
423
+ }
485
424
  },
486
425
  );
487
426
 
488
- draft
489
- .command("list")
490
- .description("List drafts")
427
+ email
428
+ .command("download <message-id>")
429
+ .description("Download a specific email message")
491
430
  .option(
492
- "--status <status>",
493
- "Filter by status (pending|approved|sent|rejected)",
494
- )
495
- .addHelpText(
496
- "after",
497
- `
498
- Lists all email drafts, optionally filtered by status. Returns an array
499
- of draft objects with their IDs, recipients, subjects, and current status.
500
-
501
- Use --status to narrow results to a specific lifecycle stage:
502
- pending — created but not yet approved
503
- approved — approved and queued for sending
504
- sent — successfully delivered
505
- rejected — delivery failed by the provider (e.g. bounce or send error)
506
-
507
- Note: drafts rejected via "draft reject" are permanently deleted and will
508
- not appear here. The "rejected" status only applies to provider-side
509
- delivery failures.
510
-
511
- Examples:
512
- $ assistant email draft list
513
- $ assistant email draft list --status pending
514
- $ assistant email draft list --status sent --json`,
431
+ "--format <type>",
432
+ "Output format: text, html, json (default: text)",
433
+ "text",
515
434
  )
516
- .action(async (opts: { status?: string }, cmd: Command) => {
517
- await run(cmd, async () => {
518
- const drafts = await svc.listDrafts(opts.status);
519
- return { drafts };
520
- });
521
- });
522
-
523
- draft
524
- .command("get <draftId>")
525
- .description("Get a draft by ID")
526
- .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
435
+ .option("-o, --output <path>", "Write to file instead of stdout")
527
436
  .addHelpText(
528
437
  "after",
529
438
  `
530
439
  Arguments:
531
- draftId The ID of the draft to retrieve (e.g. d_abc123)
440
+ message-id Email message ID (from \`assistant email list --json\`)
532
441
 
533
- Returns the full draft object including sender, recipient, subject, body,
534
- status, and timestamps. Use --inbox to scope the lookup in multi-inbox
535
- setups.
442
+ Downloads a specific email message by ID. The default format shows
443
+ headers and the plain-text body. Use --format html for the HTML body,
444
+ or --format json for the full message object.
536
445
 
537
446
  Examples:
538
- $ assistant email draft get d_abc123
539
- $ assistant email draft get d_abc123 --inbox inbox_456
540
- $ assistant email draft get d_abc123 --json`,
541
- )
542
- .action(async (draftId: string, opts: { inbox?: string }, cmd: Command) => {
543
- await run(cmd, async () => {
544
- const d = await svc.getDraft(draftId, opts.inbox);
545
- return { draft: d };
546
- });
547
- });
447
+ $ assistant email download msg_abc123
448
+ From: user@example.com
449
+ To: mybot@vellum.me
450
+ Subject: Hello
451
+ Date: 2026-04-05 12:00:00
548
452
 
549
- draft
550
- .command("approve-send")
551
- .alias("send")
552
- .alias("approve")
553
- .description("Check guardrails and send a draft")
554
- .requiredOption("--draft-id <id>", "Draft ID to send")
555
- .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
556
- .requiredOption("--confirm", "Explicit confirmation flag (required)")
557
- .addHelpText(
558
- "after",
559
- `
560
- Runs guardrail checks (outbound pause, daily send cap, address block/allow
561
- rules) and sends the draft if all checks pass. The --confirm flag is required
562
- as an explicit safety gate.
563
-
564
- If a guardrail blocks the send, the command exits with code 2 and returns
565
- the guardrail error details in the response JSON. The draft remains in
566
- pending state and can be retried after adjusting guardrails.
453
+ Hi, this is a test message.
567
454
 
568
- Aliases: "draft send", "draft approve"
455
+ $ assistant email download msg_abc123 --format json
456
+ {"id":"msg_abc123","direction":"inbound",...}
569
457
 
570
- Examples:
571
- $ assistant email draft approve-send --draft-id d_abc123 --confirm
572
- $ assistant email draft approve-send --draft-id d_abc123 --confirm --json`,
458
+ $ assistant email download msg_abc123 -o email.txt
459
+ Saved to email.txt`,
573
460
  )
574
461
  .action(
575
462
  async (
576
- opts: { draftId: string; inbox?: string; confirm: boolean },
463
+ messageId: string,
464
+ opts: {
465
+ format?: string;
466
+ output?: string;
467
+ },
577
468
  cmd: Command,
578
469
  ) => {
579
- if (!opts.confirm) {
580
- exitError("The --confirm flag is required for approve-send");
581
- return;
582
- }
583
- await run(cmd, async () => {
584
- const result = await svc.approveSend(opts.draftId, opts.inbox);
585
- return {
586
- messageId: result.messageId,
587
- threadId: result.threadId,
588
- dailyCount: result.dailyCount,
589
- };
590
- });
591
- },
592
- );
470
+ try {
471
+ const client = await VellumPlatformClient.create();
472
+ if (!client) {
473
+ throw new Error(
474
+ "Platform credentials not configured. Run: assistant platform connect",
475
+ );
476
+ }
477
+ if (!client.platformAssistantId) {
478
+ throw new Error(
479
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
480
+ );
481
+ }
482
+
483
+ const response = await client.fetch(
484
+ `/v1/assistants/${client.platformAssistantId}/emails/${messageId}/`,
485
+ );
593
486
 
594
- draft
595
- .command("reject")
596
- .description("Reject a draft")
597
- .requiredOption("--draft-id <id>", "Draft ID to reject")
598
- .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
599
- .option("--reason <text>", "Reason for rejection")
600
- .addHelpText(
601
- "after",
602
- `
603
- Rejects a pending draft by permanently deleting it so it will not be
604
- sent. The --reason flag is accepted for logging but the draft itself is
605
- removed from the provider and cannot be recovered.
487
+ if (!response.ok) {
488
+ const body = (await response.json().catch(() => ({}))) as Record<
489
+ string,
490
+ unknown
491
+ >;
492
+ const detail = body.detail ?? `HTTP ${response.status}`;
493
+ throw new Error(String(detail));
494
+ }
495
+
496
+ const msg = (await response.json()) as {
497
+ id: string;
498
+ direction: string;
499
+ from_address: string;
500
+ to_addresses: string[];
501
+ subject: string;
502
+ body_text: string;
503
+ body_html: string;
504
+ in_reply_to: string;
505
+ references: string[];
506
+ created_at: string;
507
+ };
606
508
 
607
- Examples:
608
- $ assistant email draft reject --draft-id d_abc123
609
- $ assistant email draft reject --draft-id d_abc123 --reason "Wrong recipient"
610
- $ assistant email draft reject --draft-id d_abc123 --inbox inbox_456`,
611
- )
612
- .action(
613
- async (
614
- opts: { draftId: string; inbox?: string; reason?: string },
615
- cmd: Command,
616
- ) => {
617
- await run(cmd, async () => {
618
- await svc.rejectDraft(opts.draftId, opts.reason, opts.inbox);
619
- return { draftId: opts.draftId, action: "rejected" };
620
- });
509
+ const fmt = opts.format ?? "text";
510
+
511
+ let content: string;
512
+ if (fmt === "json" || shouldOutputJson(cmd)) {
513
+ content = JSON.stringify(msg, null, 2) + "\n";
514
+ } else if (fmt === "html") {
515
+ if (!msg.body_html) {
516
+ throw new Error("No HTML body available for this message.");
517
+ }
518
+ content = msg.body_html;
519
+ } else {
520
+ // text format: headers + body
521
+ const to = Array.isArray(msg.to_addresses)
522
+ ? msg.to_addresses.join(", ")
523
+ : "";
524
+ const date = new Date(msg.created_at).toLocaleString();
525
+ const lines = [
526
+ `From: ${msg.from_address}`,
527
+ `To: ${to}`,
528
+ `Subject: ${msg.subject || "(no subject)"}`,
529
+ `Date: ${date}`,
530
+ ];
531
+ if (msg.in_reply_to) {
532
+ lines.push(`In-Reply-To: ${msg.in_reply_to}`);
533
+ }
534
+ lines.push("", msg.body_text || "(no plain-text body)");
535
+ content = lines.join("\n") + "\n";
536
+ }
537
+
538
+ if (opts.output) {
539
+ writeFileSync(opts.output, content, "utf-8");
540
+ if (!shouldOutputJson(cmd)) {
541
+ log.info(`✓ Saved to ${opts.output}`);
542
+ } else {
543
+ writeOutput(cmd, { saved: opts.output, bytes: content.length });
544
+ }
545
+ } else {
546
+ process.stdout.write(content);
547
+ }
548
+ } catch (err) {
549
+ const message = err instanceof Error ? err.message : String(err);
550
+ if (shouldOutputJson(cmd)) {
551
+ writeOutput(cmd, { error: message });
552
+ } else {
553
+ log.error(`Error: ${message}`);
554
+ }
555
+ process.exitCode = 1;
556
+ }
621
557
  },
622
558
  );
623
559
 
624
- draft
625
- .command("delete <draftId>")
626
- .description("Delete a draft")
627
- .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
560
+ email
561
+ .command("send <to>")
562
+ .description("Send an email from this assistant")
563
+ .option("-s, --subject <text>", "Subject line")
564
+ .option("-b, --body <text>", "Email body (plain text)")
565
+ .option("-f, --file <path>", "Read body from file")
566
+ .option("--html <path>", "HTML body file (optional)")
628
567
  .addHelpText(
629
568
  "after",
630
569
  `
631
570
  Arguments:
632
- draftId The ID of the draft to delete (e.g. d_abc123)
633
-
634
- Permanently removes a draft from the system. Both "reject" and "delete"
635
- result in permanent deletion; use "reject" when you want to log a reason
636
- for not sending. Use --inbox to scope the deletion in multi-inbox setups.
637
-
638
- Examples:
639
- $ assistant email draft delete d_abc123
640
- $ assistant email draft delete d_abc123 --inbox inbox_456`,
641
- )
642
- .action(async (draftId: string, opts: { inbox?: string }, cmd: Command) => {
643
- await run(cmd, async () => {
644
- await svc.deleteDraft(draftId, opts.inbox);
645
- return { draftId, action: "deleted" };
646
- });
647
- });
571
+ to Recipient email address
648
572
 
649
- // =========================================================================
650
- // Inbound subcommands
651
- // =========================================================================
652
- const inbound = email.command("inbound").description("View inbound messages");
653
-
654
- inbound.addHelpText(
655
- "after",
656
- `
657
- View messages received by your inboxes. Inbound messages are emails sent
658
- to your configured inbox addresses by external senders.
573
+ Sends an email from the assistant's registered email address via the
574
+ Vellum runtime proxy. The "from" address is automatically resolved
575
+ from the assistant's registered email address.
659
576
 
660
- Use "inbound list" to browse received messages and "inbound get" to
661
- retrieve the full content of a specific message.
577
+ Body source priority: --body flag > --file flag > stdin (if not a TTY).
662
578
 
663
579
  Examples:
664
- $ assistant email inbound list
665
- $ assistant email inbound list --thread-id thr_abc123
666
- $ assistant email inbound get msg_def456`,
667
- );
580
+ $ assistant email send user@example.com -s "Hello" -b "Hi there"
581
+ Sent to user@example.com (delivery_id: abc123)
668
582
 
669
- inbound
670
- .command("list")
671
- .description("List inbound messages")
672
- .option("--thread-id <id>", "Filter by thread ID")
673
- .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
674
- .addHelpText(
675
- "after",
676
- `
677
- Lists inbound messages received by your inboxes. Optionally filter by
678
- thread ID to see only messages belonging to a specific conversation, or
679
- by inbox ID to scope to a particular inbox.
583
+ $ echo "Body text" | assistant email send user@example.com -s "Hello"
584
+ ✓ Sent to user@example.com (delivery_id: def456)
680
585
 
681
- Examples:
682
- $ assistant email inbound list
683
- $ assistant email inbound list --thread-id thr_abc123
684
- $ assistant email inbound list --inbox inbox_456
685
- $ assistant email inbound list --json`,
586
+ $ assistant email send user@example.com -s "Hello" -b "Hi" --json
587
+ {"delivery_id":"abc123","status":"accepted"}`,
686
588
  )
687
589
  .action(
688
- async (opts: { threadId?: string; inbox?: string }, cmd: Command) => {
689
- await run(cmd, async () => {
690
- const messages = await svc.listMessages(opts.threadId, opts.inbox);
691
- return { messages };
692
- });
693
- },
694
- );
695
-
696
- inbound
697
- .command("get <messageId>")
698
- .description("Get a specific inbound message")
699
- .addHelpText(
700
- "after",
701
- `
702
- Arguments:
703
- messageId The ID of the inbound message to retrieve (e.g. msg_abc123)
704
-
705
- Returns the full inbound message including sender, recipients, subject,
706
- body, headers, and timestamps.
707
-
708
- Examples:
709
- $ assistant email inbound get msg_abc123
710
- $ assistant email inbound get msg_abc123 --json`,
711
- )
712
- .action(async (messageId: string, _opts: unknown, cmd: Command) => {
713
- await run(cmd, async () => {
714
- const message = await svc.getMessage(messageId);
715
- return { message };
716
- });
717
- });
718
-
719
- // =========================================================================
720
- // Thread subcommands
721
- // =========================================================================
722
- const thread = email.command("thread").description("View email threads");
723
-
724
- thread.addHelpText(
725
- "after",
726
- `
727
- Threads group related emails (original message and replies) into a single
728
- conversation. Each thread has a unique ID and contains one or more messages.
729
-
730
- Use "thread list" to browse all threads and "thread get" to retrieve
731
- the full conversation history for a specific thread.
732
-
733
- Examples:
734
- $ assistant email thread list
735
- $ assistant email thread get thr_abc123`,
736
- );
737
-
738
- thread
739
- .command("list")
740
- .description("List threads")
741
- .addHelpText(
742
- "after",
743
- `
744
- Lists all email threads. Each thread entry includes its ID, subject,
745
- participant addresses, message count, and timestamps.
746
-
747
- Examples:
748
- $ assistant email thread list
749
- $ assistant email thread list --json`,
750
- )
751
- .action(async (_opts: unknown, cmd: Command) => {
752
- await run(cmd, async () => {
753
- const threads = await svc.listThreads();
754
- return { threads };
755
- });
756
- });
757
-
758
- thread
759
- .command("get <threadId>")
760
- .description("Get a specific thread")
761
- .addHelpText(
762
- "after",
763
- `
764
- Arguments:
765
- threadId The ID of the thread to retrieve (e.g. thr_abc123)
766
-
767
- Returns the full thread including all messages (inbound and outbound),
768
- participants, subject, and timestamps. Messages are ordered chronologically.
769
-
770
- Examples:
771
- $ assistant email thread get thr_abc123
772
- $ assistant email thread get thr_abc123 --json`,
773
- )
774
- .action(async (threadId: string, _opts: unknown, cmd: Command) => {
775
- await run(cmd, async () => {
776
- const t = await svc.getThread(threadId);
777
- return { thread: t };
778
- });
779
- });
780
-
781
- // =========================================================================
782
- // Guardrails subcommands
783
- // =========================================================================
784
- const guardrails = email
785
- .command("guardrails")
786
- .description("Manage email guardrails");
787
-
788
- guardrails.addHelpText(
789
- "after",
790
- `
791
- Guardrails are safety controls enforced at send time (during "draft
792
- approve-send"). Three types of guardrails exist:
793
-
794
- 1. Outbound pause — when paused=true, all sends are blocked
795
- 2. Daily send cap — limits the total number of emails sent per day
796
- 3. Address rules — block or allow patterns (e.g. *@spam.com)
797
-
798
- When a guardrail blocks a send, exit code 2 is returned with the specific
799
- guardrail error in the response.
800
-
801
- Examples:
802
- $ assistant email guardrails get
803
- $ assistant email guardrails set --paused true
804
- $ assistant email guardrails set --daily-cap 100
805
- $ assistant email guardrails block "*@spam.com"`,
806
- );
807
-
808
- guardrails
809
- .command("get")
810
- .description("Show current guardrail settings")
811
- .addHelpText(
812
- "after",
813
- `
814
- Returns the current guardrail configuration: outbound pause state,
815
- daily send cap, today's send count, and a summary of address rules.
816
-
817
- Use this to verify guardrail settings before sending emails.
590
+ async (
591
+ to: string,
592
+ opts: {
593
+ subject?: string;
594
+ body?: string;
595
+ file?: string;
596
+ html?: string;
597
+ },
598
+ cmd: Command,
599
+ ) => {
600
+ try {
601
+ const client = await VellumPlatformClient.create();
602
+ if (!client) {
603
+ throw new Error(
604
+ "Platform credentials not configured. Run: assistant platform connect",
605
+ );
606
+ }
607
+ if (!client.platformAssistantId) {
608
+ throw new Error(
609
+ "Assistant ID not configured. Set PLATFORM_ASSISTANT_ID or run: assistant platform connect",
610
+ );
611
+ }
612
+
613
+ // 1. Resolve the assistant's registered email address (the "from").
614
+ const listResponse = await client.fetch(
615
+ `/v1/assistants/${client.platformAssistantId}/email-addresses/`,
616
+ );
818
617
 
819
- Examples:
820
- $ assistant email guardrails get
821
- $ assistant email guardrails get --json`,
822
- )
823
- .action((_opts: unknown, cmd: Command) => {
824
- output({ ok: true, ...svc.getGuardrails() }, getJson(cmd));
825
- });
618
+ if (!listResponse.ok) {
619
+ throw new Error(
620
+ `Failed to list email addresses: HTTP ${listResponse.status}`,
621
+ );
622
+ }
826
623
 
827
- guardrails
828
- .command("set")
829
- .description("Update guardrail settings")
830
- .option("--paused <value>", "Pause outbound (true/false)")
831
- .option("--daily-cap <n>", "Daily send cap")
832
- .addHelpText(
833
- "after",
834
- `
835
- Updates one or both guardrail settings. Omitted flags leave the existing
836
- value unchanged.
624
+ const listData = (await listResponse.json()) as {
625
+ results: { id: string; address: string }[];
626
+ };
837
627
 
838
- --paused true/false Enable or disable the outbound pause. When paused,
839
- all "approve-send" calls are blocked with exit code 2.
840
- --daily-cap <n> Set the maximum number of emails that can be sent per
841
- calendar day. Set to 0 to disable sending entirely.
628
+ const addresses = listData.results ?? [];
629
+ if (addresses.length === 0) {
630
+ throw new Error(
631
+ "No email address registered for this assistant. Run: assistant email register <username>",
632
+ );
633
+ }
634
+
635
+ const fromAddress = addresses[0].address;
636
+
637
+ // 2. Resolve body text: --body > --file > stdin
638
+ let text = opts.body;
639
+ if (!text && opts.file) {
640
+ text = readFileSync(opts.file, "utf-8");
641
+ }
642
+ if (!text && !process.stdin.isTTY) {
643
+ text = readFileSync("/dev/stdin", "utf-8");
644
+ }
645
+ if (!text) {
646
+ throw new Error(
647
+ "Email body is required. Use --body, --file, or pipe via stdin.",
648
+ );
649
+ }
650
+
651
+ // 3. Resolve optional HTML body from file
652
+ let html: string | undefined;
653
+ if (opts.html) {
654
+ html = readFileSync(opts.html, "utf-8");
655
+ }
656
+
657
+ // 4. Build payload
658
+ const payload: Record<string, string> = {
659
+ to,
660
+ from_address: fromAddress,
661
+ text,
662
+ };
663
+ if (opts.subject) payload.subject = opts.subject;
664
+ if (html) payload.html = html;
665
+
666
+ // 5. Send via runtime proxy
667
+ const response = await client.fetch("/v1/runtime-proxy/email/send/", {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify(payload),
671
+ });
672
+
673
+ if (!response.ok) {
674
+ const body = (await response.json().catch(() => ({}))) as Record<
675
+ string,
676
+ unknown
677
+ >;
678
+ const detail = body.detail ?? `HTTP ${response.status}`;
679
+ throw new Error(String(detail));
680
+ }
681
+
682
+ const data = (await response.json()) as {
683
+ delivery_id: string;
684
+ status: string;
685
+ };
842
686
 
843
- Examples:
844
- $ assistant email guardrails set --paused true
845
- $ assistant email guardrails set --paused false --daily-cap 50
846
- $ assistant email guardrails set --daily-cap 0`,
847
- )
848
- .action((opts: { paused?: string; dailyCap?: string }, cmd: Command) => {
849
- const updates: { paused?: boolean; dailyCap?: number } = {};
850
- if (opts.paused !== undefined) {
851
- updates.paused = opts.paused === "true";
852
- }
853
- if (opts.dailyCap !== undefined) {
854
- const n = parseInt(opts.dailyCap, 10);
855
- if (isNaN(n) || n < 0) {
856
- exitError("daily-cap must be a non-negative integer");
857
- return;
687
+ if (shouldOutputJson(cmd)) {
688
+ writeOutput(cmd, data);
689
+ } else {
690
+ log.info(`✓ Sent to ${to} (delivery_id: ${data.delivery_id})`);
691
+ }
692
+ } catch (err) {
693
+ const message = err instanceof Error ? err.message : String(err);
694
+ if (shouldOutputJson(cmd)) {
695
+ writeOutput(cmd, { error: message });
696
+ } else {
697
+ log.error(`Error: ${message}`);
698
+ }
699
+ process.exitCode = 1;
858
700
  }
859
- updates.dailyCap = n;
860
- }
861
- const result = svc.setGuardrails(updates);
862
- output({ ok: true, ...result }, getJson(cmd));
863
- });
864
-
865
- guardrails
866
- .command("block <pattern>")
867
- .description("Block addresses matching pattern (e.g., *@spam.com)")
868
- .addHelpText(
869
- "after",
870
- `
871
- Arguments:
872
- pattern Glob pattern matching email addresses to block. Supports * as
873
- wildcard. Examples: "*@spam.com", "user@*", "*@*.example.com"
874
-
875
- Creates a block rule. Any "approve-send" to a recipient matching this
876
- pattern will be rejected with exit code 2. Rules are evaluated in order;
877
- block rules take precedence over allow rules for the same address.
878
-
879
- Examples:
880
- $ assistant email guardrails block "*@spam.com"
881
- $ assistant email guardrails block "marketing@*"`,
882
- )
883
- .action((pattern: string, _opts: unknown, cmd: Command) => {
884
- const rule = svc.addRule("block", pattern);
885
- output({ ok: true, rule }, getJson(cmd));
886
- });
887
-
888
- guardrails
889
- .command("allow <pattern>")
890
- .description("Allow addresses matching pattern")
891
- .addHelpText(
892
- "after",
893
- `
894
- Arguments:
895
- pattern Glob pattern matching email addresses to allow. Supports * as
896
- wildcard. Examples: "*@partner.com", "vip@*", "*@*.trusted.com"
897
-
898
- Creates an allow rule. Addresses matching this pattern will pass the
899
- address-rule guardrail check during "approve-send". Note that block rules
900
- take precedence over allow rules for the same address.
901
-
902
- Examples:
903
- $ assistant email guardrails allow "*@partner.com"
904
- $ assistant email guardrails allow "vip@example.com"`,
905
- )
906
- .action((pattern: string, _opts: unknown, cmd: Command) => {
907
- const rule = svc.addRule("allow", pattern);
908
- output({ ok: true, rule }, getJson(cmd));
909
- });
910
-
911
- guardrails
912
- .command("rules")
913
- .description("List all address rules")
914
- .addHelpText(
915
- "after",
916
- `
917
- Lists all configured address rules (both block and allow). Each rule
918
- entry includes its ID, type (block or allow), and the glob pattern.
919
-
920
- Use the rule ID with "guardrails unrule" to remove a specific rule.
921
-
922
- Examples:
923
- $ assistant email guardrails rules
924
- $ assistant email guardrails rules --json`,
925
- )
926
- .action((_opts: unknown, cmd: Command) => {
927
- output({ ok: true, rules: svc.listAddressRules() }, getJson(cmd));
928
- });
929
-
930
- guardrails
931
- .command("unrule <ruleId>")
932
- .description("Remove an address rule by ID")
933
- .addHelpText(
934
- "after",
935
- `
936
- Arguments:
937
- ruleId The ID of the address rule to remove. Use "guardrails rules" to
938
- list all rules and their IDs.
939
-
940
- Permanently removes a block or allow rule. The rule takes effect immediately —
941
- subsequent "approve-send" calls will no longer be affected by the removed rule.
942
-
943
- Examples:
944
- $ assistant email guardrails unrule rule_abc123
945
- $ assistant email guardrails rules # list rules to find the ID first`,
946
- )
947
- .action((ruleId: string, _opts: unknown, cmd: Command) => {
948
- if (svc.removeRule(ruleId)) {
949
- output({ ok: true, ruleId, action: "removed" }, getJson(cmd));
950
- } else {
951
- exitError(`No rule found matching "${ruleId}"`);
952
- }
953
- });
701
+ },
702
+ );
954
703
  }