@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
@@ -0,0 +1,85 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { getRecentInvocations } from "../memory/tool-usage-store.js";
4
+ import { getCliLogger } from "../util/logger.js";
5
+
6
+ const log = getCliLogger("cli");
7
+
8
+ export function registerAuditCommand(program: Command): void {
9
+ program
10
+ .command("audit")
11
+ .description("Show recent tool invocations")
12
+ .option("-l, --limit <n>", "Number of entries to show", "20")
13
+ .addHelpText(
14
+ "after",
15
+ `
16
+ Reads from the local tool invocation log stored by the assistant. Each row
17
+ represents one tool call the assistant made, including what was invoked,
18
+ how the approval system classified it, and how long it took.
19
+
20
+ Table columns:
21
+ Timestamp When the tool was invoked (UTC, YYYY-MM-DD HH:MM:SS)
22
+ Tool Tool name (e.g. bash, read_file, write_file, browser)
23
+ Input Truncated summary of the tool input (command, path, etc.)
24
+ Decision Approval decision: allow, deny, or ask
25
+ Risk Risk classification: none, low, medium, high
26
+ Duration Wall-clock execution time (e.g. 120ms, 1.3s)
27
+
28
+ Examples:
29
+ $ vellum audit
30
+ $ vellum audit --limit 50`,
31
+ )
32
+ .action((opts: { limit: string }) => {
33
+ const limit = parseInt(opts.limit, 10) || 20;
34
+ const rows = getRecentInvocations(limit);
35
+ if (rows.length === 0) {
36
+ log.info("No tool invocations recorded");
37
+ return;
38
+ }
39
+ const tsW = 20;
40
+ const toolW = 14;
41
+ const inputW = 30;
42
+ const decW = 8;
43
+ const riskW = 8;
44
+ const durW = 8;
45
+ log.info(
46
+ "Timestamp".padEnd(tsW) +
47
+ "Tool".padEnd(toolW) +
48
+ "Input".padEnd(inputW) +
49
+ "Decision".padEnd(decW) +
50
+ "Risk".padEnd(riskW) +
51
+ "Duration",
52
+ );
53
+ log.info("-".repeat(tsW + toolW + inputW + decW + riskW + durW));
54
+ for (const r of rows) {
55
+ const ts = new Date(r.createdAt)
56
+ .toISOString()
57
+ .slice(0, 19)
58
+ .replace("T", " ");
59
+ let inputSummary = "";
60
+ try {
61
+ const parsed = JSON.parse(r.input);
62
+ if (parsed.command) inputSummary = parsed.command;
63
+ else if (parsed.path) inputSummary = parsed.path;
64
+ else inputSummary = r.input;
65
+ } catch {
66
+ inputSummary = r.input;
67
+ }
68
+ if (inputSummary.length > inputW - 2) {
69
+ inputSummary = inputSummary.slice(0, inputW - 4) + "..";
70
+ }
71
+ const dur =
72
+ r.durationMs < 1000
73
+ ? `${r.durationMs}ms`
74
+ : `${(r.durationMs / 1000).toFixed(1)}s`;
75
+ log.info(
76
+ ts.padEnd(tsW) +
77
+ r.toolName.padEnd(toolW) +
78
+ inputSummary.padEnd(inputW) +
79
+ r.decision.padEnd(decW) +
80
+ r.riskLevel.padEnd(riskW) +
81
+ dur,
82
+ );
83
+ }
84
+ });
85
+ }
@@ -0,0 +1,369 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ import type { Command } from "commander";
6
+
7
+ import { getCliLogger } from "../util/logger.js";
8
+
9
+ const log = getCliLogger("cli");
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types & constants
13
+ // ---------------------------------------------------------------------------
14
+
15
+ type AutonomyTier = "auto" | "draft" | "notify";
16
+
17
+ const AUTONOMY_TIERS: readonly AutonomyTier[] = ["auto", "draft", "notify"];
18
+
19
+ interface AutonomyConfig {
20
+ defaultTier: AutonomyTier;
21
+ channelDefaults: Record<string, AutonomyTier>;
22
+ categoryOverrides: Record<string, AutonomyTier>;
23
+ contactOverrides: Record<string, AutonomyTier>;
24
+ }
25
+
26
+ const DEFAULT_AUTONOMY_CONFIG: AutonomyConfig = {
27
+ defaultTier: "notify",
28
+ channelDefaults: {},
29
+ categoryOverrides: {},
30
+ contactOverrides: {},
31
+ };
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Config persistence
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function getConfigPath(): string {
38
+ const root = join(process.env.BASE_DATA_DIR?.trim() || homedir(), ".vellum");
39
+ return join(root, "workspace", "autonomy.json");
40
+ }
41
+
42
+ function isValidTier(value: unknown): value is AutonomyTier {
43
+ return (
44
+ typeof value === "string" && AUTONOMY_TIERS.includes(value as AutonomyTier)
45
+ );
46
+ }
47
+
48
+ function validateTierRecord(raw: unknown): Record<string, AutonomyTier> {
49
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
50
+ const result: Record<string, AutonomyTier> = {};
51
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
52
+ if (isValidTier(value)) {
53
+ result[key] = value;
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function validateConfig(raw: unknown): AutonomyConfig {
60
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
61
+ return structuredClone(DEFAULT_AUTONOMY_CONFIG);
62
+ }
63
+ const obj = raw as Record<string, unknown>;
64
+ return {
65
+ defaultTier: isValidTier(obj.defaultTier)
66
+ ? obj.defaultTier
67
+ : DEFAULT_AUTONOMY_CONFIG.defaultTier,
68
+ channelDefaults: validateTierRecord(obj.channelDefaults),
69
+ categoryOverrides: validateTierRecord(obj.categoryOverrides),
70
+ contactOverrides: validateTierRecord(obj.contactOverrides),
71
+ };
72
+ }
73
+
74
+ function loadConfig(): AutonomyConfig {
75
+ const configPath = getConfigPath();
76
+ if (!existsSync(configPath)) {
77
+ return structuredClone(DEFAULT_AUTONOMY_CONFIG);
78
+ }
79
+ try {
80
+ const raw = readFileSync(configPath, "utf-8");
81
+ return validateConfig(JSON.parse(raw));
82
+ } catch {
83
+ log.error("Warning: failed to parse autonomy config; using defaults");
84
+ return structuredClone(DEFAULT_AUTONOMY_CONFIG);
85
+ }
86
+ }
87
+
88
+ function saveConfig(config: AutonomyConfig): void {
89
+ const configPath = getConfigPath();
90
+ mkdirSync(dirname(configPath), { recursive: true });
91
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
92
+ }
93
+
94
+ function applyUpdate(updates: Partial<AutonomyConfig>): AutonomyConfig {
95
+ const current = loadConfig();
96
+ if (updates.defaultTier !== undefined) {
97
+ current.defaultTier = updates.defaultTier;
98
+ }
99
+ if (updates.channelDefaults !== undefined) {
100
+ current.channelDefaults = {
101
+ ...current.channelDefaults,
102
+ ...updates.channelDefaults,
103
+ };
104
+ }
105
+ if (updates.categoryOverrides !== undefined) {
106
+ current.categoryOverrides = {
107
+ ...current.categoryOverrides,
108
+ ...updates.categoryOverrides,
109
+ };
110
+ }
111
+ if (updates.contactOverrides !== undefined) {
112
+ current.contactOverrides = {
113
+ ...current.contactOverrides,
114
+ ...updates.contactOverrides,
115
+ };
116
+ }
117
+ saveConfig(current);
118
+ return current;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Output helpers
123
+ // ---------------------------------------------------------------------------
124
+
125
+ function outputJson(data: unknown): void {
126
+ process.stdout.write(JSON.stringify(data) + "\n");
127
+ }
128
+
129
+ function getJson(cmd: Command): boolean {
130
+ let c: Command | null = cmd;
131
+ while (c) {
132
+ if ((c.opts() as { json?: boolean }).json) return true;
133
+ c = c.parent;
134
+ }
135
+ return false;
136
+ }
137
+
138
+ function formatConfigForHuman(config: AutonomyConfig): string {
139
+ const lines: string[] = [` Default tier: ${config.defaultTier}`];
140
+
141
+ const channelEntries = Object.entries(config.channelDefaults);
142
+ if (channelEntries.length > 0) {
143
+ lines.push(" Channel defaults:");
144
+ for (const [channel, tier] of channelEntries) {
145
+ lines.push(` ${channel}: ${tier}`);
146
+ }
147
+ } else {
148
+ lines.push(" Channel defaults: (none)");
149
+ }
150
+
151
+ const categoryEntries = Object.entries(config.categoryOverrides);
152
+ if (categoryEntries.length > 0) {
153
+ lines.push(" Category overrides:");
154
+ for (const [category, tier] of categoryEntries) {
155
+ lines.push(` ${category}: ${tier}`);
156
+ }
157
+ } else {
158
+ lines.push(" Category overrides: (none)");
159
+ }
160
+
161
+ const contactEntries = Object.entries(config.contactOverrides);
162
+ if (contactEntries.length > 0) {
163
+ lines.push(" Contact overrides:");
164
+ for (const [contactId, tier] of contactEntries) {
165
+ lines.push(` ${contactId}: ${tier}`);
166
+ }
167
+ } else {
168
+ lines.push(" Contact overrides: (none)");
169
+ }
170
+
171
+ return lines.join("\n");
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Command registration
176
+ // ---------------------------------------------------------------------------
177
+
178
+ export function registerAutonomyCommand(program: Command): void {
179
+ const autonomy = program
180
+ .command("autonomy")
181
+ .description("View and configure autonomy tiers")
182
+ .option("--json", "Machine-readable JSON output");
183
+
184
+ autonomy.addHelpText(
185
+ "after",
186
+ `
187
+ Autonomy tiers control how independently the assistant acts on each message:
188
+
189
+ auto Assistant acts independently — sends messages, executes actions
190
+ without asking for permission.
191
+ draft Assistant creates drafts for your approval before sending or
192
+ executing. You review and confirm each action.
193
+ notify Assistant notifies you about incoming messages and events but
194
+ does not act or draft. Purely informational.
195
+
196
+ Resolution order (first match wins):
197
+ 1. Contact override — per-contact tier set via --contact
198
+ 2. Category override — per-category tier set via --category
199
+ 3. Channel default — per-channel tier set via --channel
200
+ 4. Global default — the fallback tier set via --default
201
+
202
+ Config is stored in <data-dir>/.vellum/workspace/autonomy.json, where
203
+ <data-dir> is the BASE_DATA_DIR environment variable (defaults to $HOME).
204
+
205
+ Examples:
206
+ $ vellum autonomy get
207
+ $ vellum autonomy set --default draft
208
+ $ vellum autonomy set --channel telegram --tier auto`,
209
+ );
210
+
211
+ autonomy
212
+ .command("get")
213
+ .description("Show current autonomy configuration")
214
+ .addHelpText(
215
+ "after",
216
+ `
217
+ Prints the full autonomy configuration: the global default tier, per-channel
218
+ defaults, category overrides, and contact overrides. Sections with no entries
219
+ are shown as "(none)".
220
+
221
+ Pass --json (on the parent command) for machine-readable output containing
222
+ the complete config object.
223
+
224
+ Examples:
225
+ $ vellum autonomy get
226
+ $ vellum autonomy --json get`,
227
+ )
228
+ .action((_opts: Record<string, unknown>, cmd: Command) => {
229
+ const json = getJson(cmd);
230
+ const config = loadConfig();
231
+ if (json) {
232
+ outputJson({ ok: true, config });
233
+ } else {
234
+ process.stdout.write("Autonomy configuration:\n\n");
235
+ process.stdout.write(formatConfigForHuman(config) + "\n");
236
+ }
237
+ });
238
+
239
+ autonomy
240
+ .command("set")
241
+ .description("Set autonomy tier for default, channel, category, or contact")
242
+ .option("--default <tier>", "Set the global default tier")
243
+ .option("--channel <channel>", "Channel to configure")
244
+ .option("--category <category>", "Category to configure")
245
+ .option("--contact <contactId>", "Contact to configure")
246
+ .option("--tier <tier>", "Tier to set (auto, draft, notify)")
247
+ .addHelpText(
248
+ "after",
249
+ `
250
+ Four targeting modes — provide one of the following per invocation. If multiple
251
+ are given, the first match is applied in this priority order:
252
+
253
+ --default <tier> Set the global default tier. The <tier>
254
+ value is the argument itself — do not
255
+ combine with --tier.
256
+ --channel <channel> --tier <t> Set the default tier for a specific channel.
257
+ --category <cat> --tier <t> Set the tier override for a message category.
258
+ --contact <id> --tier <t> Set the tier override for a specific contact.
259
+
260
+ Valid tier values: auto, draft, notify.
261
+
262
+ Each call merges into the existing config — it does not replace other entries.
263
+ For example, setting a channel default leaves all other channel defaults,
264
+ category overrides, and contact overrides intact.
265
+
266
+ Examples:
267
+ $ vellum autonomy set --default draft
268
+ $ vellum autonomy set --channel telegram --tier auto
269
+ $ vellum autonomy set --category billing --tier notify
270
+ $ vellum autonomy set --contact c_8f3a1b2d --tier draft`,
271
+ )
272
+ .action(
273
+ (
274
+ opts: {
275
+ default?: string;
276
+ channel?: string;
277
+ category?: string;
278
+ contact?: string;
279
+ tier?: string;
280
+ },
281
+ cmd: Command,
282
+ ) => {
283
+ const json = getJson(cmd);
284
+
285
+ if (opts.default) {
286
+ if (!isValidTier(opts.default)) {
287
+ outputJson({
288
+ ok: false,
289
+ error: `Invalid tier "${opts.default}". Must be one of: ${AUTONOMY_TIERS.join(", ")}`,
290
+ });
291
+ process.exitCode = 1;
292
+ return;
293
+ }
294
+ const config = applyUpdate({ defaultTier: opts.default });
295
+ if (json) {
296
+ outputJson({ ok: true, config });
297
+ } else {
298
+ log.info(`Set global default tier to "${opts.default}".`);
299
+ }
300
+ return;
301
+ }
302
+
303
+ if (!opts.tier) {
304
+ outputJson({
305
+ ok: false,
306
+ error: "Missing --tier. Use --tier <auto|draft|notify>.",
307
+ });
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+ if (!isValidTier(opts.tier)) {
312
+ outputJson({
313
+ ok: false,
314
+ error: `Invalid tier "${opts.tier}". Must be one of: ${AUTONOMY_TIERS.join(", ")}`,
315
+ });
316
+ process.exitCode = 1;
317
+ return;
318
+ }
319
+
320
+ if (opts.channel) {
321
+ const config = applyUpdate({
322
+ channelDefaults: { [opts.channel]: opts.tier },
323
+ });
324
+ if (json) {
325
+ outputJson({ ok: true, config });
326
+ } else {
327
+ log.info(
328
+ `Set channel "${opts.channel}" default to "${opts.tier}".`,
329
+ );
330
+ }
331
+ return;
332
+ }
333
+
334
+ if (opts.category) {
335
+ const config = applyUpdate({
336
+ categoryOverrides: { [opts.category]: opts.tier },
337
+ });
338
+ if (json) {
339
+ outputJson({ ok: true, config });
340
+ } else {
341
+ log.info(
342
+ `Set category "${opts.category}" override to "${opts.tier}".`,
343
+ );
344
+ }
345
+ return;
346
+ }
347
+
348
+ if (opts.contact) {
349
+ const config = applyUpdate({
350
+ contactOverrides: { [opts.contact]: opts.tier },
351
+ });
352
+ if (json) {
353
+ outputJson({ ok: true, config });
354
+ } else {
355
+ log.info(
356
+ `Set contact "${opts.contact}" override to "${opts.tier}".`,
357
+ );
358
+ }
359
+ return;
360
+ }
361
+
362
+ log.error(
363
+ "Specify one of: --default <tier>, --channel <channel> --tier <tier>, " +
364
+ "--category <category> --tier <tier>, or --contact <contactId> --tier <tier>.",
365
+ );
366
+ process.exitCode = 1;
367
+ },
368
+ );
369
+ }
@@ -0,0 +1,51 @@
1
+ import type { Command } from "commander";
2
+
3
+ import { gatewayGet, runRead, toQueryString } from "./integrations.js";
4
+
5
+ export function registerChannelsCommand(program: Command): void {
6
+ const channels = program
7
+ .command("channels")
8
+ .description("Query channel status")
9
+ .option("--json", "Machine-readable compact JSON output");
10
+
11
+ channels.addHelpText(
12
+ "after",
13
+ `
14
+ Queries channel readiness and configuration status through the gateway API.
15
+ Channels are the communication interfaces (telegram, voice, sms, email, etc.)
16
+ that the assistant uses to send and receive messages.
17
+
18
+ The assistant must be running — channel status is read from the live gateway.
19
+
20
+ Examples:
21
+ $ vellum channels readiness
22
+ $ vellum channels readiness --channel telegram
23
+ $ vellum channels readiness --json`,
24
+ );
25
+
26
+ channels
27
+ .command("readiness")
28
+ .description("Check channel readiness for accepting messages")
29
+ .option("--channel <channel>", "Filter by channel type")
30
+ .addHelpText(
31
+ "after",
32
+ `
33
+ Reports whether each configured channel is ready to accept messages. A channel
34
+ is "ready" when its credentials are valid, its integration is connected, and
35
+ it can deliver messages to the user.
36
+
37
+ The --channel flag filters results to a single channel type. Without it, all
38
+ configured channels are returned. Common channel types include: telegram,
39
+ voice, sms, email, slack, vellum.
40
+
41
+ Examples:
42
+ $ vellum channels readiness
43
+ $ vellum channels readiness --channel telegram`,
44
+ )
45
+ .action(async (opts: { channel?: string }, cmd: Command) => {
46
+ const query = toQueryString({ channel: opts.channel });
47
+ await runRead(cmd, async () =>
48
+ gatewayGet(`/v1/channels/readiness${query}`),
49
+ );
50
+ });
51
+ }