@vellumai/assistant 0.4.35 → 0.4.37

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 (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
package/src/cli/email.ts CHANGED
@@ -68,6 +68,31 @@ export function registerEmailCommand(program: Command): void {
68
68
  .description("Email operations (provider-agnostic)")
69
69
  .option("--json", "Machine-readable JSON output");
70
70
 
71
+ email.addHelpText(
72
+ "after",
73
+ `
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.
87
+
88
+ Examples:
89
+ $ vellum email status
90
+ $ vellum email draft create --from hello@example.com --to user@test.com --subject "Hello" --body "Hi there"
91
+ $ vellum email draft approve-send --draft-id d_abc123 --confirm
92
+ $ vellum email guardrails set --daily-cap 50
93
+ $ vellum email setup domain --domain example.com`,
94
+ );
95
+
71
96
  const svc = getEmailService();
72
97
 
73
98
  // =========================================================================
@@ -77,9 +102,32 @@ export function registerEmailCommand(program: Command): void {
77
102
  .command("provider")
78
103
  .description("Manage email provider");
79
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
+ $ vellum email provider get
114
+ $ vellum email provider set agentmail`,
115
+ );
116
+
80
117
  provider
81
118
  .command("get")
82
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
+ $ vellum email provider get
129
+ $ vellum email provider get --json`,
130
+ )
83
131
  .action((_opts: unknown, cmd: Command) => {
84
132
  output({ ok: true, provider: svc.getProviderName() }, getJson(cmd));
85
133
  });
@@ -89,6 +137,18 @@ export function registerEmailCommand(program: Command): void {
89
137
  .description(
90
138
  `Set the active email provider (${SUPPORTED_PROVIDERS.join(", ")})`,
91
139
  )
140
+ .addHelpText(
141
+ "after",
142
+ `
143
+ Arguments:
144
+ provider The email provider to activate. Supported values: ${SUPPORTED_PROVIDERS.join(", ")}
145
+
146
+ Persists the provider selection to config. All subsequent email commands
147
+ (setup, inbox, draft, guardrails) will route through the selected provider.
148
+
149
+ Examples:
150
+ $ vellum email provider set agentmail`,
151
+ )
92
152
  .action((name: string, _opts: unknown, cmd: Command) => {
93
153
  if (!SUPPORTED_PROVIDERS.includes(name as SupportedProvider)) {
94
154
  exitError(
@@ -106,6 +166,19 @@ export function registerEmailCommand(program: Command): void {
106
166
  email
107
167
  .command("status")
108
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.
177
+
178
+ Examples:
179
+ $ vellum email status
180
+ $ vellum email status --json`,
181
+ )
109
182
  .action(async (_opts: unknown, cmd: Command) => {
110
183
  await run(cmd, async () => {
111
184
  const status = await svc.status();
@@ -120,11 +193,42 @@ export function registerEmailCommand(program: Command): void {
120
193
  .command("setup")
121
194
  .description("Domain, inbox, and webhook setup");
122
195
 
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
205
+
206
+ Run each step in sequence. "verify" will fail until DNS records propagate.
207
+
208
+ Examples:
209
+ $ vellum email setup domain --domain example.com
210
+ $ vellum email setup dns --domain example.com
211
+ $ vellum email setup verify --domain example.com`,
212
+ );
213
+
123
214
  setup
124
215
  .command("domain")
125
216
  .description("Create/register a domain")
126
217
  .requiredOption("--domain <domain>", "Domain name")
127
218
  .option("--dry-run", "Preview without creating")
219
+ .addHelpText(
220
+ "after",
221
+ `
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.
227
+
228
+ Examples:
229
+ $ vellum email setup domain --domain example.com
230
+ $ vellum email setup domain --domain example.com --dry-run`,
231
+ )
128
232
  .action(
129
233
  async (opts: { domain: string; dryRun?: boolean }, cmd: Command) => {
130
234
  await run(cmd, async () => {
@@ -138,6 +242,19 @@ export function registerEmailCommand(program: Command): void {
138
242
  .command("dns")
139
243
  .description("Get DNS records (SPF/DKIM/DMARC) for a domain")
140
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.
251
+
252
+ Add all returned records to your DNS provider before running "setup verify".
253
+
254
+ Examples:
255
+ $ vellum email setup dns --domain example.com
256
+ $ vellum email setup dns --domain example.com --json`,
257
+ )
141
258
  .action(async (opts: { domain: string }, cmd: Command) => {
142
259
  await run(cmd, async () => {
143
260
  const records = await svc.getDomainDnsRecords(opts.domain);
@@ -149,6 +266,19 @@ export function registerEmailCommand(program: Command): void {
149
266
  .command("verify")
150
267
  .description("Verify domain after DNS is configured")
151
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.
275
+
276
+ Run this after adding all records returned by "setup dns". If verification
277
+ fails, wait for propagation and retry.
278
+
279
+ Examples:
280
+ $ vellum email setup verify --domain example.com`,
281
+ )
152
282
  .action(async (opts: { domain: string }, cmd: Command) => {
153
283
  await run(cmd, async () => {
154
284
  const domain = await svc.verifyDomain(opts.domain);
@@ -160,6 +290,17 @@ export function registerEmailCommand(program: Command): void {
160
290
  .command("inboxes")
161
291
  .description("Create standard inboxes (hello@, support@, ops@)")
162
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.
298
+
299
+ The domain must be registered and verified before creating inboxes.
300
+
301
+ Examples:
302
+ $ vellum email setup inboxes --domain example.com`,
303
+ )
163
304
  .action(async (opts: { domain: string }, cmd: Command) => {
164
305
  await run(cmd, async () => {
165
306
  const inboxes = await svc.ensureInboxes(opts.domain);
@@ -172,6 +313,20 @@ export function registerEmailCommand(program: Command): void {
172
313
  .description("Register inbound webhook")
173
314
  .requiredOption("--url <url>", "Webhook URL")
174
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.
321
+
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).
325
+
326
+ Examples:
327
+ $ vellum email setup webhook --url https://example.com/api/email/inbound
328
+ $ vellum email setup webhook --url https://example.com/api/email/inbound --secret whsec_abc123`,
329
+ )
175
330
  .action(async (opts: { url: string; secret?: string }, cmd: Command) => {
176
331
  await run(cmd, async () => {
177
332
  const webhook = await svc.setupWebhook(opts.url, opts.secret);
@@ -184,6 +339,18 @@ export function registerEmailCommand(program: Command): void {
184
339
  // =========================================================================
185
340
  const inbox = email.command("inbox").description("Manage inboxes");
186
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
+ $ vellum email inbox list
351
+ $ vellum email inbox create --username sam --domain example.com --display-name "Samwise"`,
352
+ );
353
+
187
354
  inbox
188
355
  .command("create")
189
356
  .description("Create a new inbox")
@@ -193,6 +360,20 @@ export function registerEmailCommand(program: Command): void {
193
360
  'Domain (e.g. "agentmail.to"). Omit for provider default.',
194
361
  )
195
362
  .option("--display-name <name>", 'Display name (e.g. "Samwise")')
363
+ .addHelpText(
364
+ "after",
365
+ `
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>").
371
+
372
+ Examples:
373
+ $ vellum email inbox create --username sam --domain example.com
374
+ $ vellum email inbox create --username support --domain example.com --display-name "Support Team"
375
+ $ vellum email inbox create --username hello`,
376
+ )
196
377
  .action(
197
378
  async (
198
379
  opts: { username: string; domain?: string; displayName?: string },
@@ -212,6 +393,19 @@ export function registerEmailCommand(program: Command): void {
212
393
  inbox
213
394
  .command("list")
214
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.
401
+
402
+ Use this to verify which inboxes are available before creating drafts or
403
+ configuring inbound webhooks.
404
+
405
+ Examples:
406
+ $ vellum email inbox list
407
+ $ vellum email inbox list --json`,
408
+ )
215
409
  .action(async (_opts: unknown, cmd: Command) => {
216
410
  await run(cmd, async () => {
217
411
  const inboxes = await svc.listInboxes();
@@ -224,6 +418,24 @@ export function registerEmailCommand(program: Command): void {
224
418
  // =========================================================================
225
419
  const draft = email.command("draft").description("Manage email drafts");
226
420
 
421
+ draft.addHelpText(
422
+ "after",
423
+ `
424
+ Drafts follow a lifecycle: create -> approve-send or reject.
425
+
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)
429
+
430
+ Drafts can also be listed, inspected by ID, or deleted. Use "draft list
431
+ --status pending" to see drafts awaiting approval.
432
+
433
+ Examples:
434
+ $ vellum email draft create --from hello@example.com --to user@test.com --subject "Hi" --body "Hello"
435
+ $ vellum email draft list --status pending
436
+ $ vellum email draft approve-send --draft-id d_abc123 --confirm`,
437
+ );
438
+
227
439
  draft
228
440
  .command("create")
229
441
  .description("Create a new draft")
@@ -233,6 +445,27 @@ export function registerEmailCommand(program: Command): void {
233
445
  .requiredOption("--body <body>", "Email body (plain text)")
234
446
  .option("--cc <address>", "CC address")
235
447
  .option("--in-reply-to <messageId>", "Message ID to reply to")
448
+ .addHelpText(
449
+ "after",
450
+ `
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)
463
+
464
+ Examples:
465
+ $ vellum email draft create --from hello@example.com --to user@test.com --subject "Hello" --body "Hi there"
466
+ $ vellum email draft create --from hello@example.com --to user@test.com --subject "Re: Question" --body "Sure thing" --in-reply-to msg_abc123
467
+ $ vellum email draft create --from hello@example.com --to user@test.com --subject "Update" --body "FYI" --cc manager@test.com`,
468
+ )
236
469
  .action(
237
470
  async (
238
471
  opts: {
@@ -259,6 +492,27 @@ export function registerEmailCommand(program: Command): void {
259
492
  "--status <status>",
260
493
  "Filter by status (pending|approved|sent|rejected)",
261
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
+ $ vellum email draft list
513
+ $ vellum email draft list --status pending
514
+ $ vellum email draft list --status sent --json`,
515
+ )
262
516
  .action(async (opts: { status?: string }, cmd: Command) => {
263
517
  await run(cmd, async () => {
264
518
  const drafts = await svc.listDrafts(opts.status);
@@ -270,6 +524,21 @@ export function registerEmailCommand(program: Command): void {
270
524
  .command("get <draftId>")
271
525
  .description("Get a draft by ID")
272
526
  .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
527
+ .addHelpText(
528
+ "after",
529
+ `
530
+ Arguments:
531
+ draftId The ID of the draft to retrieve (e.g. d_abc123)
532
+
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.
536
+
537
+ Examples:
538
+ $ vellum email draft get d_abc123
539
+ $ vellum email draft get d_abc123 --inbox inbox_456
540
+ $ vellum email draft get d_abc123 --json`,
541
+ )
273
542
  .action(async (draftId: string, opts: { inbox?: string }, cmd: Command) => {
274
543
  await run(cmd, async () => {
275
544
  const d = await svc.getDraft(draftId, opts.inbox);
@@ -285,6 +554,23 @@ export function registerEmailCommand(program: Command): void {
285
554
  .requiredOption("--draft-id <id>", "Draft ID to send")
286
555
  .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
287
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.
567
+
568
+ Aliases: "draft send", "draft approve"
569
+
570
+ Examples:
571
+ $ vellum email draft approve-send --draft-id d_abc123 --confirm
572
+ $ vellum email draft approve-send --draft-id d_abc123 --confirm --json`,
573
+ )
288
574
  .action(
289
575
  async (
290
576
  opts: { draftId: string; inbox?: string; confirm: boolean },
@@ -311,6 +597,18 @@ export function registerEmailCommand(program: Command): void {
311
597
  .requiredOption("--draft-id <id>", "Draft ID to reject")
312
598
  .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
313
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.
606
+
607
+ Examples:
608
+ $ vellum email draft reject --draft-id d_abc123
609
+ $ vellum email draft reject --draft-id d_abc123 --reason "Wrong recipient"
610
+ $ vellum email draft reject --draft-id d_abc123 --inbox inbox_456`,
611
+ )
314
612
  .action(
315
613
  async (
316
614
  opts: { draftId: string; inbox?: string; reason?: string },
@@ -327,6 +625,20 @@ export function registerEmailCommand(program: Command): void {
327
625
  .command("delete <draftId>")
328
626
  .description("Delete a draft")
329
627
  .option("--inbox <id>", "Inbox ID (for multi-inbox setups)")
628
+ .addHelpText(
629
+ "after",
630
+ `
631
+ 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
+ $ vellum email draft delete d_abc123
640
+ $ vellum email draft delete d_abc123 --inbox inbox_456`,
641
+ )
330
642
  .action(async (draftId: string, opts: { inbox?: string }, cmd: Command) => {
331
643
  await run(cmd, async () => {
332
644
  await svc.deleteDraft(draftId, opts.inbox);
@@ -339,11 +651,39 @@ export function registerEmailCommand(program: Command): void {
339
651
  // =========================================================================
340
652
  const inbound = email.command("inbound").description("View inbound messages");
341
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.
659
+
660
+ Use "inbound list" to browse received messages and "inbound get" to
661
+ retrieve the full content of a specific message.
662
+
663
+ Examples:
664
+ $ vellum email inbound list
665
+ $ vellum email inbound list --thread-id thr_abc123
666
+ $ vellum email inbound get msg_def456`,
667
+ );
668
+
342
669
  inbound
343
670
  .command("list")
344
671
  .description("List inbound messages")
345
672
  .option("--thread-id <id>", "Filter by thread ID")
346
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.
680
+
681
+ Examples:
682
+ $ vellum email inbound list
683
+ $ vellum email inbound list --thread-id thr_abc123
684
+ $ vellum email inbound list --inbox inbox_456
685
+ $ vellum email inbound list --json`,
686
+ )
347
687
  .action(
348
688
  async (opts: { threadId?: string; inbox?: string }, cmd: Command) => {
349
689
  await run(cmd, async () => {
@@ -356,6 +696,19 @@ export function registerEmailCommand(program: Command): void {
356
696
  inbound
357
697
  .command("get <messageId>")
358
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
+ $ vellum email inbound get msg_abc123
710
+ $ vellum email inbound get msg_abc123 --json`,
711
+ )
359
712
  .action(async (messageId: string, _opts: unknown, cmd: Command) => {
360
713
  await run(cmd, async () => {
361
714
  const message = await svc.getMessage(messageId);
@@ -368,9 +721,33 @@ export function registerEmailCommand(program: Command): void {
368
721
  // =========================================================================
369
722
  const thread = email.command("thread").description("View email threads");
370
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
+ $ vellum email thread list
735
+ $ vellum email thread get thr_abc123`,
736
+ );
737
+
371
738
  thread
372
739
  .command("list")
373
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
+ $ vellum email thread list
749
+ $ vellum email thread list --json`,
750
+ )
374
751
  .action(async (_opts: unknown, cmd: Command) => {
375
752
  await run(cmd, async () => {
376
753
  const threads = await svc.listThreads();
@@ -381,6 +758,19 @@ export function registerEmailCommand(program: Command): void {
381
758
  thread
382
759
  .command("get <threadId>")
383
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
+ $ vellum email thread get thr_abc123
772
+ $ vellum email thread get thr_abc123 --json`,
773
+ )
384
774
  .action(async (threadId: string, _opts: unknown, cmd: Command) => {
385
775
  await run(cmd, async () => {
386
776
  const t = await svc.getThread(threadId);
@@ -395,9 +785,41 @@ export function registerEmailCommand(program: Command): void {
395
785
  .command("guardrails")
396
786
  .description("Manage email guardrails");
397
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
+ $ vellum email guardrails get
803
+ $ vellum email guardrails set --paused true
804
+ $ vellum email guardrails set --daily-cap 100
805
+ $ vellum email guardrails block "*@spam.com"`,
806
+ );
807
+
398
808
  guardrails
399
809
  .command("get")
400
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.
818
+
819
+ Examples:
820
+ $ vellum email guardrails get
821
+ $ vellum email guardrails get --json`,
822
+ )
401
823
  .action((_opts: unknown, cmd: Command) => {
402
824
  output({ ok: true, ...svc.getGuardrails() }, getJson(cmd));
403
825
  });
@@ -407,6 +829,22 @@ export function registerEmailCommand(program: Command): void {
407
829
  .description("Update guardrail settings")
408
830
  .option("--paused <value>", "Pause outbound (true/false)")
409
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.
837
+
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.
842
+
843
+ Examples:
844
+ $ vellum email guardrails set --paused true
845
+ $ vellum email guardrails set --paused false --daily-cap 50
846
+ $ vellum email guardrails set --daily-cap 0`,
847
+ )
410
848
  .action((opts: { paused?: string; dailyCap?: string }, cmd: Command) => {
411
849
  const updates: { paused?: boolean; dailyCap?: number } = {};
412
850
  if (opts.paused !== undefined) {
@@ -427,6 +865,21 @@ export function registerEmailCommand(program: Command): void {
427
865
  guardrails
428
866
  .command("block <pattern>")
429
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
+ $ vellum email guardrails block "*@spam.com"
881
+ $ vellum email guardrails block "marketing@*"`,
882
+ )
430
883
  .action((pattern: string, _opts: unknown, cmd: Command) => {
431
884
  const rule = svc.addRule("block", pattern);
432
885
  output({ ok: true, rule }, getJson(cmd));
@@ -435,6 +888,21 @@ export function registerEmailCommand(program: Command): void {
435
888
  guardrails
436
889
  .command("allow <pattern>")
437
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
+ $ vellum email guardrails allow "*@partner.com"
904
+ $ vellum email guardrails allow "vip@example.com"`,
905
+ )
438
906
  .action((pattern: string, _opts: unknown, cmd: Command) => {
439
907
  const rule = svc.addRule("allow", pattern);
440
908
  output({ ok: true, rule }, getJson(cmd));
@@ -443,6 +911,18 @@ export function registerEmailCommand(program: Command): void {
443
911
  guardrails
444
912
  .command("rules")
445
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
+ $ vellum email guardrails rules
924
+ $ vellum email guardrails rules --json`,
925
+ )
446
926
  .action((_opts: unknown, cmd: Command) => {
447
927
  output({ ok: true, rules: svc.listAddressRules() }, getJson(cmd));
448
928
  });
@@ -450,6 +930,20 @@ export function registerEmailCommand(program: Command): void {
450
930
  guardrails
451
931
  .command("unrule <ruleId>")
452
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
+ $ vellum email guardrails unrule rule_abc123
945
+ $ vellum email guardrails rules # list rules to find the ID first`,
946
+ )
453
947
  .action((ruleId: string, _opts: unknown, cmd: Command) => {
454
948
  if (svc.removeRule(ruleId)) {
455
949
  output({ ok: true, ruleId, action: "removed" }, getJson(cmd));