@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
@@ -0,0 +1,471 @@
1
+ import type { Command } from "commander";
2
+
3
+ import {
4
+ getAssistantContactMetadata,
5
+ getContact,
6
+ listContacts,
7
+ mergeContacts,
8
+ searchContacts,
9
+ } from "../contacts/contact-store.js";
10
+ import type { ContactRole } from "../contacts/types.js";
11
+ import { initializeDb } from "../memory/db.js";
12
+ import {
13
+ createIngressInvite,
14
+ listIngressInvites,
15
+ redeemIngressInvite,
16
+ redeemVoiceInviteCode,
17
+ revokeIngressInvite,
18
+ } from "../runtime/invite-service.js";
19
+ import { writeOutput } from "./integrations.js";
20
+
21
+ export function registerContactsCommand(program: Command): void {
22
+ const contacts = program
23
+ .command("contacts")
24
+ .description("Manage and query the contact graph")
25
+ .option("--json", "Machine-readable compact JSON output");
26
+
27
+ contacts.addHelpText(
28
+ "after",
29
+ `
30
+ Contacts represent people and entities the assistant interacts with. Each
31
+ contact is identified by a UUID, has a role (contact or guardian), and
32
+ can be linked to external identifiers — phone numbers,
33
+ Telegram IDs, email addresses — via channel memberships. The contact graph
34
+ is the source of truth for identity resolution across all channels.
35
+
36
+ Examples:
37
+ $ vellum contacts list
38
+ $ vellum contacts get abc-123
39
+ $ vellum contacts merge keep-id merge-id
40
+ $ vellum contacts invites list`,
41
+ );
42
+
43
+ contacts
44
+ .command("list")
45
+ .description("List contacts")
46
+ .option("--role <role>", "Filter by role (default: contact)", "contact")
47
+ .option("--limit <limit>", "Maximum number of contacts to return")
48
+ .option("--query <query>", "Search query to filter contacts")
49
+ .addHelpText(
50
+ "after",
51
+ `
52
+ Lists contacts with optional filtering. The --role flag accepts: contact
53
+ or guardian (defaults to contact). The --limit flag sets
54
+ the maximum number of results (defaults to 50).
55
+
56
+ When --query is provided, a full-text search is performed across contact
57
+ names and linked external identifiers (phone numbers, emails, Telegram
58
+ usernames). Without --query, returns all contacts matching the role filter.
59
+
60
+ Examples:
61
+ $ vellum contacts list
62
+ $ vellum contacts list --role guardian
63
+ $ vellum contacts list --query "john" --limit 10
64
+ $ vellum contacts list --role guardian --json`,
65
+ )
66
+ .action(
67
+ async (
68
+ opts: {
69
+ role?: string;
70
+ limit?: string;
71
+ query?: string;
72
+ },
73
+ cmd: Command,
74
+ ) => {
75
+ try {
76
+ initializeDb();
77
+ const role = opts.role as ContactRole | undefined;
78
+ const limit = opts.limit ? Number(opts.limit) : undefined;
79
+
80
+ const effectiveLimit = limit ?? 50;
81
+
82
+ const results = opts.query
83
+ ? searchContacts({ query: opts.query, role, limit: effectiveLimit })
84
+ : listContacts(effectiveLimit, role);
85
+
86
+ writeOutput(cmd, { ok: true, contacts: results });
87
+ } catch (err) {
88
+ const message = err instanceof Error ? err.message : String(err);
89
+ writeOutput(cmd, { ok: false, error: message });
90
+ process.exitCode = 1;
91
+ }
92
+ },
93
+ );
94
+
95
+ contacts
96
+ .command("get <id>")
97
+ .description("Get a contact by ID")
98
+ .addHelpText(
99
+ "after",
100
+ `
101
+ Arguments:
102
+ id UUID of the contact to retrieve
103
+
104
+ Returns the full contact record including role, display name, and all
105
+ channel memberships (phone numbers, Telegram IDs, email addresses, etc.).
106
+ For assistant-type contacts, additional assistant metadata is included.
107
+
108
+ Examples:
109
+ $ vellum contacts get 7a3b1c2d-4e5f-6789-abcd-ef0123456789
110
+ $ vellum contacts get abc-123 --json`,
111
+ )
112
+ .action(async (id: string, _opts: unknown, cmd: Command) => {
113
+ try {
114
+ initializeDb();
115
+ const contact = getContact(id);
116
+ if (!contact) {
117
+ writeOutput(cmd, { ok: false, error: "Contact not found" });
118
+ process.exitCode = 1;
119
+ return;
120
+ }
121
+ const assistantMeta =
122
+ contact.contactType === "assistant"
123
+ ? getAssistantContactMetadata(contact.id)
124
+ : undefined;
125
+ writeOutput(cmd, {
126
+ ok: true,
127
+ contact,
128
+ assistantMetadata: assistantMeta ?? undefined,
129
+ });
130
+ } catch (err) {
131
+ const message = err instanceof Error ? err.message : String(err);
132
+ writeOutput(cmd, { ok: false, error: message });
133
+ process.exitCode = 1;
134
+ }
135
+ });
136
+
137
+ contacts
138
+ .command("merge <keepId> <mergeId>")
139
+ .description("Merge two contacts")
140
+ .addHelpText(
141
+ "after",
142
+ `
143
+ Arguments:
144
+ keepId UUID of the surviving contact that will absorb the other
145
+ mergeId UUID of the contact to be merged and deleted
146
+
147
+ All channel memberships, conversation history, and metadata from mergeId
148
+ are transferred to keepId. After the merge, mergeId is permanently deleted.
149
+ This operation is irreversible.
150
+
151
+ Examples:
152
+ $ vellum contacts merge 7a3b1c2d-4e5f-6789-abcd-ef0123456789 9f8e7d6c-5b4a-3210-fedc-ba9876543210
153
+ $ vellum contacts merge keep-id merge-id --json`,
154
+ )
155
+ .action(
156
+ async (keepId: string, mergeId: string, _opts: unknown, cmd: Command) => {
157
+ try {
158
+ initializeDb();
159
+ const contact = mergeContacts(keepId, mergeId);
160
+ writeOutput(cmd, { ok: true, contact });
161
+ } catch (err) {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ writeOutput(cmd, { ok: false, error: message });
164
+ process.exitCode = 1;
165
+ }
166
+ },
167
+ );
168
+
169
+ const invites = contacts
170
+ .command("invites")
171
+ .description("Manage contact invites");
172
+
173
+ invites.addHelpText(
174
+ "after",
175
+ `
176
+ Invites are tokens that grant channel access when redeemed. Each invite is
177
+ tied to a source channel (telegram, voice, sms, email, whatsapp) and can
178
+ optionally have usage limits, expiration, and notes. When redeemed, the
179
+ invite creates a channel membership linking a contact to an external
180
+ identifier on the source channel.
181
+
182
+ Examples:
183
+ $ vellum contacts invites list
184
+ $ vellum contacts invites create --source-channel telegram
185
+ $ vellum contacts invites revoke abc-123
186
+ $ vellum contacts invites redeem --token xyz-789 --source-channel telegram --external-user-id 12345`,
187
+ );
188
+
189
+ invites
190
+ .command("list", { isDefault: true })
191
+ .description("List invites")
192
+ .option("--source-channel <sourceChannel>", "Filter by source channel")
193
+ .option("--status <status>", "Filter by invite status")
194
+ .addHelpText(
195
+ "after",
196
+ `
197
+ Lists all invites with optional filtering by source channel or status.
198
+ Returns invite tokens, their source channels, usage counts, and expiration.
199
+
200
+ Examples:
201
+ $ vellum contacts invites list
202
+ $ vellum contacts invites list --source-channel telegram
203
+ $ vellum contacts invites list --status active
204
+ $ vellum contacts invites list --source-channel voice --json`,
205
+ )
206
+ .action(
207
+ async (
208
+ opts: { sourceChannel?: string; status?: string },
209
+ cmd: Command,
210
+ ) => {
211
+ try {
212
+ initializeDb();
213
+ const result = listIngressInvites({
214
+ sourceChannel: opts.sourceChannel,
215
+ status: opts.status,
216
+ });
217
+ if (result.ok) {
218
+ writeOutput(cmd, { ok: true, invites: result.data });
219
+ } else {
220
+ writeOutput(cmd, result);
221
+ }
222
+ } catch (err) {
223
+ const message = err instanceof Error ? err.message : String(err);
224
+ writeOutput(cmd, { ok: false, error: message });
225
+ process.exitCode = 1;
226
+ }
227
+ },
228
+ );
229
+
230
+ invites
231
+ .command("create")
232
+ .description("Create a new invite")
233
+ .requiredOption(
234
+ "--source-channel <channel>",
235
+ "Source channel (e.g. telegram, voice, sms, email, whatsapp)",
236
+ )
237
+ .option("--note <note>", "Optional note")
238
+ .option("--max-uses <n>", "Max redemptions")
239
+ .option("--expires-in-ms <ms>", "Expiry duration in milliseconds")
240
+ .option(
241
+ "--contact-name <name>",
242
+ "Contact name for personalizing instructions",
243
+ )
244
+ .option(
245
+ "--expected-external-user-id <id>",
246
+ "E.164 phone number (required for voice invites)",
247
+ )
248
+ .option("--friend-name <name>", "Friend name (required for voice invites)")
249
+ .option(
250
+ "--guardian-name <name>",
251
+ "Guardian name (required for voice invites)",
252
+ )
253
+ .addHelpText(
254
+ "after",
255
+ `
256
+ Creates a new invite token for the specified source channel. The --source-channel
257
+ flag is required and must be one of: telegram, voice, sms, email, whatsapp.
258
+
259
+ Optional fields:
260
+ --note Free-text note attached to the invite
261
+ --max-uses Maximum number of times the invite can be redeemed
262
+ --expires-in-ms Expiry duration in milliseconds from creation
263
+ --contact-name Name used to personalize invite instructions
264
+
265
+ Voice invites require three additional fields:
266
+ --expected-external-user-id E.164 phone number of the expected caller (e.g. +15551234567)
267
+ --friend-name Name the contact uses for the assistant's owner
268
+ --guardian-name Name of the guardian associated with this invite
269
+
270
+ Examples:
271
+ $ vellum contacts invites create --source-channel telegram --note "For Alice" --max-uses 1
272
+ $ vellum contacts invites create --source-channel voice --expected-external-user-id "+15551234567" --friend-name "Alice" --guardian-name "Bob" --contact-name "Alice Smith"`,
273
+ )
274
+ .action(
275
+ async (
276
+ opts: {
277
+ sourceChannel: string;
278
+ note?: string;
279
+ maxUses?: string;
280
+ expiresInMs?: string;
281
+ contactName?: string;
282
+ expectedExternalUserId?: string;
283
+ friendName?: string;
284
+ guardianName?: string;
285
+ },
286
+ cmd: Command,
287
+ ) => {
288
+ try {
289
+ const maxUses = opts.maxUses ? Number(opts.maxUses) : undefined;
290
+ if (maxUses !== undefined && !Number.isFinite(maxUses)) {
291
+ writeOutput(cmd, {
292
+ ok: false,
293
+ error: `--max-uses must be a number, got: ${opts.maxUses}`,
294
+ });
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+ const expiresInMs = opts.expiresInMs
299
+ ? Number(opts.expiresInMs)
300
+ : undefined;
301
+ if (expiresInMs !== undefined && !Number.isFinite(expiresInMs)) {
302
+ writeOutput(cmd, {
303
+ ok: false,
304
+ error: `--expires-in-ms must be a number, got: ${opts.expiresInMs}`,
305
+ });
306
+ process.exitCode = 1;
307
+ return;
308
+ }
309
+ initializeDb();
310
+ const result = await createIngressInvite({
311
+ sourceChannel: opts.sourceChannel,
312
+ note: opts.note,
313
+ maxUses,
314
+ expiresInMs,
315
+ contactName: opts.contactName,
316
+ expectedExternalUserId: opts.expectedExternalUserId,
317
+ friendName: opts.friendName,
318
+ guardianName: opts.guardianName,
319
+ });
320
+ if (result.ok) {
321
+ writeOutput(cmd, { ok: true, invite: result.data });
322
+ } else {
323
+ writeOutput(cmd, result);
324
+ }
325
+ if (!result.ok) {
326
+ process.exitCode = 1;
327
+ }
328
+ } catch (err) {
329
+ const message = err instanceof Error ? err.message : String(err);
330
+ writeOutput(cmd, { ok: false, error: message });
331
+ process.exitCode = 1;
332
+ }
333
+ },
334
+ );
335
+
336
+ invites
337
+ .command("revoke <inviteId>")
338
+ .description("Revoke an active invite")
339
+ .addHelpText(
340
+ "after",
341
+ `
342
+ Arguments:
343
+ inviteId UUID of the invite to revoke
344
+
345
+ Revokes an active invite so it can no longer be redeemed. Already-redeemed
346
+ channel memberships are not affected. Returns the updated invite record.
347
+
348
+ Examples:
349
+ $ vellum contacts invites revoke 7a3b1c2d-4e5f-6789-abcd-ef0123456789
350
+ $ vellum contacts invites revoke abc-123 --json`,
351
+ )
352
+ .action(async (inviteId: string, _opts: unknown, cmd: Command) => {
353
+ try {
354
+ initializeDb();
355
+ const result = revokeIngressInvite(inviteId);
356
+ if (result.ok) {
357
+ writeOutput(cmd, { ok: true, invite: result.data });
358
+ } else {
359
+ writeOutput(cmd, result);
360
+ }
361
+ if (!result.ok) process.exitCode = 1;
362
+ } catch (err) {
363
+ const message = err instanceof Error ? err.message : String(err);
364
+ writeOutput(cmd, { ok: false, error: message });
365
+ process.exitCode = 1;
366
+ }
367
+ });
368
+
369
+ invites
370
+ .command("redeem")
371
+ .description("Redeem an invite via token or voice code")
372
+ .option("--token <token>", "Invite token")
373
+ .option("--source-channel <channel>", "Channel for redemption")
374
+ .option("--external-user-id <id>", "External user ID")
375
+ .option("--external-chat-id <id>", "External chat ID")
376
+ .option("--code <code>", "6-digit voice code")
377
+ .option(
378
+ "--caller-external-user-id <phone>",
379
+ "E.164 phone number for voice code redemption",
380
+ )
381
+ .option("--assistant-id <id>", "Assistant ID for voice code redemption")
382
+ .addHelpText(
383
+ "after",
384
+ `
385
+ Two redemption modes:
386
+
387
+ 1. Token-based redemption: Provide --token, --source-channel, and at
388
+ least one of --external-user-id or --external-chat-id. Creates a
389
+ channel membership linking the contact to the external identifier.
390
+
391
+ 2. Voice-code-based redemption: Provide --code (6-digit code) and
392
+ --caller-external-user-id (E.164 phone number). Optionally include
393
+ --assistant-id to scope the redemption to a specific assistant.
394
+
395
+ Examples:
396
+ $ vellum contacts invites redeem --token xyz-789 --source-channel telegram --external-user-id 12345
397
+ $ vellum contacts invites redeem --code 123456 --caller-external-user-id "+15551234567"
398
+ $ vellum contacts invites redeem --code 654321 --caller-external-user-id "+15559876543" --assistant-id asst-abc --json`,
399
+ )
400
+ .action(
401
+ async (
402
+ opts: {
403
+ token?: string;
404
+ sourceChannel?: string;
405
+ externalUserId?: string;
406
+ externalChatId?: string;
407
+ code?: string;
408
+ callerExternalUserId?: string;
409
+ assistantId?: string;
410
+ },
411
+ cmd: Command,
412
+ ) => {
413
+ try {
414
+ initializeDb();
415
+ if (opts.code) {
416
+ if (!opts.callerExternalUserId) {
417
+ writeOutput(cmd, {
418
+ ok: false,
419
+ error:
420
+ "--caller-external-user-id is required for voice code redemption",
421
+ });
422
+ process.exitCode = 1;
423
+ return;
424
+ }
425
+ const result = redeemVoiceInviteCode({
426
+ code: opts.code,
427
+ callerExternalUserId: opts.callerExternalUserId,
428
+ sourceChannel: "voice",
429
+ ...(opts.assistantId ? { assistantId: opts.assistantId } : {}),
430
+ });
431
+ if (result.ok) {
432
+ writeOutput(cmd, {
433
+ ok: true,
434
+ type: result.type,
435
+ memberId: result.memberId,
436
+ ...(result.type === "redeemed"
437
+ ? { inviteId: result.inviteId }
438
+ : {}),
439
+ });
440
+ } else {
441
+ writeOutput(cmd, { ok: false, error: result.reason });
442
+ process.exitCode = 1;
443
+ }
444
+ } else {
445
+ const result = redeemIngressInvite({
446
+ token: opts.token,
447
+ sourceChannel: opts.sourceChannel,
448
+ ...(opts.externalUserId
449
+ ? { externalUserId: opts.externalUserId }
450
+ : {}),
451
+ ...(opts.externalChatId
452
+ ? { externalChatId: opts.externalChatId }
453
+ : {}),
454
+ });
455
+ if (result.ok) {
456
+ writeOutput(cmd, { ok: true, invite: result.data });
457
+ } else {
458
+ writeOutput(cmd, result);
459
+ }
460
+ if (!result.ok) {
461
+ process.exitCode = 1;
462
+ }
463
+ }
464
+ } catch (err) {
465
+ const message = err instanceof Error ? err.message : String(err);
466
+ writeOutput(cmd, { ok: false, error: message });
467
+ process.exitCode = 1;
468
+ }
469
+ },
470
+ );
471
+ }