@vellumai/assistant 0.4.34 → 0.4.36

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 (251) 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 +4 -1
  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 +91 -43
  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 +806 -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 +491 -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} +133 -242
  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 +177 -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 +175 -145
  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 +12 -1
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  175. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  176. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  177. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  178. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  179. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  180. package/src/memory/migrations/114-notifications.ts +37 -15
  181. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  182. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  183. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  184. package/src/memory/migrations/index.ts +2 -0
  185. package/src/memory/migrations/schema-introspection.ts +18 -0
  186. package/src/memory/schema-migration.ts +1 -0
  187. package/src/memory/shared-app-links-store.ts +1 -1
  188. package/src/messaging/registry.ts +27 -0
  189. package/src/notifications/README.md +79 -70
  190. package/src/notifications/broadcaster.ts +2 -1
  191. package/src/notifications/conversation-pairing.ts +147 -13
  192. package/src/notifications/copy-composer.ts +7 -3
  193. package/src/notifications/destination-resolver.ts +14 -1
  194. package/src/notifications/emit-signal.ts +3 -2
  195. package/src/notifications/signal.ts +105 -1
  196. package/src/notifications/types.ts +16 -0
  197. package/src/permissions/checker.ts +29 -3
  198. package/src/permissions/prompter.ts +11 -3
  199. package/src/runtime/access-request-helper.ts +2 -1
  200. package/src/runtime/auth/route-policy.ts +7 -1
  201. package/src/runtime/channel-invite-transport.ts +40 -63
  202. package/src/runtime/channel-invite-transports/email.ts +13 -39
  203. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  204. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  205. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  206. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  207. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  208. package/src/runtime/channel-readiness-service.ts +202 -45
  209. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  210. package/src/runtime/guardian-outbound-actions.ts +8 -5
  211. package/src/runtime/http-server.ts +5 -9
  212. package/src/runtime/http-types.ts +13 -1
  213. package/src/runtime/invite-instruction-generator.ts +178 -0
  214. package/src/runtime/invite-service.ts +22 -25
  215. package/src/runtime/migrations/migration-transport.ts +13 -0
  216. package/src/runtime/routes/app-routes.ts +1 -1
  217. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  218. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  219. package/src/runtime/routes/contact-routes.ts +54 -26
  220. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
  221. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  222. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  223. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  224. package/src/runtime/routes/integration-routes.ts +1 -1
  225. package/src/runtime/routes/invite-routes.ts +1 -1
  226. package/src/runtime/routes/secret-routes.ts +31 -7
  227. package/src/runtime/routes/surface-content-routes.ts +104 -0
  228. package/src/runtime/routes/twilio-routes.ts +32 -1
  229. package/src/runtime/routes/usage-routes.ts +114 -0
  230. package/src/runtime/tool-grant-request-helper.ts +2 -1
  231. package/src/security/encrypted-store.ts +9 -5
  232. package/src/security/keychain-broker-client.ts +393 -0
  233. package/src/security/secure-keys.ts +106 -321
  234. package/src/tools/apps/executors.ts +73 -0
  235. package/src/tools/browser/auto-navigate.ts +15 -6
  236. package/src/tools/browser/chrome-cdp.ts +211 -0
  237. package/src/tools/browser/network-recorder.test.ts +83 -0
  238. package/src/tools/browser/network-recorder.ts +8 -7
  239. package/src/tools/browser/x-auto-navigate.ts +12 -6
  240. package/src/tools/credentials/policy-types.ts +24 -0
  241. package/src/tools/credentials/vault.ts +22 -27
  242. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  243. package/src/tools/permission-checker.ts +1 -0
  244. package/src/tools/types.ts +2 -0
  245. package/src/tools/ui-surface/definitions.ts +1 -2
  246. package/src/tools/watch/watch-state.ts +2 -0
  247. package/src/__tests__/key-migration.test.ts +0 -240
  248. package/src/__tests__/keychain.test.ts +0 -286
  249. package/src/cli/core-commands.ts +0 -899
  250. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  251. package/src/security/keychain.ts +0 -490
@@ -61,6 +61,21 @@ export function registerInfluencerCommand(program: Command): void {
61
61
  )
62
62
  .option("--json", "Machine-readable JSON output");
63
63
 
64
+ inf.addHelpText(
65
+ "after",
66
+ `
67
+ Researches influencers via the Chrome extension relay, which automates
68
+ browsing on each platform. The user must be logged into each target
69
+ platform (Instagram, TikTok, X/Twitter) in Chrome for the relay to work.
70
+
71
+ Supported platforms: instagram, tiktok, twitter (X).
72
+
73
+ Examples:
74
+ $ vellum influencer search "fitness coach" --platforms instagram,tiktok
75
+ $ vellum influencer profile natgeo --platform instagram
76
+ $ vellum influencer compare instagram:nike twitter:nike tiktok:nike`,
77
+ );
78
+
64
79
  // =========================================================================
65
80
  // search — search for influencers across platforms
66
81
  // =========================================================================
@@ -88,6 +103,27 @@ export function registerInfluencerCommand(program: Command): void {
88
103
  )
89
104
  .option("--limit <n>", "Max results per platform", "10")
90
105
  .option("--verified", "Only return verified accounts")
106
+ .addHelpText(
107
+ "after",
108
+ `
109
+ Arguments:
110
+ query Search query — niche, topic, or keywords (e.g. "fitness coach")
111
+
112
+ --platforms filters which platforms to search. Defaults to all three:
113
+ instagram, tiktok, twitter. Provide a comma-separated list to narrow.
114
+
115
+ --min-followers and --max-followers accept human-friendly notation:
116
+ 10k = 10,000 1.5m = 1,500,000 100k = 100,000
117
+ Plain integers are also accepted (e.g. 50000).
118
+
119
+ --limit caps the number of results returned per platform (default: 10).
120
+ --verified restricts results to verified/blue-check accounts only.
121
+
122
+ Examples:
123
+ $ vellum influencer search "vegan food" --min-followers 10k --max-followers 1m
124
+ $ vellum influencer search "tech reviewer" --platforms tiktok --limit 5 --verified
125
+ $ vellum influencer search "streetwear" --platforms instagram,twitter --min-followers 50k`,
126
+ )
91
127
  .action(
92
128
  async (
93
129
  query: string,
@@ -147,6 +183,23 @@ export function registerInfluencerCommand(program: Command): void {
147
183
  "Platform (instagram, tiktok, or twitter)",
148
184
  "instagram",
149
185
  )
186
+ .addHelpText(
187
+ "after",
188
+ `
189
+ Arguments:
190
+ username The influencer's handle without the @ prefix (e.g. "natgeo", not "@natgeo")
191
+
192
+ --platform selects which platform to look up. Defaults to instagram.
193
+ Valid values: instagram, tiktok, twitter.
194
+
195
+ Returns detailed profile data including follower count, bio, engagement
196
+ metrics, and recent post statistics.
197
+
198
+ Examples:
199
+ $ vellum influencer profile natgeo --platform instagram
200
+ $ vellum influencer profile charlidamelio --platform tiktok
201
+ $ vellum influencer profile elonmusk --platform twitter`,
202
+ )
150
203
  .action(
151
204
  async (username: string, opts: { platform: string }, cmd: Command) => {
152
205
  await run(cmd, async () => {
@@ -187,6 +240,25 @@ export function registerInfluencerCommand(program: Command): void {
187
240
  "<influencers...>",
188
241
  "Space-separated list of platform:username pairs (e.g. instagram:nike twitter:nike tiktok:nike)",
189
242
  )
243
+ .addHelpText(
244
+ "after",
245
+ `
246
+ Arguments:
247
+ influencers Space-separated platform:username pairs (e.g. instagram:nike twitter:nike)
248
+
249
+ Each argument must be in platform:username format. Valid platforms:
250
+ instagram, tiktok, twitter. If no platform prefix is provided, defaults
251
+ to instagram.
252
+
253
+ Returns side-by-side profile data for all specified influencers,
254
+ useful for comparing follower counts, engagement rates, and content
255
+ metrics across platforms or between competing accounts.
256
+
257
+ Examples:
258
+ $ vellum influencer compare instagram:nike twitter:nike tiktok:nike
259
+ $ vellum influencer compare instagram:natgeo instagram:discoverearth
260
+ $ vellum influencer compare tiktok:charlidamelio tiktok:addisonre`,
261
+ )
190
262
  .action(async (influencers: string[], _opts: unknown, cmd: Command) => {
191
263
  await run(cmd, async () => {
192
264
  const parsed = influencers.map((inf) => {
@@ -10,7 +10,6 @@ import {
10
10
  mintEdgeRelayToken,
11
11
  } from "../runtime/auth/token-service.js";
12
12
 
13
- type IngressChannel = "telegram" | "voice" | "sms";
14
13
  type GuardianChannel = "telegram" | "voice" | "sms";
15
14
 
16
15
  function asRecord(value: unknown): Record<string, unknown> {
@@ -20,7 +19,7 @@ function asRecord(value: unknown): Record<string, unknown> {
20
19
  return value as Record<string, unknown>;
21
20
  }
22
21
 
23
- function shouldOutputJson(cmd: Command): boolean {
22
+ export function shouldOutputJson(cmd: Command): boolean {
24
23
  let current: Command | null = cmd;
25
24
  while (current) {
26
25
  if ((current.opts() as { json?: boolean }).json) return true;
@@ -29,7 +28,7 @@ function shouldOutputJson(cmd: Command): boolean {
29
28
  return false;
30
29
  }
31
30
 
32
- function writeOutput(cmd: Command, payload: unknown): void {
31
+ export function writeOutput(cmd: Command, payload: unknown): void {
33
32
  const compact = shouldOutputJson(cmd);
34
33
  process.stdout.write(
35
34
  compact
@@ -49,7 +48,9 @@ function getGatewayToken(): string {
49
48
  return mintEdgeRelayToken();
50
49
  }
51
50
 
52
- function toQueryString(params: Record<string, string | undefined>): string {
51
+ export function toQueryString(
52
+ params: Record<string, string | undefined>,
53
+ ): string {
53
54
  const query = new URLSearchParams();
54
55
  for (const [key, value] of Object.entries(params)) {
55
56
  if (value) query.set(key, value);
@@ -107,7 +108,7 @@ function readVoiceConfig(): {
107
108
  // CLI-specific gateway helper — uses GATEWAY_AUTH_TOKEN env var for out-of-process
108
109
  // access. See runtime/gateway-internal-client.ts for daemon-internal usage which
109
110
  // mints fresh tokens.
110
- async function gatewayGet(path: string): Promise<unknown> {
111
+ export async function gatewayGet(path: string): Promise<unknown> {
111
112
  const gatewayBase = getGatewayInternalBaseUrl();
112
113
  const token = getGatewayToken();
113
114
 
@@ -141,7 +142,46 @@ async function gatewayGet(path: string): Promise<unknown> {
141
142
  return parsed;
142
143
  }
143
144
 
144
- async function runRead(
145
+ export async function gatewayPost(
146
+ path: string,
147
+ body: unknown,
148
+ ): Promise<unknown> {
149
+ const gatewayBase = getGatewayInternalBaseUrl();
150
+ const token = getGatewayToken();
151
+
152
+ const response = await fetch(`${gatewayBase}${path}`, {
153
+ method: "POST",
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ Accept: "application/json",
157
+ Authorization: `Bearer ${token}`,
158
+ },
159
+ body: JSON.stringify(body),
160
+ });
161
+
162
+ const rawBody = await response.text();
163
+ let parsed: unknown = { ok: false, error: rawBody };
164
+
165
+ if (rawBody.length > 0) {
166
+ try {
167
+ parsed = JSON.parse(rawBody) as unknown;
168
+ } catch {
169
+ parsed = { ok: false, error: rawBody };
170
+ }
171
+ }
172
+
173
+ if (!response.ok) {
174
+ const message =
175
+ typeof parsed === "object" && parsed && "error" in parsed
176
+ ? String((parsed as { error?: unknown }).error)
177
+ : `Gateway request failed (${response.status})`;
178
+ throw new Error(`${message} [${response.status}]`);
179
+ }
180
+
181
+ return parsed;
182
+ }
183
+
184
+ export async function runRead(
145
185
  cmd: Command,
146
186
  reader: () => Promise<unknown>,
147
187
  ): Promise<void> {
@@ -155,70 +195,64 @@ async function runRead(
155
195
  }
156
196
  }
157
197
 
158
- export function registerContactsCommand(program: Command): void {
159
- const contacts = program
160
- .command("contacts")
161
- .description("Manage and query the contact graph")
162
- .option("--json", "Machine-readable compact JSON output");
163
-
164
- contacts
165
- .command("list")
166
- .description("List contacts (calls /v1/contacts)")
167
- .option("--role <role>", "Filter by role (default: contact)", "contact")
168
- .option("--limit <limit>", "Maximum number of contacts to return")
169
- .option("--query <query>", "Search query to filter contacts")
170
- .action(
171
- async (
172
- opts: {
173
- role?: string;
174
- limit?: string;
175
- query?: string;
176
- },
177
- cmd: Command,
178
- ) => {
179
- const query = toQueryString({
180
- role: opts.role,
181
- limit: opts.limit,
182
- query: opts.query,
183
- });
184
- await runRead(cmd, async () => gatewayGet(`/v1/contacts${query}`));
185
- },
186
- );
187
-
188
- contacts
189
- .command("invites")
190
- .description("List contact invites")
191
- .option("--source-channel <sourceChannel>", "Filter by source channel")
192
- .option("--status <status>", "Filter by invite status")
193
- .action(
194
- async (
195
- opts: { sourceChannel?: IngressChannel; status?: string },
196
- cmd: Command,
197
- ) => {
198
- const query = toQueryString({
199
- sourceChannel: opts.sourceChannel,
200
- status: opts.status,
201
- });
202
- await runRead(cmd, async () =>
203
- gatewayGet(`/v1/contacts/invites${query}`),
204
- );
205
- },
206
- );
207
- }
208
-
209
198
  export function registerIntegrationsCommand(program: Command): void {
210
199
  const integrations = program
211
200
  .command("integrations")
212
201
  .description("Read integration and ingress status through the gateway API")
213
202
  .option("--json", "Machine-readable compact JSON output");
214
203
 
204
+ integrations.addHelpText(
205
+ "after",
206
+ `
207
+ Reads integration configuration and status through the gateway API. The
208
+ assistant must be running for most subcommands (telegram, twilio, guardian)
209
+ since they query the gateway. Exceptions: "ingress config" and "voice config"
210
+ read from the local config file and do not require the gateway.
211
+
212
+ Integration categories:
213
+ telegram Telegram bot configuration and webhook status
214
+ twilio Twilio credentials, phone numbers, and SMS compliance
215
+ guardian Guardian trust verification system for contacts
216
+ ingress Public ingress URL and local gateway target (config-only)
217
+ voice Voice/call readiness and ElevenLabs voice ID (config-only)
218
+
219
+ Examples:
220
+ $ vellum integrations telegram config
221
+ $ vellum integrations twilio numbers
222
+ $ vellum integrations guardian status --channel sms`,
223
+ );
224
+
215
225
  const telegram = integrations
216
226
  .command("telegram")
217
227
  .description("Telegram integration status");
218
228
 
229
+ telegram.addHelpText(
230
+ "after",
231
+ `
232
+ Checks the Telegram bot configuration status through the gateway API.
233
+ Requires the assistant to be running.
234
+
235
+ Examples:
236
+ $ vellum integrations telegram config
237
+ $ vellum integrations telegram config --json`,
238
+ );
239
+
219
240
  telegram
220
241
  .command("config")
221
242
  .description("Get Telegram integration configuration status")
243
+ .addHelpText(
244
+ "after",
245
+ `
246
+ Returns the Telegram bot token status, webhook URL, and bot username from
247
+ the gateway. Requires the assistant to be running.
248
+
249
+ The response includes whether a bot token is configured, the current webhook
250
+ endpoint, and the bot's Telegram username.
251
+
252
+ Examples:
253
+ $ vellum integrations telegram config
254
+ $ vellum integrations telegram config --json`,
255
+ )
222
256
  .action(async (_opts: unknown, cmd: Command) => {
223
257
  await runRead(cmd, async () =>
224
258
  gatewayGet("/v1/integrations/telegram/config"),
@@ -229,10 +263,38 @@ export function registerIntegrationsCommand(program: Command): void {
229
263
  .command("guardian")
230
264
  .description("Guardian verification status");
231
265
 
266
+ guardian.addHelpText(
267
+ "after",
268
+ `
269
+ Guardian is the trust verification system for contacts. It tracks whether
270
+ contacts on each channel have completed identity verification. Requires
271
+ the assistant to be running.
272
+
273
+ Examples:
274
+ $ vellum integrations guardian status
275
+ $ vellum integrations guardian status --channel voice`,
276
+ );
277
+
232
278
  guardian
233
279
  .command("status")
234
280
  .description("Get guardian status for a channel")
235
281
  .option("--channel <channel>", "Channel: telegram|voice|sms", "telegram")
282
+ .addHelpText(
283
+ "after",
284
+ `
285
+ Returns the guardian verification state for the specified channel. Requires
286
+ the assistant to be running.
287
+
288
+ The --channel flag accepts: telegram, voice, sms. Defaults to telegram if
289
+ not specified. The response includes whether guardian verification is active
290
+ and the current verification state for that channel.
291
+
292
+ Examples:
293
+ $ vellum integrations guardian status
294
+ $ vellum integrations guardian status --channel telegram
295
+ $ vellum integrations guardian status --channel voice
296
+ $ vellum integrations guardian status --channel sms --json`,
297
+ )
236
298
  .action(async (opts: { channel?: GuardianChannel }, cmd: Command) => {
237
299
  const channel = opts.channel ?? "telegram";
238
300
  await runRead(cmd, async () =>
@@ -246,9 +308,40 @@ export function registerIntegrationsCommand(program: Command): void {
246
308
  .command("twilio")
247
309
  .description("Twilio integration status");
248
310
 
311
+ twilio.addHelpText(
312
+ "after",
313
+ `
314
+ Covers Twilio credential status, phone number management, and SMS regulatory
315
+ compliance. All subcommands require the assistant to be running since they
316
+ query the gateway API.
317
+
318
+ Subcommands:
319
+ config Check Twilio credential status and phone number configuration
320
+ numbers List all Twilio incoming phone numbers
321
+ sms compliance Check SMS regulatory compliance status
322
+
323
+ Examples:
324
+ $ vellum integrations twilio config
325
+ $ vellum integrations twilio numbers
326
+ $ vellum integrations twilio sms compliance`,
327
+ );
328
+
249
329
  twilio
250
330
  .command("config")
251
331
  .description("Get Twilio credential and phone number status")
332
+ .addHelpText(
333
+ "after",
334
+ `
335
+ Checks the Twilio credential status and phone number configuration through
336
+ the gateway. Requires the assistant to be running.
337
+
338
+ The response includes whether the Twilio account SID and auth token are
339
+ configured, and the currently assigned phone number.
340
+
341
+ Examples:
342
+ $ vellum integrations twilio config
343
+ $ vellum integrations twilio config --json`,
344
+ )
252
345
  .action(async (_opts: unknown, cmd: Command) => {
253
346
  await runRead(cmd, async () =>
254
347
  gatewayGet("/v1/integrations/twilio/config"),
@@ -258,6 +351,19 @@ export function registerIntegrationsCommand(program: Command): void {
258
351
  twilio
259
352
  .command("numbers")
260
353
  .description("List Twilio incoming phone numbers")
354
+ .addHelpText(
355
+ "after",
356
+ `
357
+ Lists all incoming phone numbers associated with the configured Twilio
358
+ account. Requires the assistant to be running.
359
+
360
+ Returns an array of phone number objects with their SID, phone number,
361
+ friendly name, and capabilities.
362
+
363
+ Examples:
364
+ $ vellum integrations twilio numbers
365
+ $ vellum integrations twilio numbers --json`,
366
+ )
261
367
  .action(async (_opts: unknown, cmd: Command) => {
262
368
  await runRead(cmd, async () =>
263
369
  gatewayGet("/v1/integrations/twilio/numbers"),
@@ -266,9 +372,36 @@ export function registerIntegrationsCommand(program: Command): void {
266
372
 
267
373
  const twilioSms = twilio.command("sms").description("Twilio SMS status");
268
374
 
375
+ twilioSms.addHelpText(
376
+ "after",
377
+ `
378
+ Covers SMS regulatory compliance for the configured Twilio account. All
379
+ subcommands require the assistant to be running since they query the gateway API.
380
+
381
+ Subcommands:
382
+ compliance Check SMS regulatory compliance status
383
+
384
+ Examples:
385
+ $ vellum integrations twilio sms compliance
386
+ $ vellum integrations twilio sms compliance --json`,
387
+ );
388
+
269
389
  twilioSms
270
390
  .command("compliance")
271
391
  .description("Get Twilio SMS compliance status")
392
+ .addHelpText(
393
+ "after",
394
+ `
395
+ Checks the SMS regulatory compliance status for the configured Twilio
396
+ account. Requires the assistant to be running.
397
+
398
+ Returns the current compliance state, including whether the account is
399
+ approved for SMS messaging and any outstanding compliance requirements.
400
+
401
+ Examples:
402
+ $ vellum integrations twilio sms compliance
403
+ $ vellum integrations twilio sms compliance --json`,
404
+ )
272
405
  .action(async (_opts: unknown, cmd: Command) => {
273
406
  await runRead(cmd, async () =>
274
407
  gatewayGet("/v1/integrations/twilio/sms/compliance"),
@@ -278,6 +411,16 @@ export function registerIntegrationsCommand(program: Command): void {
278
411
  twilio
279
412
  .command("sms-compliance")
280
413
  .description('Alias for "vellum integrations twilio sms compliance"')
414
+ .addHelpText(
415
+ "after",
416
+ `
417
+ Shortcut alias for "vellum integrations twilio sms compliance". Prefer
418
+ the canonical path for scripts and documentation.
419
+
420
+ Examples:
421
+ $ vellum integrations twilio sms-compliance
422
+ $ vellum integrations twilio sms compliance # canonical form`,
423
+ )
281
424
  .action(async (_opts: unknown, cmd: Command) => {
282
425
  await runRead(cmd, async () =>
283
426
  gatewayGet("/v1/integrations/twilio/sms/compliance"),
@@ -288,18 +431,66 @@ export function registerIntegrationsCommand(program: Command): void {
288
431
  .command("ingress")
289
432
  .description("Trusted contact membership and invite status");
290
433
 
434
+ ingress.addHelpText(
435
+ "after",
436
+ `
437
+ Shows the public ingress URL and local gateway target URL. Reads from the
438
+ local config file and does not require the gateway to be running.
439
+
440
+ Examples:
441
+ $ vellum integrations ingress config`,
442
+ );
443
+
291
444
  ingress
292
445
  .command("config")
293
446
  .description("Get public ingress URL and local gateway target")
447
+ .addHelpText(
448
+ "after",
449
+ `
450
+ Shows the public ingress URL and the local gateway target URL. Reads from
451
+ the local config file and does not require the gateway to be running.
452
+
453
+ The response includes whether ingress is enabled, the configured public base
454
+ URL (if any), and the local gateway target address. Ingress is considered
455
+ enabled if explicitly set to true or if a publicBaseUrl is configured.
456
+
457
+ Examples:
458
+ $ vellum integrations ingress config
459
+ $ vellum integrations ingress config --json`,
460
+ )
294
461
  .action(async (_opts: unknown, cmd: Command) => {
295
462
  await runRead(cmd, async () => readIngressConfig());
296
463
  });
297
464
 
298
465
  const voice = integrations.command("voice").description("Voice setup status");
299
466
 
467
+ voice.addHelpText(
468
+ "after",
469
+ `
470
+ Shows voice and call readiness configuration. Reads from the local config
471
+ file and does not require the gateway to be running.
472
+
473
+ Examples:
474
+ $ vellum integrations voice config`,
475
+ );
476
+
300
477
  voice
301
478
  .command("config")
302
479
  .description("Get voice and call readiness config")
480
+ .addHelpText(
481
+ "after",
482
+ `
483
+ Shows voice and call readiness status. Reads from the local config file and
484
+ does not require the gateway to be running.
485
+
486
+ The response includes whether calls are enabled, the active ElevenLabs voice
487
+ ID (falls back to default if not configured), whether a custom voice ID is
488
+ set, and whether the default voice is in use.
489
+
490
+ Examples:
491
+ $ vellum integrations voice config
492
+ $ vellum integrations voice config --json`,
493
+ )
303
494
  .action(async (_opts: unknown, cmd: Command) => {
304
495
  await runRead(cmd, async () => readVoiceConfig());
305
496
  });
@@ -0,0 +1,114 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { API_KEY_PROVIDERS } from "../config/loader.js";
4
+ import {
5
+ deleteSecureKey,
6
+ getSecureKey,
7
+ setSecureKey,
8
+ } from "../security/secure-keys.js";
9
+ import { getCliLogger } from "../util/logger.js";
10
+
11
+ const log = getCliLogger("cli");
12
+
13
+ export function registerKeysCommand(program: Command): void {
14
+ const keys = program
15
+ .command("keys")
16
+ .description("Manage API keys in secure storage");
17
+
18
+ keys.addHelpText(
19
+ "after",
20
+ `
21
+ Keys are stored in secure local storage and are never written to disk in
22
+ plaintext. Each key is identified by provider name.
23
+
24
+ Known providers: ${API_KEY_PROVIDERS.join(", ")}
25
+
26
+ Examples:
27
+ $ vellum keys list
28
+ $ vellum keys set anthropic sk-ant-...
29
+ $ vellum keys delete openai`,
30
+ );
31
+
32
+ keys
33
+ .command("list")
34
+ .description("List all stored API key names")
35
+ .addHelpText(
36
+ "after",
37
+ `
38
+ Checks each known provider (${API_KEY_PROVIDERS.join(", ")}) and prints the
39
+ names of providers that have a stored key. Providers without a stored key are
40
+ omitted from the output.
41
+
42
+ Examples:
43
+ $ vellum keys list`,
44
+ )
45
+ .action(() => {
46
+ const stored: string[] = [];
47
+ for (const provider of API_KEY_PROVIDERS) {
48
+ const value = getSecureKey(provider);
49
+ if (value) stored.push(provider);
50
+ }
51
+ if (stored.length === 0) {
52
+ log.info("No API keys stored");
53
+ } else {
54
+ for (const name of stored) {
55
+ log.info(` ${name}`);
56
+ }
57
+ }
58
+ });
59
+
60
+ keys
61
+ .command("set <provider> <key>")
62
+ .description("Store an API key (e.g. vellum keys set anthropic sk-ant-...)")
63
+ .addHelpText(
64
+ "after",
65
+ `
66
+ Arguments:
67
+ provider Provider name (e.g. anthropic, openai, gemini)
68
+ key The API key value to store
69
+
70
+ If a key already exists for the given provider, it is silently overwritten.
71
+
72
+ Examples:
73
+ $ vellum keys set anthropic sk-ant-abc123
74
+ $ vellum keys set openai sk-proj-xyz789
75
+ $ vellum keys set fireworks fw-abc123`,
76
+ )
77
+ .action((provider: string, key: string) => {
78
+ if (setSecureKey(provider, key)) {
79
+ log.info(`Stored API key for "${provider}"`);
80
+ } else {
81
+ log.error(`Failed to store API key for "${provider}"`);
82
+ process.exit(1);
83
+ }
84
+ });
85
+
86
+ keys
87
+ .command("delete <provider>")
88
+ .description("Delete a stored API key")
89
+ .addHelpText(
90
+ "after",
91
+ `
92
+ Arguments:
93
+ provider Provider name whose key should be removed from secure storage
94
+
95
+ Removes the API key for the given provider from secure local storage. If
96
+ no key exists for the provider, exits with an error.
97
+
98
+ Examples:
99
+ $ vellum keys delete openai
100
+ $ vellum keys delete anthropic`,
101
+ )
102
+ .action((provider: string) => {
103
+ const result = deleteSecureKey(provider);
104
+ if (result === "deleted") {
105
+ log.info(`Deleted API key for "${provider}"`);
106
+ } else if (result === "error") {
107
+ log.error(`Failed to delete API key for "${provider}": storage error`);
108
+ process.exit(1);
109
+ } else {
110
+ log.error(`No API key found for "${provider}"`);
111
+ process.exit(1);
112
+ }
113
+ });
114
+ }