@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,564 @@
1
+ import type { Command } from "commander";
2
+
3
+ import {
4
+ deleteSecureKey,
5
+ getSecureKey,
6
+ setSecureKey,
7
+ } from "../security/secure-keys.js";
8
+ import {
9
+ assertMetadataWritable,
10
+ type CredentialMetadata,
11
+ deleteCredentialMetadata,
12
+ getCredentialMetadata,
13
+ getCredentialMetadataById,
14
+ listCredentialMetadata,
15
+ upsertCredentialMetadata,
16
+ } from "../tools/credentials/metadata-store.js";
17
+ import { getCliLogger } from "../util/logger.js";
18
+ import { shouldOutputJson, writeOutput } from "./integrations.js";
19
+
20
+ const log = getCliLogger("cli");
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Shared helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Parse a `service:field` name string. Returns the parsed pair or undefined
28
+ * if the format is invalid (no colon or empty segments).
29
+ */
30
+ function parseCredentialName(
31
+ name: string,
32
+ ): { service: string; field: string } | undefined {
33
+ const colonIndex = name.indexOf(":");
34
+ if (colonIndex <= 0 || colonIndex >= name.length - 1) return undefined;
35
+ return {
36
+ service: name.slice(0, colonIndex),
37
+ field: name.slice(colonIndex + 1),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Scrub a secret value for display. Shows `****` + last 4 characters for
43
+ * secrets longer than 4 chars, `****` for secrets 4 chars or fewer, and
44
+ * `(not set)` when no secret is stored.
45
+ */
46
+ function scrubSecret(secret: string | undefined): string {
47
+ if (secret == null || secret.length === 0) return "(not set)";
48
+ if (secret.length <= 4) return "****";
49
+ return "****" + secret.slice(-4);
50
+ }
51
+
52
+ /**
53
+ * Build a structured credential output object suitable for both `inspect`
54
+ * and `list` responses. Produces an identical shape for every credential.
55
+ */
56
+ function buildCredentialOutput(
57
+ metadata: CredentialMetadata,
58
+ secret: string | undefined,
59
+ ): Record<string, unknown> {
60
+ return {
61
+ ok: true,
62
+ service: metadata.service,
63
+ field: metadata.field,
64
+ credentialId: metadata.credentialId,
65
+ scrubbedValue: scrubSecret(secret),
66
+ hasSecret: secret != null && secret.length > 0,
67
+ alias: metadata.alias ?? null,
68
+ usageDescription: metadata.usageDescription ?? null,
69
+ allowedTools: metadata.allowedTools,
70
+ allowedDomains: metadata.allowedDomains,
71
+ accountInfo: metadata.accountInfo ?? null,
72
+ grantedScopes: metadata.grantedScopes ?? null,
73
+ expiresAt: metadata.expiresAt
74
+ ? new Date(metadata.expiresAt).toISOString()
75
+ : null,
76
+ createdAt: new Date(metadata.createdAt).toISOString(),
77
+ updatedAt: new Date(metadata.updatedAt).toISOString(),
78
+ injectionTemplateCount: metadata.injectionTemplates?.length ?? 0,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Print a human-readable view of a single credential to the logger.
84
+ */
85
+ function printCredentialHuman(output: Record<string, unknown>): void {
86
+ log.info(` ${output.service}:${output.field}`);
87
+ log.info(` ID: ${output.credentialId}`);
88
+ log.info(` Value: ${output.scrubbedValue}`);
89
+ if (output.alias) log.info(` Label: ${output.alias}`);
90
+ if (output.usageDescription)
91
+ log.info(` Description: ${output.usageDescription}`);
92
+ if (
93
+ Array.isArray(output.allowedTools) &&
94
+ (output.allowedTools as string[]).length > 0
95
+ )
96
+ log.info(
97
+ ` Tools: ${(output.allowedTools as string[]).join(", ")}`,
98
+ );
99
+ if (
100
+ Array.isArray(output.allowedDomains) &&
101
+ (output.allowedDomains as string[]).length > 0
102
+ )
103
+ log.info(
104
+ ` Domains: ${(output.allowedDomains as string[]).join(", ")}`,
105
+ );
106
+ if (output.accountInfo) log.info(` Account: ${output.accountInfo}`);
107
+ if (output.grantedScopes)
108
+ log.info(
109
+ ` Scopes: ${(output.grantedScopes as string[]).join(", ")}`,
110
+ );
111
+ if (output.expiresAt) log.info(` Expires: ${output.expiresAt}`);
112
+ log.info(` Created: ${output.createdAt}`);
113
+ log.info(` Updated: ${output.updatedAt}`);
114
+ if ((output.injectionTemplateCount as number) > 0)
115
+ log.info(` Templates: ${output.injectionTemplateCount}`);
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Command registration
120
+ // ---------------------------------------------------------------------------
121
+
122
+ export function registerCredentialsCommand(program: Command): void {
123
+ const credential = program
124
+ .command("credentials")
125
+ .description(
126
+ "Manage credentials in the encrypted vault (API keys, tokens, passwords)",
127
+ )
128
+ .option("--json", "Machine-readable compact JSON output");
129
+
130
+ credential.addHelpText(
131
+ "after",
132
+ `
133
+ Credentials are identified by name in service:field format, matching the
134
+ storage convention used internally (credential:{service}:{field}):
135
+
136
+ twilio:account_sid Twilio account SID
137
+ twilio:auth_token Twilio auth token
138
+ telegram:bot_token Telegram bot token
139
+ slack_channel:bot_token Slack channel bot token
140
+ github:token GitHub personal access token
141
+ agentmail:api_key AgentMail API key
142
+
143
+ Secrets are stored in AES-256-GCM encrypted storage. Metadata (policy,
144
+ timestamps, labels) is tracked separately and never contains secret values.
145
+
146
+ Examples:
147
+ $ vellum credentials list
148
+ $ vellum credentials list --search twilio
149
+ $ vellum credentials set twilio:account_sid AC1234567890
150
+ $ vellum credentials inspect twilio:account_sid
151
+ $ vellum credentials delete twilio:auth_token`,
152
+ );
153
+
154
+ // -------------------------------------------------------------------------
155
+ // list
156
+ // -------------------------------------------------------------------------
157
+
158
+ credential
159
+ .command("list")
160
+ .description("List all stored credentials with metadata and masked values")
161
+ .option(
162
+ "--search <query>",
163
+ "Filter credentials by substring match on service, field, label, or description",
164
+ )
165
+ .addHelpText(
166
+ "after",
167
+ `
168
+ Lists all credentials in the vault. Each entry includes the same fields as
169
+ "inspect" — scrubbed value, timestamps, policy, and metadata.
170
+
171
+ The --search flag filters results by case-insensitive substring match against
172
+ the credential's service name, field name, label, or description. For example, --search
173
+ twilio matches twilio:account_sid, twilio:auth_token, and twilio:phone_number.
174
+
175
+ Returns an array of credential objects. Empty array if no credentials exist
176
+ or none match the search query.
177
+
178
+ Examples:
179
+ $ vellum credentials list
180
+ $ vellum credentials list --search twilio
181
+ $ vellum credentials list --search bot_token
182
+ $ vellum credentials list --json`,
183
+ )
184
+ .action((opts: { search?: string }, cmd: Command) => {
185
+ try {
186
+ let allMetadata = listCredentialMetadata();
187
+
188
+ if (opts.search) {
189
+ const query = opts.search.toLowerCase();
190
+ allMetadata = allMetadata.filter((m) => {
191
+ const service = m.service.toLowerCase();
192
+ const field = m.field.toLowerCase();
193
+ const alias = (m.alias ?? "").toLowerCase();
194
+ const description = (m.usageDescription ?? "").toLowerCase();
195
+ return (
196
+ service.includes(query) ||
197
+ field.includes(query) ||
198
+ alias.includes(query) ||
199
+ description.includes(query)
200
+ );
201
+ });
202
+ }
203
+
204
+ const credentials = allMetadata.map((m) => {
205
+ const secret = getSecureKey(`credential:${m.service}:${m.field}`);
206
+ return buildCredentialOutput(m, secret);
207
+ });
208
+
209
+ writeOutput(cmd, { ok: true, credentials });
210
+
211
+ if (!shouldOutputJson(cmd)) {
212
+ if (credentials.length === 0) {
213
+ log.info("No credentials found");
214
+ } else {
215
+ log.info(`${credentials.length} credential(s):\n`);
216
+ for (const cred of credentials) {
217
+ printCredentialHuman(cred);
218
+ log.info("");
219
+ }
220
+ }
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
+ // set
231
+ // -------------------------------------------------------------------------
232
+
233
+ credential
234
+ .command("set <name> <value>")
235
+ .description("Store a secret and create or update its metadata")
236
+ .option("--label <label>", 'Human-friendly label (e.g. "prod", "work")')
237
+ .option("--description <description>", "What this credential is used for")
238
+ .option(
239
+ "--allowed-tools <tools>",
240
+ "Comma-separated tool names that may use this credential",
241
+ )
242
+ .addHelpText(
243
+ "after",
244
+ `
245
+ Arguments:
246
+ name Credential name in service:field format (e.g. twilio:account_sid)
247
+ value The secret value to store
248
+
249
+ If the credential already exists, the secret is overwritten and metadata is
250
+ updated with any provided flags. Omitted flags leave existing metadata intact.
251
+
252
+ Examples:
253
+ $ vellum credentials set twilio:account_sid AC1234567890
254
+ $ vellum credentials set fal:api_key key_live_abc --label "fal-prod" --description "Image generation"
255
+ $ vellum credentials set github:token ghp_abc --allowed-tools "bash,host_bash"`,
256
+ )
257
+ .action(
258
+ (
259
+ name: string,
260
+ value: string,
261
+ opts: { label?: string; description?: string; allowedTools?: string },
262
+ cmd: Command,
263
+ ) => {
264
+ try {
265
+ const parsed = parseCredentialName(name);
266
+ if (!parsed) {
267
+ writeOutput(cmd, {
268
+ ok: false,
269
+ error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
270
+ });
271
+ process.exitCode = 1;
272
+ return;
273
+ }
274
+
275
+ const { service, field } = parsed;
276
+ const storageKey = `credential:${service}:${field}`;
277
+
278
+ assertMetadataWritable();
279
+
280
+ const stored = setSecureKey(storageKey, value);
281
+ if (!stored) {
282
+ writeOutput(cmd, {
283
+ ok: false,
284
+ error: `Failed to store secret for ${name}`,
285
+ });
286
+ process.exitCode = 1;
287
+ return;
288
+ }
289
+
290
+ const allowedTools = opts.allowedTools
291
+ ? opts.allowedTools.split(",").map((t) => t.trim())
292
+ : undefined;
293
+
294
+ const metadata = upsertCredentialMetadata(service, field, {
295
+ alias: opts.label,
296
+ usageDescription: opts.description,
297
+ allowedTools,
298
+ });
299
+
300
+ writeOutput(cmd, {
301
+ ok: true,
302
+ credentialId: metadata.credentialId,
303
+ service,
304
+ field,
305
+ });
306
+
307
+ if (!shouldOutputJson(cmd)) {
308
+ log.info(
309
+ `Stored credential ${service}:${field} (${metadata.credentialId})`,
310
+ );
311
+ }
312
+ } catch (err) {
313
+ const message = err instanceof Error ? err.message : String(err);
314
+ writeOutput(cmd, { ok: false, error: message });
315
+ process.exitCode = 1;
316
+ }
317
+ },
318
+ );
319
+
320
+ // -------------------------------------------------------------------------
321
+ // delete
322
+ // -------------------------------------------------------------------------
323
+
324
+ credential
325
+ .command("delete <name>")
326
+ .description("Remove a secret and its metadata from the vault")
327
+ .addHelpText(
328
+ "after",
329
+ `
330
+ Arguments:
331
+ name Credential name in service:field format (e.g. twilio:account_sid)
332
+
333
+ Deletes both the encrypted secret and all associated metadata (policy,
334
+ timestamps, injection templates). This action cannot be undone.
335
+
336
+ Examples:
337
+ $ vellum credentials delete twilio:auth_token
338
+ $ vellum credentials delete github:token`,
339
+ )
340
+ .action((name: string, _opts: unknown, cmd: Command) => {
341
+ try {
342
+ const parsed = parseCredentialName(name);
343
+ if (!parsed) {
344
+ writeOutput(cmd, {
345
+ ok: false,
346
+ error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
347
+ });
348
+ process.exitCode = 1;
349
+ return;
350
+ }
351
+
352
+ const { service, field } = parsed;
353
+ const storageKey = `credential:${service}:${field}`;
354
+
355
+ assertMetadataWritable();
356
+
357
+ const secretResult = deleteSecureKey(storageKey);
358
+ if (secretResult === "error") {
359
+ writeOutput(cmd, {
360
+ ok: false,
361
+ error: "Failed to delete credential from secure storage",
362
+ });
363
+ process.exitCode = 1;
364
+ return;
365
+ }
366
+
367
+ const metadataDeleted = deleteCredentialMetadata(service, field);
368
+
369
+ if (secretResult !== "deleted" && !metadataDeleted) {
370
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
371
+ process.exitCode = 1;
372
+ return;
373
+ }
374
+
375
+ writeOutput(cmd, { ok: true, service, field });
376
+
377
+ if (!shouldOutputJson(cmd)) {
378
+ log.info(`Deleted credential ${service}:${field}`);
379
+ }
380
+ } catch (err) {
381
+ const message = err instanceof Error ? err.message : String(err);
382
+ writeOutput(cmd, { ok: false, error: message });
383
+ process.exitCode = 1;
384
+ }
385
+ });
386
+
387
+ // -------------------------------------------------------------------------
388
+ // inspect
389
+ // -------------------------------------------------------------------------
390
+
391
+ credential
392
+ .command("inspect <name>")
393
+ .description("Show metadata and a masked preview of a stored credential")
394
+ .addHelpText(
395
+ "after",
396
+ `
397
+ Arguments:
398
+ name Credential name in service:field format, or a credential UUID
399
+
400
+ Shows everything known about a credential without revealing the secret value.
401
+ The secret is masked to show only the last 4 characters (e.g. ****c123).
402
+
403
+ Displayed fields include: label, creation/update timestamps, allowed tools,
404
+ allowed domains, OAuth2 scopes, account info, and injection template count.
405
+
406
+ Examples:
407
+ $ vellum credentials inspect twilio:account_sid
408
+ $ vellum credentials inspect 7a3b1c2d-4e5f-6789-abcd-ef0123456789
409
+ $ vellum credentials inspect --json slack_channel:bot_token`,
410
+ )
411
+ .action((name: string, _opts: unknown, cmd: Command) => {
412
+ try {
413
+ let metadata: CredentialMetadata | undefined;
414
+ let storageKey: string;
415
+
416
+ if (name.includes(":")) {
417
+ const parsed = parseCredentialName(name);
418
+ if (!parsed) {
419
+ writeOutput(cmd, {
420
+ ok: false,
421
+ error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
422
+ });
423
+ process.exitCode = 1;
424
+ return;
425
+ }
426
+ metadata = getCredentialMetadata(parsed.service, parsed.field);
427
+ storageKey = `credential:${parsed.service}:${parsed.field}`;
428
+ } else {
429
+ metadata = getCredentialMetadataById(name);
430
+ if (metadata) {
431
+ storageKey = `credential:${metadata.service}:${metadata.field}`;
432
+ } else {
433
+ // No metadata found by UUID, and we can't determine the storage key
434
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
435
+ process.exitCode = 1;
436
+ return;
437
+ }
438
+ }
439
+
440
+ const secret = getSecureKey(storageKey);
441
+
442
+ if (!metadata && (secret == null || secret.length === 0)) {
443
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
444
+ process.exitCode = 1;
445
+ return;
446
+ }
447
+
448
+ // If we have a secret but no metadata, we still need metadata for the output.
449
+ // This can happen if someone stored a key directly without going through the
450
+ // credential set command. Build a minimal output in that case.
451
+ if (!metadata) {
452
+ // We only get here for the service:field path where we have storageKey
453
+ // but no metadata record. Output what we can.
454
+ const parsed = parseCredentialName(name)!;
455
+ writeOutput(cmd, {
456
+ ok: true,
457
+ service: parsed.service,
458
+ field: parsed.field,
459
+ credentialId: null,
460
+ scrubbedValue: scrubSecret(secret),
461
+ hasSecret: secret != null && secret.length > 0,
462
+ alias: null,
463
+ usageDescription: null,
464
+ allowedTools: [],
465
+ allowedDomains: [],
466
+ accountInfo: null,
467
+ grantedScopes: null,
468
+ expiresAt: null,
469
+ createdAt: null,
470
+ updatedAt: null,
471
+ injectionTemplateCount: 0,
472
+ });
473
+
474
+ if (!shouldOutputJson(cmd)) {
475
+ log.info(` ${parsed.service}:${parsed.field}`);
476
+ log.info(` Value: ${scrubSecret(secret)}`);
477
+ log.info(" (no metadata record)");
478
+ }
479
+ return;
480
+ }
481
+
482
+ const output = buildCredentialOutput(metadata, secret);
483
+ writeOutput(cmd, output);
484
+
485
+ if (!shouldOutputJson(cmd)) {
486
+ printCredentialHuman(output);
487
+ }
488
+ } catch (err) {
489
+ const message = err instanceof Error ? err.message : String(err);
490
+ writeOutput(cmd, { ok: false, error: message });
491
+ process.exitCode = 1;
492
+ }
493
+ });
494
+
495
+ // -------------------------------------------------------------------------
496
+ // reveal
497
+ // -------------------------------------------------------------------------
498
+
499
+ credential
500
+ .command("reveal <name>")
501
+ .description("Print the plaintext value of a credential")
502
+ .addHelpText(
503
+ "after",
504
+ `
505
+ Arguments:
506
+ name Credential name in service:field format, or a credential UUID
507
+
508
+ Prints the raw secret value to stdout for piping into other tools. In JSON
509
+ mode the value is returned as {"ok": true, "value": "..."}. In human mode
510
+ only the bare secret is printed (no labels or decoration) so it can be
511
+ captured with shell substitution, e.g. $(vellum credentials reveal twilio:auth_token).
512
+
513
+ Examples:
514
+ $ vellum credentials reveal twilio:auth_token
515
+ $ vellum credentials reveal 7a3b1c2d-4e5f-6789-abcd-ef0123456789
516
+ $ vellum credentials reveal --json twilio:account_sid
517
+ $ export TWILIO_TOKEN=$(vellum credentials reveal twilio:auth_token)`,
518
+ )
519
+ .action((name: string, _opts: unknown, cmd: Command) => {
520
+ try {
521
+ let storageKey: string;
522
+
523
+ if (name.includes(":")) {
524
+ const parsed = parseCredentialName(name);
525
+ if (!parsed) {
526
+ writeOutput(cmd, {
527
+ ok: false,
528
+ error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
529
+ });
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+ storageKey = `credential:${parsed.service}:${parsed.field}`;
534
+ } else {
535
+ const metadata = getCredentialMetadataById(name);
536
+ if (metadata) {
537
+ storageKey = `credential:${metadata.service}:${metadata.field}`;
538
+ } else {
539
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
540
+ process.exitCode = 1;
541
+ return;
542
+ }
543
+ }
544
+
545
+ const secret = getSecureKey(storageKey);
546
+
547
+ if (secret == null || secret.length === 0) {
548
+ writeOutput(cmd, { ok: false, error: "Credential not found" });
549
+ process.exitCode = 1;
550
+ return;
551
+ }
552
+
553
+ if (shouldOutputJson(cmd)) {
554
+ writeOutput(cmd, { ok: true, value: secret });
555
+ } else {
556
+ process.stdout.write(secret + "\n");
557
+ }
558
+ } catch (err) {
559
+ const message = err instanceof Error ? err.message : String(err);
560
+ writeOutput(cmd, { ok: false, error: message });
561
+ process.exitCode = 1;
562
+ }
563
+ });
564
+ }
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { startCli } from "../cli.js";
4
+ import { shouldAutoStartDaemon } from "../daemon/connection-policy.js";
5
+ import { ensureDaemonRunning } from "../daemon/lifecycle.js";
6
+
7
+ export function registerDefaultAction(program: Command): void {
8
+ program.action(async () => {
9
+ if (shouldAutoStartDaemon()) {
10
+ await ensureDaemonRunning();
11
+ }
12
+ await startCli();
13
+ });
14
+ }
package/src/cli/dev.ts ADDED
@@ -0,0 +1,131 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+
4
+ import type { Command } from "commander";
5
+
6
+ import { getDaemonStatus, stopDaemon } from "../daemon/lifecycle.js";
7
+ import { getCliLogger } from "../util/logger.js";
8
+
9
+ const log = getCliLogger("cli");
10
+
11
+ export function registerDevCommand(program: Command): void {
12
+ program
13
+ .command("dev")
14
+ .description("Run the assistant in dev mode")
15
+ .option(
16
+ "--watch",
17
+ "Auto-restart on source file changes (disruptive during Claude Code sessions)",
18
+ )
19
+ .addHelpText(
20
+ "after",
21
+ `
22
+ Starts the assistant in foreground dev mode for local development. If an
23
+ existing assistant is running, it is stopped first (waits up to 5 seconds
24
+ for an unresponsive assistant before force-killing it).
25
+
26
+ Behavioral notes:
27
+ - Sets VELLUM_DEBUG=1 for DEBUG-level logging
28
+ - Sets VELLUM_LOG_STDERR=1 so logs stream to stderr (visible in terminal)
29
+ - Sets BASE_DATA_DIR to the repository root
30
+ - The assistant runs in the foreground; press Ctrl+C to stop
31
+
32
+ The --watch flag passes bun --watch to the child process, which
33
+ auto-restarts the assistant whenever source files change. This is useful
34
+ during development but disruptive if a Claude Code session is active,
35
+ since the restart kills the running assistant mid-conversation.
36
+
37
+ Examples:
38
+ $ vellum dev
39
+ $ vellum dev --watch`,
40
+ )
41
+ .action(async (opts: { watch?: boolean }) => {
42
+ let status = await getDaemonStatus();
43
+ if (status.running) {
44
+ log.info("Stopping existing assistant...");
45
+ const stopResult = await stopDaemon();
46
+ if (!stopResult.stopped && stopResult.reason === "stop_failed") {
47
+ log.error(
48
+ "Failed to stop existing assistant — process survived SIGKILL",
49
+ );
50
+ process.exit(1);
51
+ }
52
+ } else if (status.pid) {
53
+ // PID file references a live process but the socket is unresponsive.
54
+ // This can happen during the daemon startup window before the socket
55
+ // is bound. Wait briefly for it to come up before replacing.
56
+ log.info(
57
+ "Assistant process alive but socket unresponsive — waiting for startup...",
58
+ );
59
+ const maxWait = 5000;
60
+ const interval = 500;
61
+ let waited = 0;
62
+ let resolved = false;
63
+ while (waited < maxWait) {
64
+ await new Promise((r) => setTimeout(r, interval));
65
+ waited += interval;
66
+ status = await getDaemonStatus();
67
+ if (status.running) {
68
+ // Socket came up — stop the daemon normally.
69
+ log.info("Assistant became responsive, stopping it...");
70
+ const stopResult = await stopDaemon();
71
+ if (!stopResult.stopped && stopResult.reason === "stop_failed") {
72
+ log.error(
73
+ "Failed to stop existing assistant — process survived SIGKILL",
74
+ );
75
+ process.exit(1);
76
+ }
77
+ resolved = true;
78
+ break;
79
+ }
80
+ if (!status.pid) {
81
+ // Process exited on its own — PID file already cleaned up.
82
+ resolved = true;
83
+ break;
84
+ }
85
+ }
86
+ if (!resolved) {
87
+ // Still alive but unresponsive after waiting — stop it via stopDaemon()
88
+ // which handles SIGTERM → SIGKILL escalation and PID file cleanup.
89
+ log.info("Assistant still unresponsive after wait — stopping it...");
90
+ const stopResult = await stopDaemon();
91
+ if (!stopResult.stopped && stopResult.reason === "stop_failed") {
92
+ log.error(
93
+ "Failed to stop existing assistant — process survived SIGKILL",
94
+ );
95
+ process.exit(1);
96
+ }
97
+ }
98
+ }
99
+
100
+ const mainPath = `${import.meta.dirname}/../daemon/main.ts`;
101
+
102
+ const useWatch = opts.watch === true;
103
+ log.info(
104
+ `Starting assistant in dev mode${
105
+ useWatch ? " with file watching" : ""
106
+ } (Ctrl+C to stop)`,
107
+ );
108
+
109
+ const repoRoot = join(import.meta.dirname, "..", "..", "..");
110
+ const args = useWatch ? ["--watch", "run", mainPath] : ["run", mainPath];
111
+ const child = spawn("bun", args, {
112
+ stdio: "inherit",
113
+ env: {
114
+ ...process.env,
115
+ BASE_DATA_DIR: repoRoot,
116
+ VELLUM_LOG_STDERR: "1",
117
+ VELLUM_DEBUG: "1",
118
+ },
119
+ });
120
+
121
+ const forward = (signal: NodeJS.Signals) => {
122
+ child.kill(signal);
123
+ };
124
+ process.on("SIGINT", () => forward("SIGINT"));
125
+ process.on("SIGTERM", () => forward("SIGTERM"));
126
+
127
+ child.on("exit", (code) => {
128
+ process.exit(code ?? 0);
129
+ });
130
+ });
131
+ }