@vellumai/assistant 0.6.0 → 0.6.2

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 (358) 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 +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Workspace allowlist module for the daemon log export endpoint.
3
+ *
4
+ * `POST /v1/export` collects audit DB rows, daemon logs, and a sanitized
5
+ * `config.json` snapshot. This module governs which subpaths of the user's
6
+ * workspace directory (`~/.vellum/workspace/`) are *opted in* to the export
7
+ * archive. The default is "nothing from the workspace ships" — every entry
8
+ * here must be justified against the rules in `./AGENTS.md`.
9
+ *
10
+ * The first allowlisted entry is `<workspace>/conversations/`, which honors
11
+ * both the time filter (via the parsed timestamp prefix on each conversation
12
+ * directory name) and the conversationId filter (via exact match on the id
13
+ * suffix). Directory names that don't match the canonical
14
+ * `<ISO-with-dashes>_<conversationId>` format are silently skipped (Rule 3).
15
+ */
16
+
17
+ import {
18
+ closeSync,
19
+ cpSync,
20
+ existsSync,
21
+ lstatSync,
22
+ mkdirSync,
23
+ openSync,
24
+ readdirSync,
25
+ readSync,
26
+ } from "node:fs";
27
+ import { join } from "node:path";
28
+ import { StringDecoder } from "node:string_decoder";
29
+
30
+ import { parseConversationDirName } from "../../../memory/conversation-directories.js";
31
+ import { getLogger } from "../../../util/logger.js";
32
+ import { getConversationsDir } from "../../../util/platform.js";
33
+
34
+ const log = getLogger("log-export-workspace");
35
+
36
+ /**
37
+ * Maximum total bytes that the workspace allowlist may contribute to a
38
+ * single export archive. Mirrors `MAX_LOG_PAYLOAD_BYTES` in
39
+ * `log-export-routes.ts` so that the workspace section can never blow past
40
+ * the same 10 MB cap that already governs the daemon-logs section.
41
+ */
42
+ export const MAX_WORKSPACE_PAYLOAD_BYTES = 10 * 1024 * 1024;
43
+
44
+ export interface CollectWorkspaceDataOptions {
45
+ /** Absolute path of the export staging directory. */
46
+ staging: string;
47
+ /** When set, restrict allowlisted entries to this conversation. */
48
+ conversationId?: string;
49
+ /** Lower bound (epoch ms, inclusive). */
50
+ startTime?: number;
51
+ /** Upper bound (epoch ms, inclusive). */
52
+ endTime?: number;
53
+ /** Override the default 10 MB cap (used in tests). */
54
+ maxBytes?: number;
55
+ }
56
+
57
+ export interface CollectWorkspaceDataResult {
58
+ /** Allowlisted entries that were copied to staging/workspace/. */
59
+ entries: Array<{
60
+ /** Allowlist entry name (e.g. "conversations"). */
61
+ entry: string;
62
+ /** Number of items (files or subdirs) copied. */
63
+ itemCount: number;
64
+ /** Total bytes copied for this entry. */
65
+ bytes: number;
66
+ /** Items skipped because the cap would be exceeded. */
67
+ skippedDueToCap: number;
68
+ }>;
69
+ totalBytes: number;
70
+ }
71
+
72
+ /**
73
+ * Walk a directory recursively and sum the sizes of every regular file
74
+ * underneath it. Bails out early once the running total would push the
75
+ * workspace cap over `remainingBudget` bytes — that way we never burn
76
+ * cycles totalling a multi-gigabyte directory only to discard it.
77
+ *
78
+ * Returns `null` to signal "this directory is too big to fit in the
79
+ * remaining budget"; returns the exact byte total otherwise.
80
+ */
81
+ function dirSizeWithinBudget(
82
+ rootDir: string,
83
+ remainingBudget: number,
84
+ ): number | null {
85
+ let total = 0;
86
+ const stack: string[] = [rootDir];
87
+ while (stack.length > 0) {
88
+ const current = stack.pop()!;
89
+ let entries: string[];
90
+ try {
91
+ entries = readdirSync(current);
92
+ } catch (err) {
93
+ log.warn(
94
+ { err, dir: current },
95
+ "Failed to read workspace directory while sizing; skipping",
96
+ );
97
+ continue;
98
+ }
99
+ for (const name of entries) {
100
+ const child = join(current, name);
101
+ let stat: ReturnType<typeof lstatSync>;
102
+ try {
103
+ // Use lstat (not stat) so symlinks are NOT dereferenced. Without
104
+ // this, a symlink cycle inside a conversation directory (e.g.
105
+ // `loop -> .`) would cause the walker to recurse forever and
106
+ // hang `collectWorkspaceData`. With lstat, symlinks show up as
107
+ // symlinks — neither `isDirectory()` nor `isFile()` is true on
108
+ // the lstat result, so they're naturally skipped below.
109
+ stat = lstatSync(child);
110
+ } catch (err) {
111
+ log.warn(
112
+ { err, path: child },
113
+ "Failed to stat workspace path while sizing; skipping",
114
+ );
115
+ continue;
116
+ }
117
+ if (stat.isDirectory()) {
118
+ stack.push(child);
119
+ } else if (stat.isFile()) {
120
+ total += stat.size;
121
+ if (total > remainingBudget) {
122
+ return null;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ return total;
128
+ }
129
+
130
+ /**
131
+ * Chunk size used by the streaming `messages.jsonl` reader. 64 KB is
132
+ * large enough to amortize syscall overhead but small enough to keep
133
+ * the synchronous read path off the event loop for any meaningful
134
+ * stretch.
135
+ */
136
+ const MESSAGES_SCAN_CHUNK_BYTES = 64 * 1024;
137
+
138
+ /**
139
+ * Check whether a single JSONL line records a message whose `ts` falls
140
+ * in the `[startTime, endTime]` window. Returns `false` for malformed
141
+ * lines, missing/wrong-typed `ts` fields, and dates outside the window.
142
+ * Pulled out as a helper so the streaming reader can call it on each
143
+ * decoded line without duplicating the parsing logic.
144
+ */
145
+ function lineMatchesWindow(
146
+ line: string,
147
+ startTime: number | undefined,
148
+ endTime: number | undefined,
149
+ ): boolean {
150
+ if (!line) return false;
151
+ let record: { ts?: unknown };
152
+ try {
153
+ record = JSON.parse(line) as { ts?: unknown };
154
+ } catch {
155
+ return false;
156
+ }
157
+ if (typeof record.ts !== "string") return false;
158
+ const ms = Date.parse(record.ts);
159
+ if (Number.isNaN(ms)) return false;
160
+ if (startTime !== undefined && ms < startTime) return false;
161
+ if (endTime !== undefined && ms > endTime) return false;
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Scan a conversation's `messages.jsonl` file and report whether any
167
+ * message's `ts` (an ISO 8601 string written by `conversation-disk-view`)
168
+ * falls inside the `[startTime, endTime]` window.
169
+ *
170
+ * Returns:
171
+ * - `true` if at least one message timestamp lies in the window.
172
+ * - `false` otherwise (including: file is missing, file is empty, every
173
+ * line fails to parse, or no parsed line lands in the window).
174
+ *
175
+ * Lines that fail to parse as JSON or whose `ts` is not a parseable date
176
+ * are silently skipped — they shouldn't be able to make the function
177
+ * throw, since the export pipeline must never crash on a malformed
178
+ * conversation file.
179
+ *
180
+ * The reader streams the file in fixed-size chunks (`MESSAGES_SCAN_CHUNK_BYTES`)
181
+ * via `readSync` and decodes UTF-8 across chunk boundaries with
182
+ * `StringDecoder`. It bails out as soon as it finds the first matching
183
+ * line, so the worst case for an in-window conversation is "one early
184
+ * hit", and the worst case for an out-of-window conversation is "read
185
+ * the whole file once" — without ever holding more than one chunk plus
186
+ * one in-progress line in memory.
187
+ */
188
+ function conversationHasMessageInWindow(
189
+ conversationDir: string,
190
+ startTime: number | undefined,
191
+ endTime: number | undefined,
192
+ ): boolean {
193
+ // No window means every message trivially "matches", but the only
194
+ // caller (`collectConversations`) already short-circuits in that case
195
+ // and never invokes this helper. Defensive check kept so the helper is
196
+ // safe to reuse.
197
+ if (startTime === undefined && endTime === undefined) return true;
198
+
199
+ const messagesPath = join(conversationDir, "messages.jsonl");
200
+ let fd: number;
201
+ try {
202
+ fd = openSync(messagesPath, "r");
203
+ } catch {
204
+ // Missing or unreadable messages file → no in-window evidence.
205
+ return false;
206
+ }
207
+
208
+ const buffer = Buffer.alloc(MESSAGES_SCAN_CHUNK_BYTES);
209
+ const decoder = new StringDecoder("utf8");
210
+ let leftover = "";
211
+ try {
212
+ while (true) {
213
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
214
+ if (bytesRead === 0) break;
215
+ const text = leftover + decoder.write(buffer.subarray(0, bytesRead));
216
+ const lines = text.split("\n");
217
+ // The last segment may be a partial line — hold it back for the
218
+ // next chunk to complete.
219
+ leftover = lines.pop() ?? "";
220
+ for (const line of lines) {
221
+ if (lineMatchesWindow(line, startTime, endTime)) return true;
222
+ }
223
+ }
224
+ // Drain any partial UTF-8 sequence the decoder is still holding,
225
+ // then check the final unterminated line (the file may not end with
226
+ // a newline).
227
+ const tail = leftover + decoder.end();
228
+ if (lineMatchesWindow(tail, startTime, endTime)) return true;
229
+ } finally {
230
+ try {
231
+ closeSync(fd);
232
+ } catch {
233
+ /* best-effort close */
234
+ }
235
+ }
236
+ return false;
237
+ }
238
+
239
+ function collectConversations(
240
+ opts: CollectWorkspaceDataOptions,
241
+ result: CollectWorkspaceDataResult,
242
+ ): void {
243
+ const maxBytes = opts.maxBytes ?? MAX_WORKSPACE_PAYLOAD_BYTES;
244
+ // Initialize the entry summary and push it onto `result.entries`
245
+ // immediately so the conversations entry is always present in the
246
+ // result, even if the candidate loop below throws partway through.
247
+ // The array holds a reference to this object, so all later mutations
248
+ // to `entry.itemCount`, `entry.bytes`, and `entry.skippedDueToCap`
249
+ // are visible to consumers via `result.entries`.
250
+ const entry = {
251
+ entry: "conversations",
252
+ itemCount: 0,
253
+ bytes: 0,
254
+ skippedDueToCap: 0,
255
+ };
256
+ result.entries.push(entry);
257
+
258
+ const sourceDir = getConversationsDir();
259
+ if (!existsSync(sourceDir)) {
260
+ return;
261
+ }
262
+
263
+ let names: string[];
264
+ try {
265
+ names = readdirSync(sourceDir);
266
+ } catch (err) {
267
+ log.warn(
268
+ { err, sourceDir },
269
+ "Failed to read conversations directory; skipping conversations entry",
270
+ );
271
+ return;
272
+ }
273
+
274
+ const destBase = join(opts.staging, "workspace", "conversations");
275
+
276
+ // First pass: parse the name, apply the conversationId filter, validate
277
+ // that the entry is a real directory (not a symlink, not a regular
278
+ // file), then apply the time-window filter (which may need to read
279
+ // `messages.jsonl`). Collect surviving candidates so we can sort them
280
+ // deterministically before applying the byte cap.
281
+ //
282
+ // The non-directory / symlink validation happens BEFORE the message
283
+ // scan so a canonical-named symlink can never coerce
284
+ // `conversationHasMessageInWindow` into reading from outside the
285
+ // `conversations/` boundary.
286
+ const candidates: Array<{
287
+ name: string;
288
+ parsed: { conversationId: string; createdAtMs: number };
289
+ }> = [];
290
+ for (const name of names) {
291
+ let parsed: ReturnType<typeof parseConversationDirName>;
292
+ try {
293
+ parsed = parseConversationDirName(name);
294
+ } catch (err) {
295
+ log.warn(
296
+ { err, name },
297
+ "Failed to parse conversation directory name; skipping",
298
+ );
299
+ continue;
300
+ }
301
+ if (!parsed) continue; // Rule 3 — default deny non-canonical names.
302
+
303
+ if (
304
+ opts.conversationId !== undefined &&
305
+ parsed.conversationId !== opts.conversationId
306
+ ) {
307
+ continue;
308
+ }
309
+
310
+ const srcPath = join(sourceDir, name);
311
+
312
+ // Boundary guard: a canonical-looking entry must be a real directory
313
+ // under `conversations/`. Use `lstatSync` (not `statSync`) so
314
+ // symlinks are not dereferenced — a symlink with a canonical name
315
+ // pointing at an external directory must not be allowed to escape
316
+ // the allowlist boundary, neither for the time-window message scan
317
+ // below nor for the eventual `cpSync` copy. Symlinks and regular
318
+ // files are rejected explicitly here so the message scan and the
319
+ // copy loop only ever see real directories.
320
+ let srcStat: ReturnType<typeof lstatSync>;
321
+ try {
322
+ srcStat = lstatSync(srcPath);
323
+ } catch (err) {
324
+ log.warn({ err, srcPath }, "Failed to stat conversation entry; skipping");
325
+ continue;
326
+ }
327
+ if (srcStat.isSymbolicLink()) {
328
+ log.warn(
329
+ { srcPath },
330
+ "Conversation entry is a symbolic link; skipping to preserve allowlist boundary",
331
+ );
332
+ continue;
333
+ }
334
+ if (!srcStat.isDirectory()) {
335
+ log.warn({ srcPath }, "Conversation entry is not a directory; skipping");
336
+ continue;
337
+ }
338
+
339
+ // Time-window filter: keep the conversation if EITHER its createdAt
340
+ // (parsed from the directory name) OR any individual message inside
341
+ // `messages.jsonl` falls in the requested window. This is the union
342
+ // semantics — a conversation that was started before the window but
343
+ // received messages during it should still ship, since the user
344
+ // running an export almost always wants to see the activity that
345
+ // happened during the window, not just conversations that were
346
+ // _created_ in it.
347
+ if (opts.startTime !== undefined || opts.endTime !== undefined) {
348
+ const createdAtInWindow =
349
+ (opts.startTime === undefined ||
350
+ parsed.createdAtMs >= opts.startTime) &&
351
+ (opts.endTime === undefined || parsed.createdAtMs <= opts.endTime);
352
+ if (!createdAtInWindow) {
353
+ // Fall back to scanning messages.jsonl for in-window activity.
354
+ // This is more expensive than the directory-name parse, so we
355
+ // only do it when the cheap check failed. The boundary guard
356
+ // above guarantees `srcPath` is a real in-allowlist directory,
357
+ // so the file path the scanner reads stays inside the allowlist.
358
+ let hasMessageInWindow: boolean;
359
+ try {
360
+ hasMessageInWindow = conversationHasMessageInWindow(
361
+ srcPath,
362
+ opts.startTime,
363
+ opts.endTime,
364
+ );
365
+ } catch (err) {
366
+ log.warn(
367
+ { err, srcPath },
368
+ "Failed to scan messages.jsonl for window match; skipping",
369
+ );
370
+ continue;
371
+ }
372
+ if (!hasMessageInWindow) continue;
373
+ }
374
+ }
375
+
376
+ candidates.push({ name, parsed });
377
+ }
378
+
379
+ // Newest first so cap-truncation keeps the most recent conversations.
380
+ candidates.sort((a, b) => b.parsed.createdAtMs - a.parsed.createdAtMs);
381
+
382
+ for (const { name } of candidates) {
383
+ const srcPath = join(sourceDir, name);
384
+
385
+ const remainingBudget = maxBytes - result.totalBytes;
386
+ let dirBytes: number | null;
387
+ try {
388
+ dirBytes = dirSizeWithinBudget(srcPath, remainingBudget);
389
+ } catch (err) {
390
+ log.warn(
391
+ { err, srcPath },
392
+ "Failed to compute conversation directory size; skipping",
393
+ );
394
+ continue;
395
+ }
396
+
397
+ if (dirBytes === null) {
398
+ // Including this directory would exceed the workspace cap.
399
+ entry.skippedDueToCap += 1;
400
+ continue;
401
+ }
402
+
403
+ try {
404
+ mkdirSync(destBase, { recursive: true });
405
+ cpSync(srcPath, join(destBase, name), { recursive: true });
406
+ } catch (err) {
407
+ log.warn(
408
+ { err, srcPath },
409
+ "Failed to copy conversation directory; skipping",
410
+ );
411
+ continue;
412
+ }
413
+
414
+ entry.itemCount += 1;
415
+ entry.bytes += dirBytes;
416
+ result.totalBytes += dirBytes;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Collect allowlisted workspace data into `<staging>/workspace/`.
422
+ *
423
+ * Currently the only allowlisted entry is `conversations/`. Future entries
424
+ * should follow the rules in `./AGENTS.md` (time filter, conversation
425
+ * filter, byte cap, registry update). The function never throws — all
426
+ * filesystem errors are logged at warn level so the rest of the export
427
+ * pipeline can continue regardless.
428
+ */
429
+ export function collectWorkspaceData(
430
+ opts: CollectWorkspaceDataOptions,
431
+ ): CollectWorkspaceDataResult {
432
+ const result: CollectWorkspaceDataResult = {
433
+ entries: [],
434
+ totalBytes: 0,
435
+ };
436
+
437
+ try {
438
+ collectConversations(opts, result);
439
+ } catch (err) {
440
+ log.warn(
441
+ { err },
442
+ "Unexpected error while collecting workspace conversations entry",
443
+ );
444
+ }
445
+
446
+ log.info(
447
+ {
448
+ entries: result.entries,
449
+ totalBytes: result.totalBytes,
450
+ conversationId: opts.conversationId ?? null,
451
+ startTime: opts.startTime ?? null,
452
+ endTime: opts.endTime ?? null,
453
+ },
454
+ "Workspace allowlist collection complete",
455
+ );
456
+
457
+ return result;
458
+ }