@vellumai/assistant 0.6.0 → 0.6.1

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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,245 @@
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import {
7
+ getMockFetchCalls,
8
+ mockFetch,
9
+ resetMockFetch,
10
+ } from "../../../__tests__/mock-fetch.js";
11
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
12
+ import { setPlatformAssistantId } from "../../../config/env.js";
13
+ import { credentialKey } from "../../../security/credential-key.js";
14
+ import {
15
+ _resetBackend,
16
+ deleteSecureKeyAsync,
17
+ setSecureKeyAsync,
18
+ } from "../../../security/secure-keys.js";
19
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
20
+
21
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
22
+ const MESSAGE_ID = "msg-001";
23
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
24
+
25
+ const SAMPLE_MESSAGE = {
26
+ id: MESSAGE_ID,
27
+ direction: "inbound",
28
+ from_address: "user@example.com",
29
+ to_addresses: ["mybot@vellum.me"],
30
+ subject: "Hello bot",
31
+ body_text: "Hi, this is a test message.",
32
+ body_html: "<p>Hi, this is a <b>test</b> message.</p>",
33
+ in_reply_to: "",
34
+ references: [],
35
+ created_at: "2026-04-05T12:00:00Z",
36
+ };
37
+
38
+ function mockDetailSuccess(msg = SAMPLE_MESSAGE, status = 200): void {
39
+ mockFetch(`/emails/${msg.id}/`, {}, { body: msg, status });
40
+ }
41
+
42
+ let savedCesUrl: string | undefined;
43
+ let savedContainerized: string | undefined;
44
+ let tmpOutputPath: string;
45
+
46
+ beforeEach(async () => {
47
+ process.exitCode = 0;
48
+
49
+ savedCesUrl = process.env.CES_CREDENTIAL_URL;
50
+ savedContainerized = process.env.IS_CONTAINERIZED;
51
+ delete process.env.CES_CREDENTIAL_URL;
52
+ delete process.env.IS_CONTAINERIZED;
53
+
54
+ _resetBackend();
55
+ resetMockFetch();
56
+ _setOverridesForTesting({ "email-channel": true });
57
+ setPlatformAssistantId(ASSISTANT_ID);
58
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
59
+
60
+ tmpOutputPath = join(tmpdir(), `email-download-test-${Date.now()}.txt`);
61
+ });
62
+
63
+ afterEach(() => {
64
+ resetMockFetch();
65
+ _setOverridesForTesting({});
66
+ setPlatformAssistantId(undefined);
67
+ _resetBackend();
68
+
69
+ if (savedCesUrl !== undefined) process.env.CES_CREDENTIAL_URL = savedCesUrl;
70
+ else delete process.env.CES_CREDENTIAL_URL;
71
+ if (savedContainerized !== undefined)
72
+ process.env.IS_CONTAINERIZED = savedContainerized;
73
+ else delete process.env.IS_CONTAINERIZED;
74
+
75
+ if (existsSync(tmpOutputPath)) rmSync(tmpOutputPath);
76
+ });
77
+
78
+ describe("assistant email download", () => {
79
+ test("default format shows headers and plain-text body", async () => {
80
+ mockDetailSuccess();
81
+
82
+ const output = await runAssistantCommand("email", "download", MESSAGE_ID);
83
+
84
+ expect(output).toContain("From: user@example.com");
85
+ expect(output).toContain("To: mybot@vellum.me");
86
+ expect(output).toContain("Subject: Hello bot");
87
+ expect(output).toContain("Hi, this is a test message.");
88
+ expect(process.exitCode).toBe(0);
89
+ });
90
+
91
+ test("--format json returns full message object", async () => {
92
+ mockDetailSuccess();
93
+
94
+ const output = await runAssistantCommand(
95
+ "email",
96
+ "download",
97
+ MESSAGE_ID,
98
+ "--format",
99
+ "json",
100
+ );
101
+
102
+ const parsed = JSON.parse(output.trim());
103
+ expect(parsed.id).toBe(MESSAGE_ID);
104
+ expect(parsed.body_text).toBe("Hi, this is a test message.");
105
+ expect(parsed.body_html).toContain("<b>test</b>");
106
+ expect(process.exitCode).toBe(0);
107
+ });
108
+
109
+ test("--json flag also returns JSON", async () => {
110
+ mockDetailSuccess();
111
+
112
+ const output = await runAssistantCommand(
113
+ "email",
114
+ "--json",
115
+ "download",
116
+ MESSAGE_ID,
117
+ );
118
+
119
+ const parsed = JSON.parse(output.trim());
120
+ expect(parsed.id).toBe(MESSAGE_ID);
121
+ expect(process.exitCode).toBe(0);
122
+ });
123
+
124
+ test("--format html returns HTML body", async () => {
125
+ mockDetailSuccess();
126
+
127
+ const output = await runAssistantCommand(
128
+ "email",
129
+ "download",
130
+ MESSAGE_ID,
131
+ "--format",
132
+ "html",
133
+ );
134
+
135
+ expect(output).toContain("<p>Hi, this is a <b>test</b> message.</p>");
136
+ expect(process.exitCode).toBe(0);
137
+ });
138
+
139
+ test("--format html with no HTML body returns error", async () => {
140
+ mockDetailSuccess({ ...SAMPLE_MESSAGE, body_html: "" });
141
+
142
+ const output = await runAssistantCommand(
143
+ "email",
144
+ "download",
145
+ MESSAGE_ID,
146
+ "--format",
147
+ "html",
148
+ );
149
+
150
+ expect(process.exitCode).toBe(1);
151
+ // stderr output from log.error, but stdout may be empty — check exitCode
152
+ expect(output).not.toContain("<p>");
153
+ });
154
+
155
+ test("--output writes to file", async () => {
156
+ mockDetailSuccess();
157
+
158
+ await runAssistantCommand(
159
+ "email",
160
+ "download",
161
+ MESSAGE_ID,
162
+ "-o",
163
+ tmpOutputPath,
164
+ );
165
+
166
+ expect(process.exitCode).toBe(0);
167
+ expect(existsSync(tmpOutputPath)).toBe(true);
168
+ const content = readFileSync(tmpOutputPath, "utf-8");
169
+ expect(content).toContain("From: user@example.com");
170
+ expect(content).toContain("Hi, this is a test message.");
171
+ });
172
+
173
+ test("calls correct URL", async () => {
174
+ mockDetailSuccess();
175
+
176
+ await runAssistantCommand("email", "download", MESSAGE_ID);
177
+
178
+ const calls = getMockFetchCalls();
179
+ expect(calls).toHaveLength(1);
180
+ expect(calls[0].path).toContain(
181
+ `/v1/assistants/${ASSISTANT_ID}/emails/${MESSAGE_ID}/`,
182
+ );
183
+ });
184
+
185
+ test("404 returns error", async () => {
186
+ mockFetch(
187
+ `/emails/${MESSAGE_ID}/`,
188
+ {},
189
+ { body: { detail: "Not found." }, status: 404 },
190
+ );
191
+
192
+ const output = await runAssistantCommand(
193
+ "email",
194
+ "--json",
195
+ "download",
196
+ MESSAGE_ID,
197
+ );
198
+
199
+ expect(process.exitCode).toBe(1);
200
+ const parsed = JSON.parse(output.trim());
201
+ expect(parsed.error).toContain("Not found");
202
+ });
203
+
204
+ test("missing platform credentials returns error", async () => {
205
+ await deleteSecureKeyAsync(API_KEY_CREDENTIAL);
206
+
207
+ const output = await runAssistantCommand(
208
+ "email",
209
+ "--json",
210
+ "download",
211
+ MESSAGE_ID,
212
+ );
213
+
214
+ expect(process.exitCode).toBe(1);
215
+ const parsed = JSON.parse(output.trim());
216
+ expect(parsed.error).toContain("Platform credentials not configured");
217
+ });
218
+
219
+ test("missing assistant ID returns error", async () => {
220
+ setPlatformAssistantId("");
221
+
222
+ const output = await runAssistantCommand(
223
+ "email",
224
+ "--json",
225
+ "download",
226
+ MESSAGE_ID,
227
+ );
228
+
229
+ expect(process.exitCode).toBe(1);
230
+ const parsed = JSON.parse(output.trim());
231
+ expect(parsed.error).toContain("Assistant ID");
232
+ });
233
+
234
+ test("in_reply_to header shown when present", async () => {
235
+ mockDetailSuccess({
236
+ ...SAMPLE_MESSAGE,
237
+ in_reply_to: "<orig@mail.gmail.com>",
238
+ });
239
+
240
+ const output = await runAssistantCommand("email", "download", MESSAGE_ID);
241
+
242
+ expect(output).toContain("In-Reply-To: <orig@mail.gmail.com>");
243
+ expect(process.exitCode).toBe(0);
244
+ });
245
+ });
@@ -0,0 +1,192 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ deleteSecureKeyAsync,
14
+ setSecureKeyAsync,
15
+ } from "../../../security/secure-keys.js";
16
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
17
+
18
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
19
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
20
+
21
+ const SAMPLE_MESSAGES = [
22
+ {
23
+ id: "msg-001",
24
+ direction: "inbound",
25
+ from_address: "user@example.com",
26
+ to_addresses: ["mybot@vellum.me"],
27
+ subject: "Hello bot",
28
+ created_at: "2026-04-05T12:00:00Z",
29
+ },
30
+ {
31
+ id: "msg-002",
32
+ direction: "outbound",
33
+ from_address: "mybot@vellum.me",
34
+ to_addresses: ["user@example.com"],
35
+ subject: "Re: Hello bot",
36
+ created_at: "2026-04-05T12:01:00Z",
37
+ },
38
+ ];
39
+
40
+ function mockListEmails(
41
+ results = SAMPLE_MESSAGES,
42
+ count?: number,
43
+ status = 200,
44
+ ): void {
45
+ mockFetch(
46
+ "/emails/",
47
+ {},
48
+ { body: { results, count: count ?? results.length }, status },
49
+ );
50
+ }
51
+
52
+ let savedCesUrl: string | undefined;
53
+ let savedContainerized: string | undefined;
54
+
55
+ beforeEach(async () => {
56
+ process.exitCode = 0;
57
+
58
+ savedCesUrl = process.env.CES_CREDENTIAL_URL;
59
+ savedContainerized = process.env.IS_CONTAINERIZED;
60
+ delete process.env.CES_CREDENTIAL_URL;
61
+ delete process.env.IS_CONTAINERIZED;
62
+
63
+ _resetBackend();
64
+ resetMockFetch();
65
+ _setOverridesForTesting({ "email-channel": true });
66
+ setPlatformAssistantId(ASSISTANT_ID);
67
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
68
+ });
69
+
70
+ afterEach(() => {
71
+ resetMockFetch();
72
+ _setOverridesForTesting({});
73
+ setPlatformAssistantId(undefined);
74
+ _resetBackend();
75
+
76
+ if (savedCesUrl !== undefined) process.env.CES_CREDENTIAL_URL = savedCesUrl;
77
+ else delete process.env.CES_CREDENTIAL_URL;
78
+ if (savedContainerized !== undefined)
79
+ process.env.IS_CONTAINERIZED = savedContainerized;
80
+ else delete process.env.IS_CONTAINERIZED;
81
+ });
82
+
83
+ describe("assistant email list", () => {
84
+ test("--json returns messages and count", async () => {
85
+ mockListEmails();
86
+
87
+ const output = await runAssistantCommand("email", "--json", "list");
88
+
89
+ const parsed = JSON.parse(output.trim());
90
+ expect(parsed.results).toHaveLength(2);
91
+ expect(parsed.count).toBe(2);
92
+ expect(parsed.results[0].subject).toBe("Hello bot");
93
+ expect(parsed.results[1].direction).toBe("outbound");
94
+ expect(process.exitCode).toBe(0);
95
+ });
96
+
97
+ test("calls correct URL with no filters", async () => {
98
+ mockListEmails();
99
+
100
+ await runAssistantCommand("email", "--json", "list");
101
+
102
+ const calls = getMockFetchCalls();
103
+ expect(calls).toHaveLength(1);
104
+ expect(calls[0].path).toContain(`/v1/assistants/${ASSISTANT_ID}/emails/`);
105
+ // Default limit=20 should be in query string
106
+ expect(calls[0].path).toContain("limit=20");
107
+ });
108
+
109
+ test("--direction filters by direction", async () => {
110
+ mockListEmails();
111
+
112
+ await runAssistantCommand(
113
+ "email",
114
+ "--json",
115
+ "list",
116
+ "--direction",
117
+ "inbound",
118
+ );
119
+
120
+ const calls = getMockFetchCalls();
121
+ expect(calls[0].path).toContain("direction=inbound");
122
+ });
123
+
124
+ test("--limit sets result count", async () => {
125
+ mockListEmails();
126
+
127
+ await runAssistantCommand("email", "--json", "list", "--limit", "5");
128
+
129
+ const calls = getMockFetchCalls();
130
+ expect(calls[0].path).toContain("limit=5");
131
+ });
132
+
133
+ test("--since filters by date", async () => {
134
+ mockListEmails();
135
+
136
+ await runAssistantCommand(
137
+ "email",
138
+ "--json",
139
+ "list",
140
+ "--since",
141
+ "2026-04-01",
142
+ );
143
+
144
+ const calls = getMockFetchCalls();
145
+ expect(calls[0].path).toContain("since=2026-04-01");
146
+ });
147
+
148
+ test("empty results returns empty array", async () => {
149
+ mockListEmails([], 0);
150
+
151
+ const output = await runAssistantCommand("email", "--json", "list");
152
+
153
+ const parsed = JSON.parse(output.trim());
154
+ expect(parsed.results).toHaveLength(0);
155
+ expect(parsed.count).toBe(0);
156
+ expect(process.exitCode).toBe(0);
157
+ });
158
+
159
+ test("endpoint failure returns error", async () => {
160
+ mockFetch(
161
+ "/emails/",
162
+ {},
163
+ { body: { detail: "Internal server error" }, status: 500 },
164
+ );
165
+
166
+ const output = await runAssistantCommand("email", "--json", "list");
167
+
168
+ expect(process.exitCode).toBe(1);
169
+ const parsed = JSON.parse(output.trim());
170
+ expect(parsed.error).toContain("Internal server error");
171
+ });
172
+
173
+ test("missing platform credentials returns error", async () => {
174
+ await deleteSecureKeyAsync(API_KEY_CREDENTIAL);
175
+
176
+ const output = await runAssistantCommand("email", "--json", "list");
177
+
178
+ expect(process.exitCode).toBe(1);
179
+ const parsed = JSON.parse(output.trim());
180
+ expect(parsed.error).toContain("Platform credentials not configured");
181
+ });
182
+
183
+ test("missing assistant ID returns error", async () => {
184
+ setPlatformAssistantId("");
185
+
186
+ const output = await runAssistantCommand("email", "--json", "list");
187
+
188
+ expect(process.exitCode).toBe(1);
189
+ const parsed = JSON.parse(output.trim());
190
+ expect(parsed.error).toContain("Assistant ID");
191
+ });
192
+ });
@@ -0,0 +1,186 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ getMockFetchCalls,
5
+ mockFetch,
6
+ resetMockFetch,
7
+ } from "../../../__tests__/mock-fetch.js";
8
+ import { _setOverridesForTesting } from "../../../config/assistant-feature-flags.js";
9
+ import { setPlatformAssistantId } from "../../../config/env.js";
10
+ import { credentialKey } from "../../../security/credential-key.js";
11
+ import {
12
+ _resetBackend,
13
+ setSecureKeyAsync,
14
+ } from "../../../security/secure-keys.js";
15
+ import { runAssistantCommand } from "../../__tests__/run-assistant-command.js";
16
+
17
+ const ASSISTANT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
18
+ const API_KEY_CREDENTIAL = credentialKey("vellum", "assistant_api_key");
19
+
20
+ beforeEach(async () => {
21
+ process.exitCode = 0;
22
+ _resetBackend();
23
+ resetMockFetch();
24
+ _setOverridesForTesting({ "email-channel": true });
25
+ setPlatformAssistantId(ASSISTANT_ID);
26
+ await setSecureKeyAsync(API_KEY_CREDENTIAL, "test-api-key");
27
+ });
28
+
29
+ afterEach(() => {
30
+ resetMockFetch();
31
+ _setOverridesForTesting({});
32
+ setPlatformAssistantId(undefined);
33
+ _resetBackend();
34
+ });
35
+
36
+ describe("assistant email register", () => {
37
+ test("successful registration calls correct URL and body", async () => {
38
+ mockFetch(
39
+ "/email-addresses/",
40
+ { method: "POST" },
41
+ {
42
+ body: {
43
+ id: "550e8400-e29b-41d4-a716-446655440000",
44
+ address: "mybot@vellum.me",
45
+ created_at: "2026-04-04T21:00:00Z",
46
+ },
47
+ status: 201,
48
+ },
49
+ );
50
+
51
+ await runAssistantCommand("email", "register", "mybot");
52
+
53
+ const calls = getMockFetchCalls();
54
+ expect(calls).toHaveLength(1);
55
+ expect(calls[0].path).toBe(
56
+ `/v1/assistants/${ASSISTANT_ID}/email-addresses/`,
57
+ );
58
+ expect(calls[0].init.method).toBe("POST");
59
+ expect(JSON.parse(calls[0].init.body as string)).toEqual({
60
+ username: "mybot",
61
+ });
62
+ expect(process.exitCode).toBe(0);
63
+ });
64
+
65
+ test("--json outputs structured response", async () => {
66
+ mockFetch(
67
+ "/email-addresses/",
68
+ { method: "POST" },
69
+ {
70
+ body: {
71
+ id: "550e8400-e29b-41d4-a716-446655440000",
72
+ address: "support@vellum.me",
73
+ created_at: "2026-04-04T21:00:00Z",
74
+ },
75
+ status: 201,
76
+ },
77
+ );
78
+
79
+ const output = await runAssistantCommand(
80
+ "email",
81
+ "--json",
82
+ "register",
83
+ "support",
84
+ );
85
+
86
+ const parsed = JSON.parse(output.trim());
87
+ expect(parsed.address).toBe("support@vellum.me");
88
+ expect(parsed.id).toBe("550e8400-e29b-41d4-a716-446655440000");
89
+ expect(parsed.created_at).toBe("2026-04-04T21:00:00Z");
90
+ expect(process.exitCode).toBe(0);
91
+ });
92
+
93
+ test("duplicate address returns error", async () => {
94
+ mockFetch(
95
+ "/email-addresses/",
96
+ { method: "POST" },
97
+ {
98
+ body: {
99
+ assistant_id: ["This assistant already has an email address."],
100
+ },
101
+ status: 400,
102
+ },
103
+ );
104
+
105
+ const output = await runAssistantCommand(
106
+ "email",
107
+ "--json",
108
+ "register",
109
+ "mybot",
110
+ );
111
+
112
+ expect(process.exitCode).toBe(1);
113
+ const parsed = JSON.parse(output.trim());
114
+ expect(parsed.error).toContain("already has an email address");
115
+ });
116
+
117
+ test("missing platform credentials returns error", async () => {
118
+ // Remove the API key so create() returns null
119
+ _resetBackend();
120
+ setPlatformAssistantId(undefined);
121
+
122
+ const output = await runAssistantCommand(
123
+ "email",
124
+ "--json",
125
+ "register",
126
+ "mybot",
127
+ );
128
+
129
+ expect(process.exitCode).toBe(1);
130
+ const parsed = JSON.parse(output.trim());
131
+ expect(parsed.error).toContain("Platform credentials not configured");
132
+ });
133
+
134
+ test("missing assistant ID returns error", async () => {
135
+ setPlatformAssistantId("");
136
+
137
+ const output = await runAssistantCommand(
138
+ "email",
139
+ "--json",
140
+ "register",
141
+ "mybot",
142
+ );
143
+
144
+ expect(process.exitCode).toBe(1);
145
+ const parsed = JSON.parse(output.trim());
146
+ expect(parsed.error).toContain("Assistant ID");
147
+ });
148
+
149
+ test("platform 5xx returns error", async () => {
150
+ mockFetch(
151
+ "/email-addresses/",
152
+ { method: "POST" },
153
+ { body: { detail: "Internal server error" }, status: 500 },
154
+ );
155
+
156
+ const output = await runAssistantCommand(
157
+ "email",
158
+ "--json",
159
+ "register",
160
+ "mybot",
161
+ );
162
+
163
+ expect(process.exitCode).toBe(1);
164
+ const parsed = JSON.parse(output.trim());
165
+ expect(parsed.error).toContain("Internal server error");
166
+ });
167
+
168
+ test("username validation error from platform is surfaced", async () => {
169
+ mockFetch(
170
+ "/email-addresses/",
171
+ { method: "POST" },
172
+ { body: { username: ["Enter a valid value."] }, status: 400 },
173
+ );
174
+
175
+ const output = await runAssistantCommand(
176
+ "email",
177
+ "--json",
178
+ "register",
179
+ "invalid username!",
180
+ );
181
+
182
+ expect(process.exitCode).toBe(1);
183
+ const parsed = JSON.parse(output.trim());
184
+ expect(parsed.error).toContain("valid value");
185
+ });
186
+ });