@vellumai/assistant 0.6.3 → 0.6.4

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 (667) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +5 -13
  4. package/docs/backup-troubleshooting.md +52 -0
  5. package/docs/browser-use-architecture-phase2.md +174 -0
  6. package/docs/stt-provider-onboarding.md +120 -0
  7. package/knip.json +12 -2
  8. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  9. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  10. package/openapi.yaml +982 -72
  11. package/package.json +4 -6
  12. package/scripts/generate-openapi.ts +0 -1
  13. package/scripts/test.sh +73 -18
  14. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  15. package/src/__tests__/agent-loop.test.ts +123 -0
  16. package/src/__tests__/anthropic-provider.test.ts +263 -10
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  18. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  19. package/src/__tests__/browser-fill-credential.test.ts +11 -0
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  22. package/src/__tests__/btw-routes.test.ts +7 -0
  23. package/src/__tests__/call-controller.test.ts +581 -20
  24. package/src/__tests__/catalog-files.test.ts +138 -0
  25. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  26. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  27. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  28. package/src/__tests__/checker.test.ts +157 -10
  29. package/src/__tests__/clawhub-files.test.ts +347 -0
  30. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  31. package/src/__tests__/config-analysis.test.ts +100 -0
  32. package/src/__tests__/config-schema.test.ts +1013 -66
  33. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  34. package/src/__tests__/config-watcher.test.ts +43 -8
  35. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  36. package/src/__tests__/contacts-write.test.ts +197 -0
  37. package/src/__tests__/context-window-manager.test.ts +88 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +98 -2
  41. package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
  42. package/src/__tests__/conversation-error.test.ts +70 -0
  43. package/src/__tests__/conversation-history-web-search.test.ts +11 -4
  44. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  45. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  46. package/src/__tests__/conversation-list-source.test.ts +145 -0
  47. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  49. package/src/__tests__/conversation-queue.test.ts +901 -60
  50. package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
  51. package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
  52. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  53. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  59. package/src/__tests__/credential-health-service.test.ts +352 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  61. package/src/__tests__/credential-vault-unit.test.ts +379 -3
  62. package/src/__tests__/credentials-cli.test.ts +40 -16
  63. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  64. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  65. package/src/__tests__/device-id.test.ts +112 -0
  66. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  67. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  68. package/src/__tests__/email-html-renderer.test.ts +71 -0
  69. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  70. package/src/__tests__/emit-event-signal.test.ts +71 -0
  71. package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
  72. package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
  73. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  74. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  75. package/src/__tests__/gemini-provider.test.ts +64 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  77. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  78. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  79. package/src/__tests__/gmail-preferences.test.ts +117 -0
  80. package/src/__tests__/headless-browser-interactions.test.ts +43 -0
  81. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  82. package/src/__tests__/headless-browser-navigate.test.ts +142 -5
  83. package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
  84. package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
  85. package/src/__tests__/heartbeat-service.test.ts +70 -17
  86. package/src/__tests__/home-state-routes.test.ts +162 -0
  87. package/src/__tests__/host-bash-proxy.test.ts +0 -5
  88. package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
  89. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
  90. package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
  91. package/src/__tests__/host-cu-proxy.test.ts +0 -5
  92. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  93. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  94. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  95. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  96. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  97. package/src/__tests__/llm-usage-store.test.ts +363 -0
  98. package/src/__tests__/media-stream-output.test.ts +555 -0
  99. package/src/__tests__/media-stream-parser.test.ts +374 -0
  100. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  101. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  102. package/src/__tests__/media-turn-detector.test.ts +440 -0
  103. package/src/__tests__/message-queue.test.ts +125 -0
  104. package/src/__tests__/migration-export-http.test.ts +6 -6
  105. package/src/__tests__/migration-import-commit-http.test.ts +8 -6
  106. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  107. package/src/__tests__/migration-validate-http.test.ts +3 -3
  108. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  109. package/src/__tests__/model-intents.test.ts +2 -2
  110. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  111. package/src/__tests__/oauth-cli.test.ts +2 -0
  112. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  113. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  114. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  115. package/src/__tests__/oauth-store.test.ts +85 -0
  116. package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
  117. package/src/__tests__/onboarding-template-contract.test.ts +6 -13
  118. package/src/__tests__/openai-provider.test.ts +176 -0
  119. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  120. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  121. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  122. package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
  123. package/src/__tests__/persona-resolver.test.ts +251 -0
  124. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  125. package/src/__tests__/platform.test.ts +92 -1
  126. package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
  127. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  128. package/src/__tests__/pricing.test.ts +174 -0
  129. package/src/__tests__/qdrant-manager.test.ts +29 -8
  130. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  131. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  132. package/src/__tests__/relay-server.test.ts +423 -5
  133. package/src/__tests__/search-skills-unified.test.ts +118 -0
  134. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  135. package/src/__tests__/secure-keys.test.ts +107 -0
  136. package/src/__tests__/send-endpoint-busy.test.ts +5 -1
  137. package/src/__tests__/sequence-store.test.ts +1 -1
  138. package/src/__tests__/server-history-render.test.ts +49 -0
  139. package/src/__tests__/settings-routes.test.ts +201 -0
  140. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  141. package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
  142. package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
  143. package/src/__tests__/skills.test.ts +5 -2
  144. package/src/__tests__/skillssh-files.test.ts +446 -0
  145. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  146. package/src/__tests__/slack-channel-config.test.ts +564 -1
  147. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  148. package/src/__tests__/stt-stream-session.test.ts +535 -0
  149. package/src/__tests__/system-prompt.test.ts +112 -26
  150. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  151. package/src/__tests__/terminal-tools.test.ts +18 -7
  152. package/src/__tests__/test-preload.ts +18 -0
  153. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  154. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  155. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  156. package/src/__tests__/tool-executor.test.ts +33 -24
  157. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  158. package/src/__tests__/trust-store.test.ts +7 -1
  159. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  160. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  161. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  162. package/src/__tests__/twilio-routes.test.ts +376 -0
  163. package/src/__tests__/unicode.test.ts +293 -0
  164. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  165. package/src/__tests__/update-bulletin.test.ts +206 -5
  166. package/src/__tests__/usage-routes.test.ts +25 -4
  167. package/src/__tests__/user-reference.test.ts +46 -61
  168. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  169. package/src/__tests__/voice-config-update.test.ts +403 -0
  170. package/src/__tests__/voice-quality.test.ts +434 -19
  171. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  172. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  173. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  174. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  175. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  176. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  177. package/src/__tests__/workspace-policy.test.ts +2 -0
  178. package/src/agent/image-optimize.ts +24 -12
  179. package/src/agent/loop.ts +43 -3
  180. package/src/backup/__tests__/backup-key.test.ts +152 -0
  181. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  182. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  183. package/src/backup/__tests__/local-writer.test.ts +218 -0
  184. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  185. package/src/backup/__tests__/paths.test.ts +300 -0
  186. package/src/backup/__tests__/restore.test.ts +498 -0
  187. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  188. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  189. package/src/backup/backup-key.ts +137 -0
  190. package/src/backup/backup-worker.ts +459 -0
  191. package/src/backup/list-snapshots.ts +147 -0
  192. package/src/backup/local-writer.ts +133 -0
  193. package/src/backup/offsite-writer.ts +222 -0
  194. package/src/backup/paths.ts +226 -0
  195. package/src/backup/restore.ts +322 -0
  196. package/src/backup/snapshot-lock.ts +431 -0
  197. package/src/backup/stream-crypt.ts +263 -0
  198. package/src/bundler/package-resolver.ts +4 -0
  199. package/src/calls/audio-store.ts +11 -5
  200. package/src/calls/call-controller.ts +226 -71
  201. package/src/calls/call-domain.ts +9 -0
  202. package/src/calls/call-speech-output.ts +190 -0
  203. package/src/calls/call-transport.ts +77 -0
  204. package/src/calls/media-stream-audio-transcode.ts +173 -0
  205. package/src/calls/media-stream-output.ts +660 -0
  206. package/src/calls/media-stream-parser.ts +300 -0
  207. package/src/calls/media-stream-protocol.ts +166 -0
  208. package/src/calls/media-stream-server.ts +592 -0
  209. package/src/calls/media-stream-stt-session.ts +460 -0
  210. package/src/calls/media-turn-detector.ts +230 -0
  211. package/src/calls/relay-server.ts +90 -75
  212. package/src/calls/resolve-call-tts-provider.ts +136 -0
  213. package/src/calls/telephony-stt-routing.ts +145 -0
  214. package/src/calls/tts-call-strategy.ts +161 -0
  215. package/src/calls/tts-text-sanitizer.ts +32 -16
  216. package/src/calls/twilio-routes.ts +281 -17
  217. package/src/calls/voice-quality.ts +78 -35
  218. package/src/calls/voice-session-bridge.ts +8 -1
  219. package/src/channels/types.ts +16 -0
  220. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  221. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  222. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  223. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  224. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  225. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  226. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  227. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  228. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  229. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  230. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  231. package/src/cli/commands/backup.ts +993 -0
  232. package/src/cli/commands/conversations.ts +77 -0
  233. package/src/cli/commands/credentials.ts +0 -1
  234. package/src/cli/commands/domain.ts +210 -0
  235. package/src/cli/commands/email.ts +255 -3
  236. package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
  237. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
  238. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
  239. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
  240. package/src/cli/commands/oauth/mode.ts +12 -3
  241. package/src/cli/commands/oauth/providers.ts +15 -0
  242. package/src/cli/commands/oauth/shared.ts +2 -1
  243. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
  244. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  245. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  246. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  247. package/src/cli/program.ts +30 -4
  248. package/src/config/__tests__/backup-schema.test.ts +134 -0
  249. package/src/config/assistant-feature-flags.ts +61 -62
  250. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
  251. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  252. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  253. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  254. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  255. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  256. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +2 -2
  258. package/src/config/bundled-skills/gmail/SKILL.md +53 -7
  259. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  260. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  261. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  262. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  263. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  264. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  265. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  266. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  267. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  268. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  269. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  270. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  271. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  272. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  273. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  274. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  275. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  276. package/src/config/bundled-skills/outlook/SKILL.md +2 -2
  277. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  278. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  279. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  280. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  281. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  282. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  283. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  284. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  285. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  286. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  287. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  288. package/src/config/bundled-tool-registry.ts +8 -0
  289. package/src/config/env-registry.ts +24 -0
  290. package/src/config/env.ts +34 -10
  291. package/src/config/feature-flag-registry.json +46 -14
  292. package/src/config/loader.ts +26 -12
  293. package/src/config/schema.ts +35 -10
  294. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  295. package/src/config/schemas/analysis.ts +51 -0
  296. package/src/config/schemas/backup.ts +72 -0
  297. package/src/config/schemas/calls.ts +1 -26
  298. package/src/config/schemas/elevenlabs.ts +0 -59
  299. package/src/config/schemas/filing.ts +47 -7
  300. package/src/config/schemas/heartbeat.ts +27 -5
  301. package/src/config/schemas/host-browser.ts +47 -1
  302. package/src/config/schemas/inference.ts +1 -1
  303. package/src/config/schemas/memory-lifecycle.ts +14 -2
  304. package/src/config/schemas/services.ts +44 -0
  305. package/src/config/schemas/stt.ts +59 -0
  306. package/src/config/schemas/tts.ts +230 -0
  307. package/src/config/schemas/updates.ts +14 -0
  308. package/src/config/skills.ts +4 -0
  309. package/src/config/types.ts +4 -0
  310. package/src/contacts/contact-store.ts +56 -11
  311. package/src/contacts/contacts-write.ts +38 -1
  312. package/src/context/post-turn-tool-result-truncation.ts +3 -2
  313. package/src/context/tool-result-truncation.ts +2 -1
  314. package/src/context/window-manager.ts +45 -12
  315. package/src/credential-execution/executable-discovery.ts +12 -2
  316. package/src/credential-execution/process-manager.ts +33 -2
  317. package/src/credential-health/credential-health-service.ts +366 -0
  318. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  319. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  320. package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
  321. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  322. package/src/daemon/config-watcher.ts +99 -5
  323. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  324. package/src/daemon/conversation-agent-loop.ts +101 -24
  325. package/src/daemon/conversation-error.ts +11 -0
  326. package/src/daemon/conversation-history.ts +40 -6
  327. package/src/daemon/conversation-launch.ts +220 -0
  328. package/src/daemon/conversation-lifecycle.ts +59 -9
  329. package/src/daemon/conversation-messaging.ts +37 -3
  330. package/src/daemon/conversation-notifiers.ts +5 -0
  331. package/src/daemon/conversation-process.ts +581 -19
  332. package/src/daemon/conversation-queue-manager.ts +24 -0
  333. package/src/daemon/conversation-runtime-assembly.ts +11 -1
  334. package/src/daemon/conversation-slash.ts +36 -0
  335. package/src/daemon/conversation-surfaces.ts +94 -4
  336. package/src/daemon/conversation-tool-setup.ts +25 -0
  337. package/src/daemon/conversation-usage.ts +7 -4
  338. package/src/daemon/conversation.ts +86 -28
  339. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  340. package/src/daemon/handlers/conversations.ts +4 -1
  341. package/src/daemon/handlers/shared.ts +22 -0
  342. package/src/daemon/handlers/skills.ts +321 -77
  343. package/src/daemon/host-browser-proxy.ts +2 -1
  344. package/src/daemon/lifecycle.ts +122 -25
  345. package/src/daemon/message-protocol.ts +6 -0
  346. package/src/daemon/message-types/conversations.ts +34 -1
  347. package/src/daemon/message-types/home.ts +40 -0
  348. package/src/daemon/message-types/meet.ts +143 -0
  349. package/src/daemon/message-types/messages.ts +14 -0
  350. package/src/daemon/message-types/schedules.ts +34 -2
  351. package/src/daemon/message-types/skills.ts +16 -0
  352. package/src/daemon/message-types/surfaces.ts +2 -0
  353. package/src/daemon/server.ts +347 -2
  354. package/src/daemon/shutdown-handlers.ts +32 -4
  355. package/src/daemon/shutdown-registry.ts +40 -0
  356. package/src/daemon/tool-side-effects.ts +9 -0
  357. package/src/email/html-renderer.ts +76 -0
  358. package/src/heartbeat/heartbeat-service.ts +93 -7
  359. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  360. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  361. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  362. package/src/home/__tests__/feed-types.test.ts +275 -0
  363. package/src/home/__tests__/feed-writer.test.ts +688 -0
  364. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  365. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  366. package/src/home/__tests__/progress-formula.test.ts +213 -0
  367. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  368. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  369. package/src/home/assistant-feed-authoring.ts +124 -0
  370. package/src/home/emit-feed-event.ts +158 -0
  371. package/src/home/feed-scheduler.ts +247 -0
  372. package/src/home/feed-types.ts +181 -0
  373. package/src/home/feed-writer.ts +469 -0
  374. package/src/home/platform-gmail-digest.ts +163 -0
  375. package/src/home/progress-formula.ts +86 -0
  376. package/src/home/relationship-state-writer.ts +824 -0
  377. package/src/home/relationship-state.ts +143 -0
  378. package/src/home/rollup-producer.ts +384 -0
  379. package/src/hooks/runner.ts +7 -0
  380. package/src/inbound/platform-callback-registration.ts +12 -3
  381. package/src/inbound/public-ingress-urls.ts +12 -0
  382. package/src/instrument.ts +1 -1
  383. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  384. package/src/ipc/cli-client.ts +151 -0
  385. package/src/ipc/cli-server.ts +234 -0
  386. package/src/ipc/gateway-client.ts +180 -0
  387. package/src/ipc/routes/index.ts +5 -0
  388. package/src/ipc/routes/wake-conversation.ts +19 -0
  389. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  390. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  391. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  392. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  393. package/src/memory/app-store.ts +1 -1
  394. package/src/memory/attachments-store.ts +70 -0
  395. package/src/memory/auto-analysis-enqueue.ts +127 -0
  396. package/src/memory/auto-analysis-guard.ts +27 -0
  397. package/src/memory/cleanup-schedule-state.ts +37 -0
  398. package/src/memory/conversation-analyze-job.ts +73 -0
  399. package/src/memory/conversation-crud.ts +99 -0
  400. package/src/memory/conversation-disk-view.ts +7 -0
  401. package/src/memory/conversation-group-migration.ts +34 -2
  402. package/src/memory/conversation-queries.ts +6 -5
  403. package/src/memory/db-init.ts +6 -0
  404. package/src/memory/db-maintenance.ts +108 -0
  405. package/src/memory/db.ts +1 -0
  406. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  407. package/src/memory/graph/extraction.test.ts +23 -0
  408. package/src/memory/graph/extraction.ts +8 -0
  409. package/src/memory/graph/retriever.ts +27 -18
  410. package/src/memory/graph/scoring.test.ts +186 -0
  411. package/src/memory/graph/scoring.ts +31 -1
  412. package/src/memory/graph/tools.ts +1 -1
  413. package/src/memory/group-crud.ts +6 -1
  414. package/src/memory/indexer.ts +95 -16
  415. package/src/memory/job-handlers/cleanup.ts +11 -8
  416. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  417. package/src/memory/jobs-store.ts +64 -4
  418. package/src/memory/jobs-worker.ts +22 -9
  419. package/src/memory/llm-usage-store.ts +92 -56
  420. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  421. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  422. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  423. package/src/memory/migrations/index.ts +6 -0
  424. package/src/memory/migrations/registry.ts +8 -0
  425. package/src/memory/qdrant-manager.ts +43 -16
  426. package/src/memory/schema/conversations.ts +2 -0
  427. package/src/memory/schema/oauth.ts +3 -0
  428. package/src/memory/usage-buckets.ts +396 -0
  429. package/src/messaging/providers/gmail/client.ts +57 -6
  430. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  431. package/src/messaging/providers/slack/adapter.ts +143 -38
  432. package/src/messaging/providers/slack/client.ts +16 -0
  433. package/src/messaging/providers/slack/types.ts +4 -0
  434. package/src/notifications/decision-engine.ts +3 -3
  435. package/src/notifications/signal.ts +5 -0
  436. package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
  437. package/src/oauth/byo-connection.test.ts +18 -1
  438. package/src/oauth/byo-connection.ts +3 -1
  439. package/src/oauth/connect-orchestrator.ts +2 -0
  440. package/src/oauth/connection-resolver.ts +6 -2
  441. package/src/oauth/connection.ts +2 -0
  442. package/src/oauth/oauth-store.ts +9 -0
  443. package/src/oauth/platform-connection.test.ts +98 -0
  444. package/src/oauth/platform-connection.ts +52 -31
  445. package/src/oauth/seed-providers.ts +7 -0
  446. package/src/permissions/checker.ts +16 -6
  447. package/src/permissions/defaults.ts +49 -1
  448. package/src/permissions/trust-store.ts +3 -3
  449. package/src/permissions/workspace-policy.ts +3 -0
  450. package/src/platform/client.test.ts +10 -0
  451. package/src/platform/sync-identity.ts +129 -0
  452. package/src/prompts/persona-resolver.ts +126 -2
  453. package/src/prompts/system-prompt.ts +59 -18
  454. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  455. package/src/prompts/templates/SOUL.md +3 -1
  456. package/src/prompts/templates/UPDATES.md +12 -0
  457. package/src/prompts/templates/channels/slack.md +20 -0
  458. package/src/prompts/update-bulletin-format.ts +26 -9
  459. package/src/prompts/update-bulletin.ts +34 -23
  460. package/src/prompts/user-reference.ts +20 -17
  461. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  462. package/src/providers/anthropic/client.ts +157 -61
  463. package/src/providers/fireworks/client.ts +2 -2
  464. package/src/providers/gemini/client.ts +9 -1
  465. package/src/providers/model-catalog.ts +6 -0
  466. package/src/providers/model-intents.ts +4 -4
  467. package/src/providers/ollama/client.ts +2 -2
  468. package/src/providers/openai/chat-completions-provider.ts +474 -0
  469. package/src/providers/openai/client.ts +25 -440
  470. package/src/providers/openai/responses-provider.ts +502 -0
  471. package/src/providers/openrouter/client.ts +101 -4
  472. package/src/providers/provider-secret-catalog.ts +139 -0
  473. package/src/providers/registry.ts +2 -2
  474. package/src/providers/retry.ts +14 -3
  475. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  476. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  477. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  478. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  479. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  480. package/src/providers/speech-to-text/deepgram.ts +115 -0
  481. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  482. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  483. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  484. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  485. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  486. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  487. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  488. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  489. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  490. package/src/providers/speech-to-text/resolve.ts +386 -6
  491. package/src/providers/types.ts +9 -0
  492. package/src/runtime/AGENTS.md +43 -1
  493. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  494. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  495. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  496. package/src/runtime/agent-wake.ts +512 -0
  497. package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
  498. package/src/runtime/auth/route-policy.ts +30 -5
  499. package/src/runtime/auth/token-service.ts +56 -1
  500. package/src/runtime/btw-sidechain.ts +2 -0
  501. package/src/runtime/capability-tokens.ts +10 -10
  502. package/src/runtime/channel-invite-transport.ts +1 -1
  503. package/src/runtime/channel-invite-transports/email.ts +14 -6
  504. package/src/runtime/channel-readiness-service.ts +12 -22
  505. package/src/runtime/chrome-extension-registry.ts +38 -2
  506. package/src/runtime/http-server.ts +395 -10
  507. package/src/runtime/http-types.ts +6 -2
  508. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
  509. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  510. package/src/runtime/migrations/migration-transport.ts +1 -0
  511. package/src/runtime/migrations/migration-wizard.ts +1 -0
  512. package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
  513. package/src/runtime/migrations/vbundle-importer.ts +34 -0
  514. package/src/runtime/pending-interactions.ts +0 -11
  515. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  516. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  517. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  518. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  519. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  520. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  521. package/src/runtime/routes/app-management-routes.ts +12 -18
  522. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  523. package/src/runtime/routes/attachment-routes.ts +216 -17
  524. package/src/runtime/routes/backup-routes.ts +519 -0
  525. package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
  526. package/src/runtime/routes/btw-routes.ts +8 -6
  527. package/src/runtime/routes/contact-routes.test.ts +298 -0
  528. package/src/runtime/routes/contact-routes.ts +132 -5
  529. package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
  530. package/src/runtime/routes/conversation-management-routes.ts +115 -0
  531. package/src/runtime/routes/conversation-routes.ts +367 -146
  532. package/src/runtime/routes/filing-routes.ts +93 -0
  533. package/src/runtime/routes/home-feed-routes.ts +334 -0
  534. package/src/runtime/routes/home-state-routes.ts +138 -0
  535. package/src/runtime/routes/host-browser-routes.ts +3 -14
  536. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  537. package/src/runtime/routes/identity-routes.ts +3 -17
  538. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  539. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  540. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  541. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  542. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  543. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  544. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  545. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  546. package/src/runtime/routes/migration-routes.ts +40 -5
  547. package/src/runtime/routes/settings-routes.ts +22 -5
  548. package/src/runtime/routes/skills-routes.ts +76 -7
  549. package/src/runtime/routes/stt-routes.ts +233 -0
  550. package/src/runtime/routes/surface-action-routes.ts +41 -2
  551. package/src/runtime/routes/tts-routes.ts +108 -24
  552. package/src/runtime/routes/usage-routes.ts +30 -2
  553. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  554. package/src/runtime/routes/user-routes.ts +13 -1
  555. package/src/runtime/routes/work-items-routes.ts +8 -1
  556. package/src/runtime/runtime-mode.ts +33 -0
  557. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  558. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  559. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  560. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  561. package/src/runtime/services/analyze-conversation.ts +344 -0
  562. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  563. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  564. package/src/runtime/skill-route-registry.ts +49 -0
  565. package/src/runtime/slack-block-formatting.ts +437 -10
  566. package/src/schedule/scheduler.ts +50 -0
  567. package/src/security/oauth2.ts +26 -4
  568. package/src/security/secure-keys.ts +25 -2
  569. package/src/security/token-manager.ts +8 -0
  570. package/src/sequence/engine.ts +23 -0
  571. package/src/sequence/types.ts +1 -1
  572. package/src/skills/catalog-files.ts +64 -2
  573. package/src/skills/category-inference.ts +122 -0
  574. package/src/skills/clawhub-files.ts +213 -0
  575. package/src/skills/clawhub.ts +84 -23
  576. package/src/skills/skill-file-provider.ts +40 -0
  577. package/src/skills/skillssh-files.ts +395 -0
  578. package/src/skills/skillssh-registry.ts +4 -4
  579. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  580. package/src/stt/__tests__/types.test.ts +89 -0
  581. package/src/stt/daemon-batch-transcriber.ts +195 -0
  582. package/src/stt/stt-stream-session.ts +499 -0
  583. package/src/stt/types.ts +330 -0
  584. package/src/stt/wav-encoder.test.ts +373 -0
  585. package/src/stt/wav-encoder.ts +175 -0
  586. package/src/subagent/manager.ts +38 -14
  587. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  588. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  589. package/src/tools/browser/browser-execution.ts +1163 -23
  590. package/src/tools/browser/browser-manager.ts +45 -0
  591. package/src/tools/browser/browser-mode-constants.ts +12 -0
  592. package/src/tools/browser/browser-mode.ts +92 -0
  593. package/src/tools/browser/browser-status-constants.ts +33 -0
  594. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
  595. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
  596. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
  597. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
  598. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
  599. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
  600. package/src/tools/browser/cdp-client/errors.ts +15 -0
  601. package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
  602. package/src/tools/browser/cdp-client/factory.ts +797 -87
  603. package/src/tools/browser/cdp-client/index.ts +16 -2
  604. package/src/tools/browser/cdp-client/types.ts +68 -0
  605. package/src/tools/credentials/vault.ts +35 -6
  606. package/src/tools/network/web-fetch.ts +5 -2
  607. package/src/tools/network/web-search.ts +5 -2
  608. package/src/tools/shared/shell-output.ts +3 -1
  609. package/src/tools/side-effects.ts +2 -0
  610. package/src/tools/skills/sandbox-runner.ts +3 -2
  611. package/src/tools/terminal/safe-env.ts +10 -2
  612. package/src/tools/terminal/shell.ts +15 -4
  613. package/src/tools/tool-manifest.ts +21 -0
  614. package/src/tools/types.ts +17 -0
  615. package/src/tools/ui-surface/definitions.ts +6 -1
  616. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  617. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  618. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  619. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  620. package/src/tts/provider-catalog.ts +201 -0
  621. package/src/tts/provider-registry.ts +73 -0
  622. package/src/tts/providers/deepgram-provider.ts +219 -0
  623. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  624. package/src/tts/providers/fish-audio-provider.ts +183 -0
  625. package/src/tts/providers/index.ts +42 -0
  626. package/src/tts/providers/register-builtins.ts +130 -0
  627. package/src/tts/synthesize-text.ts +110 -0
  628. package/src/tts/tts-config-resolver.ts +78 -0
  629. package/src/tts/types.ts +153 -0
  630. package/src/types/onboarding-context.ts +7 -0
  631. package/src/util/abort-reasons.ts +58 -0
  632. package/src/util/device-id.ts +32 -16
  633. package/src/util/errors.ts +9 -1
  634. package/src/util/platform.ts +54 -10
  635. package/src/util/pricing.ts +66 -3
  636. package/src/util/spawn.ts +1 -1
  637. package/src/util/truncate.ts +4 -2
  638. package/src/util/unicode.ts +201 -0
  639. package/src/version.ts +19 -24
  640. package/src/watcher/engine.ts +23 -0
  641. package/src/watcher/watcher-store.ts +31 -0
  642. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  643. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  644. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  645. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  646. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  647. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  648. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  649. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  650. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  651. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  652. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  653. package/src/workspace/migrations/registry.ts +16 -0
  654. package/src/workspace/top-level-renderer.ts +13 -1
  655. package/src/workspace/turn-commit.ts +31 -0
  656. package/src/__tests__/email-cli.test.ts +0 -297
  657. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  658. package/src/cli/commands/browser-relay.ts +0 -466
  659. package/src/email/guardrails.ts +0 -221
  660. package/src/email/provider.ts +0 -117
  661. package/src/email/providers/agentmail.ts +0 -361
  662. package/src/email/providers/index.ts +0 -65
  663. package/src/email/service.ts +0 -384
  664. package/src/email/types.ts +0 -126
  665. package/src/prompts/templates/USER.md +0 -13
  666. package/src/providers/speech-to-text/types.ts +0 -17
  667. package/src/runtime/routes/browser-cdp-routes.ts +0 -229
@@ -0,0 +1,993 @@
1
+ /**
2
+ * `vellum backup` — manage automated backups, on-demand snapshots, restore, and verify.
3
+ *
4
+ * All subcommands run in-process (they do not call the daemon HTTP port).
5
+ * Config mutations go through `loadRawConfig` / `setNestedValue` / `saveRawConfig`
6
+ * so the on-disk `config.json` is the single source of truth and the daemon's
7
+ * config cache is invalidated via `saveRawConfig`.
8
+ */
9
+
10
+ import { stat } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+
13
+ import type { Command } from "commander";
14
+
15
+ import { readBackupKey } from "../../backup/backup-key.js";
16
+ import { createSnapshotNow } from "../../backup/backup-worker.js";
17
+ import {
18
+ listSnapshotsInDir,
19
+ type SnapshotEntry,
20
+ } from "../../backup/list-snapshots.js";
21
+ import {
22
+ getBackupKeyPath,
23
+ getLocalBackupsDir,
24
+ resolveOffsiteDestinations,
25
+ } from "../../backup/paths.js";
26
+ import { restoreFromSnapshot, verifySnapshot } from "../../backup/restore.js";
27
+ import {
28
+ getConfig,
29
+ invalidateConfigCache,
30
+ loadRawConfig,
31
+ saveRawConfig,
32
+ setNestedValue,
33
+ } from "../../config/loader.js";
34
+ import type { BackupDestination } from "../../config/schema.js";
35
+ import { isDaemonRunning } from "../../daemon/daemon-control.js";
36
+ import { getMemoryCheckpoint } from "../../memory/checkpoints.js";
37
+ import { resetDb } from "../../memory/db-connection.js";
38
+ import { clearCache as clearTrustCache } from "../../permissions/trust-store.js";
39
+ import { DefaultPathResolver } from "../../runtime/migrations/vbundle-import-analyzer.js";
40
+ import {
41
+ getWorkspaceDir,
42
+ getWorkspaceHooksDir,
43
+ } from "../../util/platform.js";
44
+ import { log } from "../logger.js";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Small formatting helpers
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /** Format a byte count as a human-readable string (B / KB / MB / GB). */
51
+ function formatBytes(bytes: number): string {
52
+ if (bytes < 1024) return `${bytes} B`;
53
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
54
+ if (bytes < 1024 * 1024 * 1024)
55
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
56
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
57
+ }
58
+
59
+ /** Format a Date as `YYYY-MM-DD HH:MM UTC`. */
60
+ function formatDate(date: Date): string {
61
+ const y = date.getUTCFullYear().toString().padStart(4, "0");
62
+ const mo = (date.getUTCMonth() + 1).toString().padStart(2, "0");
63
+ const d = date.getUTCDate().toString().padStart(2, "0");
64
+ const h = date.getUTCHours().toString().padStart(2, "0");
65
+ const mi = date.getUTCMinutes().toString().padStart(2, "0");
66
+ return `${y}-${mo}-${d} ${h}:${mi} UTC`;
67
+ }
68
+
69
+ /**
70
+ * Format a duration (milliseconds) as a short human string: "3h 12m",
71
+ * "12m", "45s", or "just now".
72
+ */
73
+ function formatDurationShort(ms: number): string {
74
+ if (ms < 0) ms = 0;
75
+ const seconds = Math.floor(ms / 1000);
76
+ if (seconds < 30) return "just now";
77
+ const minutes = Math.floor(seconds / 60);
78
+ if (minutes < 1) return `${seconds}s`;
79
+ const hours = Math.floor(minutes / 60);
80
+ const remMinutes = minutes - hours * 60;
81
+ if (hours < 1) return `${minutes}m`;
82
+ const days = Math.floor(hours / 24);
83
+ const remHours = hours - days * 24;
84
+ if (days < 1) return `${hours}h ${remMinutes}m`;
85
+ return `${days}d ${remHours}h`;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Reachability probe
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Check whether an offsite destination's parent directory exists. Mirrors the
94
+ * reachability check in `offsite-writer.ts` — if the parent is missing (e.g.
95
+ * iCloud Drive not enabled, external SSD unplugged) the destination is
96
+ * considered unreachable and we skip it at runtime.
97
+ */
98
+ async function isDestinationReachable(destPath: string): Promise<boolean> {
99
+ try {
100
+ await stat(dirname(destPath));
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Exported handlers — exported so tests can drive them directly.
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export interface EnableOptions {
112
+ interval?: string;
113
+ retention?: string;
114
+ offsite?: boolean;
115
+ }
116
+
117
+ export function handleEnable(opts: EnableOptions): void {
118
+ const raw = loadRawConfig();
119
+ setNestedValue(raw, "backup.enabled", true);
120
+
121
+ if (opts.interval !== undefined) {
122
+ const hours = Number.parseInt(opts.interval, 10);
123
+ if (!Number.isFinite(hours) || hours < 1) {
124
+ log.error(
125
+ `Invalid --interval "${opts.interval}". Must be a positive integer (hours). ` +
126
+ `Run 'vellum backup enable --help' for usage.`,
127
+ );
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ setNestedValue(raw, "backup.intervalHours", hours);
132
+ }
133
+
134
+ if (opts.retention !== undefined) {
135
+ const count = Number.parseInt(opts.retention, 10);
136
+ if (!Number.isFinite(count) || count < 1) {
137
+ log.error(
138
+ `Invalid --retention "${opts.retention}". Must be a positive integer. ` +
139
+ `Run 'vellum backup enable --help' for usage.`,
140
+ );
141
+ process.exitCode = 1;
142
+ return;
143
+ }
144
+ setNestedValue(raw, "backup.retention", count);
145
+ }
146
+
147
+ // commander's `.option("--no-offsite", ...)` sets `opts.offsite = false`
148
+ // when the flag is present and leaves it `undefined` otherwise. Only a
149
+ // literal `false` flips the offsite switch — we never touch `destinations`.
150
+ if (opts.offsite === false) {
151
+ setNestedValue(raw, "backup.offsite.enabled", false);
152
+ }
153
+
154
+ saveRawConfig(raw);
155
+
156
+ const cfg = getConfig().backup;
157
+ log.info(
158
+ `Automatic backups enabled (interval=${cfg.intervalHours}h, retention=${cfg.retention}, offsite=${cfg.offsite.enabled ? "on" : "off"})`,
159
+ );
160
+ }
161
+
162
+ export function handleDisable(): void {
163
+ const raw = loadRawConfig();
164
+ setNestedValue(raw, "backup.enabled", false);
165
+ saveRawConfig(raw);
166
+ log.info("Automatic backups disabled");
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // destinations subgroup handlers
171
+ // ---------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Load the raw destinations array, materializing the iCloud default on first
175
+ * touch. Returns the array plus the raw config so callers can mutate and
176
+ * re-persist.
177
+ *
178
+ * When `backup.offsite.destinations` is `null` in config, the runtime uses the
179
+ * iCloud default — but that default is implicit. On first `add`/`remove`/
180
+ * `set-encrypt`, we need to make it explicit so subsequent mutations have
181
+ * something to mutate.
182
+ */
183
+ function loadDestinationsForMutation(): {
184
+ raw: Record<string, unknown>;
185
+ destinations: BackupDestination[];
186
+ } {
187
+ const raw = loadRawConfig();
188
+ const current = getConfig().backup.offsite.destinations;
189
+ const destinations = resolveOffsiteDestinations(current);
190
+ return { raw, destinations };
191
+ }
192
+
193
+ export async function handleDestinationsList(): Promise<void> {
194
+ const cfg = getConfig().backup;
195
+ const destinations = resolveOffsiteDestinations(cfg.offsite.destinations);
196
+
197
+ if (destinations.length === 0) {
198
+ log.info("No offsite destinations configured");
199
+ return;
200
+ }
201
+
202
+ const pathW = Math.max(
203
+ 4,
204
+ ...destinations.map((d) => d.path.length),
205
+ );
206
+ log.info(
207
+ "Path".padEnd(pathW) + " " + "Encrypted",
208
+ );
209
+ log.info("-".repeat(pathW + 2 + 9));
210
+ for (const d of destinations) {
211
+ log.info(d.path.padEnd(pathW) + " " + (d.encrypt ? "yes" : "no"));
212
+ }
213
+ }
214
+
215
+ export interface DestinationAddOptions {
216
+ plaintext?: boolean;
217
+ }
218
+
219
+ export function handleDestinationsAdd(
220
+ path: string,
221
+ opts: DestinationAddOptions,
222
+ ): void {
223
+ const { raw, destinations } = loadDestinationsForMutation();
224
+
225
+ if (destinations.some((d) => d.path === path)) {
226
+ log.error(
227
+ `Destination "${path}" already exists. Run 'vellum backup destinations list' to see configured destinations.`,
228
+ );
229
+ process.exitCode = 1;
230
+ return;
231
+ }
232
+
233
+ const next: BackupDestination[] = [
234
+ ...destinations,
235
+ { path, encrypt: !opts.plaintext },
236
+ ];
237
+ setNestedValue(raw, "backup.offsite.destinations", next);
238
+ saveRawConfig(raw);
239
+ log.info(
240
+ `Added destination ${path} (${opts.plaintext ? "plaintext" : "encrypted"})`,
241
+ );
242
+ }
243
+
244
+ export function handleDestinationsRemove(path: string): void {
245
+ const { raw, destinations } = loadDestinationsForMutation();
246
+
247
+ const filtered = destinations.filter((d) => d.path !== path);
248
+ if (filtered.length === destinations.length) {
249
+ log.error(
250
+ `Destination "${path}" not found. Run 'vellum backup destinations list' to see configured destinations.`,
251
+ );
252
+ process.exitCode = 1;
253
+ return;
254
+ }
255
+
256
+ setNestedValue(raw, "backup.offsite.destinations", filtered);
257
+ saveRawConfig(raw);
258
+ log.info(`Removed destination ${path}`);
259
+ }
260
+
261
+ export function handleDestinationsSetEncrypt(
262
+ path: string,
263
+ value: string,
264
+ ): void {
265
+ const normalized = value.toLowerCase();
266
+ if (normalized !== "true" && normalized !== "false") {
267
+ log.error(
268
+ `Invalid encrypt value "${value}". Must be "true" or "false". ` +
269
+ `Run 'vellum backup destinations set-encrypt --help' for usage.`,
270
+ );
271
+ process.exitCode = 1;
272
+ return;
273
+ }
274
+ const encrypt = normalized === "true";
275
+
276
+ const { raw, destinations } = loadDestinationsForMutation();
277
+ const idx = destinations.findIndex((d) => d.path === path);
278
+ if (idx === -1) {
279
+ log.error(
280
+ `Destination "${path}" not found. Run 'vellum backup destinations list' to see configured destinations.`,
281
+ );
282
+ process.exitCode = 1;
283
+ return;
284
+ }
285
+
286
+ const next = destinations.map((d, i) =>
287
+ i === idx ? { ...d, encrypt } : d,
288
+ );
289
+ setNestedValue(raw, "backup.offsite.destinations", next);
290
+ saveRawConfig(raw);
291
+ log.info(
292
+ `Set ${path} encrypt=${encrypt ? "true" : "false"}`,
293
+ );
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // status
298
+ // ---------------------------------------------------------------------------
299
+
300
+ export async function handleStatus(): Promise<void> {
301
+ const cfg = getConfig().backup;
302
+
303
+ log.info(
304
+ `Automatic backups: ${cfg.enabled ? "enabled" : "disabled"}`,
305
+ );
306
+ log.info(`Interval: every ${cfg.intervalHours}h`);
307
+ log.info(
308
+ `Retention: ${cfg.retention} snapshots per destination`,
309
+ );
310
+
311
+ // Last / next run — both gated on a valid checkpoint. The daemon records
312
+ // `backup:last_run_at` as a unix-millis string.
313
+ const lastRunRaw = getMemoryCheckpoint("backup:last_run_at");
314
+ const lastRunMs = lastRunRaw ? Number.parseInt(lastRunRaw, 10) : NaN;
315
+ const now = Date.now();
316
+ if (!Number.isNaN(lastRunMs)) {
317
+ const lastRunDate = new Date(lastRunMs);
318
+ log.info(
319
+ `Last run: ${formatDate(lastRunDate)} (${formatDurationShort(now - lastRunMs)} ago)`,
320
+ );
321
+ if (cfg.enabled) {
322
+ const intervalMs = cfg.intervalHours * 3600 * 1000;
323
+ const nextMs = lastRunMs + intervalMs;
324
+ const delta = nextMs - now;
325
+ if (delta <= 0) {
326
+ log.info(`Next run: due now`);
327
+ } else {
328
+ log.info(`Next run: in ${formatDurationShort(delta)}`);
329
+ }
330
+ }
331
+ } else {
332
+ log.info(`Last run: never`);
333
+ if (cfg.enabled) {
334
+ log.info(`Next run: on next tick`);
335
+ }
336
+ }
337
+
338
+ // Local directory line — include snapshot count so users can confirm the
339
+ // pool size matches retention.
340
+ const localDir = getLocalBackupsDir(cfg.localDirectory);
341
+ const localSnapshots = await listSnapshotsInDir(localDir);
342
+ log.info(
343
+ `Local directory: ${localDir} (${localSnapshots.length} snapshots)`,
344
+ );
345
+
346
+ // Offsite destinations — resolve the iCloud default, probe reachability
347
+ // for each, and report snapshot counts.
348
+ log.info(`Offsite:`);
349
+ if (!cfg.offsite.enabled) {
350
+ log.info(` (disabled)`);
351
+ return;
352
+ }
353
+ const destinations = resolveOffsiteDestinations(cfg.offsite.destinations);
354
+ if (destinations.length === 0) {
355
+ log.info(` (no destinations configured)`);
356
+ return;
357
+ }
358
+ for (const dest of destinations) {
359
+ const reachable = await isDestinationReachable(dest.path);
360
+ const tag = reachable ? "[OK]" : "[unreachable]";
361
+ const enc = dest.encrypt ? "encrypted" : "plaintext";
362
+ const snapshots = reachable
363
+ ? await listSnapshotsInDir(dest.path)
364
+ : [];
365
+ const suffix = reachable
366
+ ? ""
367
+ : " -- parent directory not reachable";
368
+ log.info(
369
+ ` ${tag} ${dest.path} (${enc}, ${snapshots.length} snapshots)${suffix}`,
370
+ );
371
+ }
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // list
376
+ // ---------------------------------------------------------------------------
377
+
378
+ /** Print a snapshot table for a group of entries. */
379
+ function printSnapshotGroup(
380
+ heading: string,
381
+ entries: SnapshotEntry[],
382
+ ): void {
383
+ log.info(heading);
384
+ if (entries.length === 0) {
385
+ log.info(" (none)");
386
+ return;
387
+ }
388
+ const tsW = 19;
389
+ const sizeW = 10;
390
+ const encW = 9;
391
+ log.info(
392
+ " " +
393
+ "Timestamp".padEnd(tsW) +
394
+ " " +
395
+ "Size".padEnd(sizeW) +
396
+ " " +
397
+ "Encrypted".padEnd(encW) +
398
+ " " +
399
+ "Filename",
400
+ );
401
+ for (const e of entries) {
402
+ log.info(
403
+ " " +
404
+ formatDate(e.createdAt).padEnd(tsW) +
405
+ " " +
406
+ formatBytes(e.sizeBytes).padEnd(sizeW) +
407
+ " " +
408
+ (e.encrypted ? "yes" : "no").padEnd(encW) +
409
+ " " +
410
+ e.filename,
411
+ );
412
+ }
413
+ }
414
+
415
+ export async function handleList(): Promise<void> {
416
+ const cfg = getConfig().backup;
417
+ const localDir = getLocalBackupsDir(cfg.localDirectory);
418
+ const localSnapshots = await listSnapshotsInDir(localDir);
419
+ printSnapshotGroup(`Local: ${localDir}`, localSnapshots);
420
+
421
+ if (!cfg.offsite.enabled) return;
422
+ const destinations = resolveOffsiteDestinations(cfg.offsite.destinations);
423
+ for (const dest of destinations) {
424
+ const entries = await listSnapshotsInDir(dest.path);
425
+ const tag = dest.encrypt ? "encrypted" : "plaintext";
426
+ log.info("");
427
+ printSnapshotGroup(`Offsite: ${dest.path} (${tag})`, entries);
428
+ }
429
+ }
430
+
431
+ // ---------------------------------------------------------------------------
432
+ // create
433
+ // ---------------------------------------------------------------------------
434
+
435
+ export async function handleCreate(): Promise<void> {
436
+ const cfg = getConfig().backup;
437
+ try {
438
+ const result = await createSnapshotNow(cfg, new Date());
439
+ log.info(`Created snapshot: ${result.local.path}`);
440
+ log.info(` size: ${formatBytes(result.local.sizeBytes)}`);
441
+ log.info(` duration: ${result.durationMs}ms`);
442
+ if (result.offsite.length === 0) {
443
+ log.info(` offsite: (none)`);
444
+ } else {
445
+ log.info(` offsite:`);
446
+ for (const r of result.offsite) {
447
+ if (r.entry) {
448
+ log.info(
449
+ ` ok ${r.destination.path} -> ${r.entry.filename}`,
450
+ );
451
+ } else if (r.skipped) {
452
+ log.info(
453
+ ` skipped ${r.destination.path} (${r.skipped})`,
454
+ );
455
+ } else {
456
+ log.info(
457
+ ` error ${r.destination.path} (${r.error ?? "unknown"})`,
458
+ );
459
+ }
460
+ }
461
+ }
462
+ } catch (err) {
463
+ const message = err instanceof Error ? err.message : String(err);
464
+ if (message.toLowerCase().includes("snapshot in progress")) {
465
+ log.error(
466
+ "Another snapshot is already running. Wait for it to finish, then retry.",
467
+ );
468
+ } else {
469
+ log.error(`Snapshot failed: ${message}`);
470
+ }
471
+ process.exitCode = 1;
472
+ }
473
+ }
474
+
475
+ // ---------------------------------------------------------------------------
476
+ // restore / verify helpers
477
+ // ---------------------------------------------------------------------------
478
+
479
+ /** True when a snapshot path ends in `.vbundle.enc`. */
480
+ function isEncryptedPath(path: string): boolean {
481
+ return path.endsWith(".vbundle.enc");
482
+ }
483
+
484
+ /**
485
+ * Load the backup key when the snapshot is encrypted. Throws a user-facing
486
+ * error when the key file is missing or corrupt.
487
+ */
488
+ async function loadKeyForEncryptedSnapshot(
489
+ snapshotPath: string,
490
+ ): Promise<Buffer | undefined> {
491
+ if (!isEncryptedPath(snapshotPath)) return undefined;
492
+ const keyPath = getBackupKeyPath();
493
+ const key = await readBackupKey(keyPath);
494
+ if (!key) {
495
+ throw new Error(
496
+ `Encrypted snapshot requires backup key at ${keyPath}, but none was found. ` +
497
+ `The key is generated the first time automatic backup runs against an encrypted ` +
498
+ `destination.`,
499
+ );
500
+ }
501
+ return key;
502
+ }
503
+
504
+ /**
505
+ * Prompt for y/N confirmation. Defaults to `false` on empty input, EOF, or
506
+ * anything other than `y` / `yes` (case-insensitive).
507
+ */
508
+ async function promptConfirm(question: string): Promise<boolean> {
509
+ const readline = await import("node:readline");
510
+ const rl = readline.createInterface({
511
+ input: process.stdin,
512
+ output: process.stdout,
513
+ });
514
+ const answer = await new Promise<string>((resolve) => {
515
+ rl.question(question, resolve);
516
+ });
517
+ rl.close();
518
+ const normalized = answer.trim().toLowerCase();
519
+ return normalized === "y" || normalized === "yes";
520
+ }
521
+
522
+ // ---------------------------------------------------------------------------
523
+ // restore
524
+ // ---------------------------------------------------------------------------
525
+
526
+ export interface RestoreOptions {
527
+ path?: string;
528
+ latest?: boolean;
529
+ yes?: boolean;
530
+ force?: boolean;
531
+ }
532
+
533
+ export async function handleRestore(opts: RestoreOptions): Promise<void> {
534
+ if (!opts.path && !opts.latest) {
535
+ log.error(
536
+ "Must specify --path <snapshot> or --latest. " +
537
+ "Run 'vellum backup list' to see available snapshots.",
538
+ );
539
+ process.exitCode = 1;
540
+ return;
541
+ }
542
+ if (opts.path && opts.latest) {
543
+ log.error(
544
+ "Cannot combine --path and --latest. Drop one.",
545
+ );
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+
550
+ // Safety gate: a restore while the assistant is running is dangerous.
551
+ // The assistant holds an open SQLite handle (referencing the old inode on
552
+ // Unix), a cached config, and cached trust rules. Overwriting the files
553
+ // under a running process corrupts state. Refuse unless `--force` says the
554
+ // caller knows what they're doing.
555
+ if (!opts.force && isDaemonRunning()) {
556
+ log.error(
557
+ "Assistant is running — stop it first with 'vellum sleep' before restoring " +
558
+ "(safe restore requires an idle assistant). Pass --force to override.",
559
+ );
560
+ process.exitCode = 1;
561
+ return;
562
+ }
563
+
564
+ let snapshotPath: string;
565
+ if (opts.path) {
566
+ snapshotPath = opts.path;
567
+ } else {
568
+ // `--latest` is explicitly scoped to local snapshots — offsite files may
569
+ // not exist after a machine swap (per the plan), so we keep the selection
570
+ // rule predictable.
571
+ const cfg = getConfig().backup;
572
+ const localDir = getLocalBackupsDir(cfg.localDirectory);
573
+ const entries = await listSnapshotsInDir(localDir);
574
+ if (entries.length === 0) {
575
+ log.error(
576
+ `No local snapshots found in ${localDir}. ` +
577
+ `Run 'vellum backup create' to make one, or pass --path with an explicit file.`,
578
+ );
579
+ process.exitCode = 1;
580
+ return;
581
+ }
582
+ snapshotPath = entries[0]!.path;
583
+ }
584
+
585
+ if (!opts.yes) {
586
+ const confirmed = await promptConfirm(
587
+ `Restore from ${snapshotPath}? This will overwrite workspace files. (y/N) `,
588
+ );
589
+ if (!confirmed) {
590
+ log.info("Restore cancelled");
591
+ return;
592
+ }
593
+ }
594
+
595
+ let key: Buffer | undefined;
596
+ try {
597
+ key = await loadKeyForEncryptedSnapshot(snapshotPath);
598
+ } catch (err) {
599
+ log.error(err instanceof Error ? err.message : String(err));
600
+ process.exitCode = 1;
601
+ return;
602
+ }
603
+
604
+ try {
605
+ const workspaceDir = getWorkspaceDir();
606
+ const hooksDir = getWorkspaceHooksDir();
607
+ const pathResolver = new DefaultPathResolver(workspaceDir, hooksDir);
608
+
609
+ // Close the SQLite singleton before the bundle is written. If the
610
+ // assistant process was running in-process (tests, `--force`) the
611
+ // singleton may still reference the old file; resetting closes the
612
+ // handle so the restored DB file is picked up cleanly on the next
613
+ // getDb() call.
614
+ resetDb();
615
+
616
+ const result = await restoreFromSnapshot(snapshotPath, {
617
+ key,
618
+ pathResolver,
619
+ workspaceDir,
620
+ });
621
+
622
+ // Invalidate in-process caches so the restored settings.json and
623
+ // trust.json take effect (matches the HTTP handler's recovery sequence
624
+ // and the migration importer).
625
+ invalidateConfigCache();
626
+ clearTrustCache();
627
+
628
+ log.info(`Restored from ${snapshotPath}`);
629
+ log.info(` source: ${result.manifest.source ?? "unknown"}`);
630
+ log.info(` schema_version: ${result.manifest.schema_version}`);
631
+ log.info(` files restored: ${result.restoredFiles}`);
632
+ } catch (err) {
633
+ log.error(
634
+ `Restore failed: ${err instanceof Error ? err.message : String(err)}`,
635
+ );
636
+ process.exitCode = 1;
637
+ }
638
+ }
639
+
640
+ // ---------------------------------------------------------------------------
641
+ // verify
642
+ // ---------------------------------------------------------------------------
643
+
644
+ export async function handleVerify(path: string): Promise<void> {
645
+ let key: Buffer | undefined;
646
+ try {
647
+ key = await loadKeyForEncryptedSnapshot(path);
648
+ } catch (err) {
649
+ log.error(err instanceof Error ? err.message : String(err));
650
+ process.exitCode = 1;
651
+ return;
652
+ }
653
+
654
+ try {
655
+ const result = await verifySnapshot(path, { key });
656
+ if (result.valid) {
657
+ log.info(`OK: ${path}`);
658
+ if (result.manifest) {
659
+ log.info(` schema_version: ${result.manifest.schema_version}`);
660
+ log.info(` source: ${result.manifest.source ?? "unknown"}`);
661
+ }
662
+ } else {
663
+ log.error(`Invalid: ${path}`);
664
+ if (result.error) log.error(` ${result.error}`);
665
+ process.exitCode = 1;
666
+ }
667
+ } catch (err) {
668
+ log.error(
669
+ `Verify failed: ${err instanceof Error ? err.message : String(err)}`,
670
+ );
671
+ process.exitCode = 1;
672
+ }
673
+ }
674
+
675
+ // ---------------------------------------------------------------------------
676
+ // Command wiring
677
+ // ---------------------------------------------------------------------------
678
+
679
+ export function registerBackupCommand(program: Command): void {
680
+ const backup = program
681
+ .command("backup")
682
+ .description(
683
+ "Manage automated backups, on-demand snapshots, restore, and verify",
684
+ );
685
+
686
+ backup.addHelpText(
687
+ "after",
688
+ `
689
+ Backups capture a snapshot of the assistant workspace (config, conversations,
690
+ trust rules, hooks, the SQLite database) as a .vbundle file. Credentials are
691
+ NOT included — they live in the OS keychain / CES and users re-authenticate
692
+ integrations after a restore. The automated worker runs on a configurable
693
+ interval and writes to a local pool under ~/.vellum/backups/local/, optionally
694
+ mirroring each snapshot to one or more offsite destinations (iCloud Drive by
695
+ default).
696
+
697
+ Offsite destinations can be per-destination encrypted (AES-256-GCM) or
698
+ plaintext — plaintext only makes sense when the user owns physical access to
699
+ the medium (e.g. an external SSD).
700
+
701
+ Examples:
702
+ $ vellum backup enable --interval 6 --retention 7
703
+ $ vellum backup destinations add /Volumes/BackupSSD/vellum --plaintext
704
+ $ vellum backup status
705
+ $ vellum backup list
706
+ $ vellum backup create
707
+ $ vellum backup restore --latest --yes
708
+ $ vellum backup verify ~/.vellum/backups/local/backup-20260411-093000.vbundle`,
709
+ );
710
+
711
+ backup
712
+ .command("enable")
713
+ .description("Enable automated backups")
714
+ .option(
715
+ "--interval <hours>",
716
+ "Hours between automated backups (1-168). Defaults to 6.",
717
+ )
718
+ .option(
719
+ "--retention <n>",
720
+ "Snapshots to retain per destination (1-100). Defaults to 7.",
721
+ )
722
+ .option(
723
+ "--no-offsite",
724
+ "Disable offsite backup (local only). Does not touch the destinations list.",
725
+ )
726
+ .addHelpText(
727
+ "after",
728
+ `
729
+ Sets backup.enabled = true in config.json. Optionally overrides intervalHours,
730
+ retention, and the offsite.enabled flag. Does NOT modify
731
+ backup.offsite.destinations — use 'vellum backup destinations add/remove' to
732
+ manage those.
733
+
734
+ Examples:
735
+ $ vellum backup enable
736
+ $ vellum backup enable --interval 12 --retention 14
737
+ $ vellum backup enable --no-offsite`,
738
+ )
739
+ .action((opts: EnableOptions) => {
740
+ handleEnable(opts);
741
+ });
742
+
743
+ backup
744
+ .command("disable")
745
+ .description("Disable automated backups")
746
+ .addHelpText(
747
+ "after",
748
+ `
749
+ Sets backup.enabled = false in config.json. Existing snapshots are untouched;
750
+ only the automated worker stops creating new ones.
751
+
752
+ Examples:
753
+ $ vellum backup disable`,
754
+ )
755
+ .action(() => {
756
+ handleDisable();
757
+ });
758
+
759
+ // ---------------------------------------------------------------------------
760
+ // destinations — subgroup
761
+ // ---------------------------------------------------------------------------
762
+
763
+ const destinations = backup
764
+ .command("destinations")
765
+ .description("Manage offsite backup destinations");
766
+
767
+ destinations.addHelpText(
768
+ "after",
769
+ `
770
+ Offsite destinations are absolute paths the backup worker writes a copy of
771
+ each snapshot to after the local write succeeds. The default destination is
772
+ the iCloud Drive VellumAssistant folder, and it is used implicitly until an
773
+ explicit destinations array is configured. The first 'destinations add' or
774
+ 'destinations remove' materializes the iCloud default before applying the
775
+ change, so the default is never lost on an accidental "clear all".
776
+
777
+ Each destination has an 'encrypt' flag. When true (the default), snapshots
778
+ are written as .vbundle.enc (AES-256-GCM). When false, snapshots are copied
779
+ as plaintext .vbundle — only use this for media you control physically.
780
+
781
+ Examples:
782
+ $ vellum backup destinations list
783
+ $ vellum backup destinations add /Volumes/BackupSSD/vellum --plaintext
784
+ $ vellum backup destinations remove /Volumes/BackupSSD/vellum
785
+ $ vellum backup destinations set-encrypt /Volumes/BackupSSD/vellum false`,
786
+ );
787
+
788
+ destinations
789
+ .command("list")
790
+ .description("List configured offsite destinations")
791
+ .addHelpText(
792
+ "after",
793
+ `
794
+ Resolves the current destinations array (materializing the iCloud default if
795
+ no explicit array is configured) and prints a table with the path and
796
+ encryption flag per row.
797
+
798
+ Examples:
799
+ $ vellum backup destinations list`,
800
+ )
801
+ .action(async () => {
802
+ await handleDestinationsList();
803
+ });
804
+
805
+ destinations
806
+ .command("add <path>")
807
+ .description("Add an offsite backup destination")
808
+ .option(
809
+ "--plaintext",
810
+ "Write snapshots as plaintext .vbundle (default is AES-256-GCM encrypted .vbundle.enc)",
811
+ )
812
+ .addHelpText(
813
+ "after",
814
+ `
815
+ Arguments:
816
+ path Absolute path to the destination directory. Must be on a mount the
817
+ caller controls; the backup worker writes files inside this
818
+ directory, not the directory itself.
819
+
820
+ If backup.offsite.destinations is currently null (the implicit iCloud default),
821
+ the iCloud default is materialized first so the new entry appends to a
822
+ 2-element array rather than replacing the default.
823
+
824
+ Examples:
825
+ $ vellum backup destinations add /Volumes/BackupSSD/vellum --plaintext
826
+ $ vellum backup destinations add ~/Dropbox/VellumAssistant/backups`,
827
+ )
828
+ .action((path: string, opts: DestinationAddOptions) => {
829
+ handleDestinationsAdd(path, opts);
830
+ });
831
+
832
+ destinations
833
+ .command("remove <path>")
834
+ .description("Remove an offsite backup destination by path")
835
+ .addHelpText(
836
+ "after",
837
+ `
838
+ Arguments:
839
+ path Exact path match of the destination to remove. Run
840
+ 'vellum backup destinations list' to see configured paths.
841
+
842
+ Errors if no destination with the given path exists.
843
+
844
+ Examples:
845
+ $ vellum backup destinations remove /Volumes/BackupSSD/vellum`,
846
+ )
847
+ .action((path: string) => {
848
+ handleDestinationsRemove(path);
849
+ });
850
+
851
+ destinations
852
+ .command("set-encrypt <path> <value>")
853
+ .description("Toggle encryption for an existing destination")
854
+ .addHelpText(
855
+ "after",
856
+ `
857
+ Arguments:
858
+ path Exact path match of an existing destination. Run
859
+ 'vellum backup destinations list' to see configured paths.
860
+ value "true" to encrypt, "false" for plaintext writes.
861
+
862
+ Errors if no destination with the given path exists. Existing snapshot files
863
+ are not modified; only future writes honour the new setting.
864
+
865
+ Examples:
866
+ $ vellum backup destinations set-encrypt /Volumes/BackupSSD/vellum false
867
+ $ vellum backup destinations set-encrypt /Volumes/BackupSSD/vellum true`,
868
+ )
869
+ .action((path: string, value: string) => {
870
+ handleDestinationsSetEncrypt(path, value);
871
+ });
872
+
873
+ // ---------------------------------------------------------------------------
874
+ // status / list / create / restore / verify
875
+ // ---------------------------------------------------------------------------
876
+
877
+ backup
878
+ .command("status")
879
+ .description("Show backup status and next-run timing")
880
+ .addHelpText(
881
+ "after",
882
+ `
883
+ Reports enabled/disabled state, interval and retention, last-run and next-run
884
+ timing (from the backup:last_run_at memory checkpoint), and a per-destination
885
+ reachability probe. Unreachable destinations (parent directory missing, e.g.
886
+ iCloud Drive not enabled or external volume unplugged) are flagged
887
+ [unreachable] and skipped by the worker.
888
+
889
+ Examples:
890
+ $ vellum backup status`,
891
+ )
892
+ .action(async () => {
893
+ await handleStatus();
894
+ });
895
+
896
+ backup
897
+ .command("list")
898
+ .description("List all backup snapshots, grouped by destination")
899
+ .addHelpText(
900
+ "after",
901
+ `
902
+ Prints a per-destination table of snapshots with timestamp, size, and
903
+ encryption flag. Local destination is listed first, followed by each offsite
904
+ destination. Unreachable destinations are listed with an empty snapshot set.
905
+
906
+ Examples:
907
+ $ vellum backup list`,
908
+ )
909
+ .action(async () => {
910
+ await handleList();
911
+ });
912
+
913
+ backup
914
+ .command("create")
915
+ .description("Create a backup snapshot immediately (ignores interval)")
916
+ .addHelpText(
917
+ "after",
918
+ `
919
+ Triggers an on-demand snapshot. Bypasses the interval gate so it will run even
920
+ if the automated worker just ran, but still honours the concurrency mutex --
921
+ a second concurrent caller errors with "snapshot in progress". Does NOT update
922
+ the last-run checkpoint (manual snapshots should not reset the cadence).
923
+
924
+ Examples:
925
+ $ vellum backup create`,
926
+ )
927
+ .action(async () => {
928
+ await handleCreate();
929
+ });
930
+
931
+ backup
932
+ .command("restore")
933
+ .description("Restore a backup snapshot into the workspace")
934
+ .option(
935
+ "--path <path>",
936
+ "Absolute path to the .vbundle or .vbundle.enc file to restore",
937
+ )
938
+ .option(
939
+ "--latest",
940
+ "Restore the newest local snapshot (offsite files are not considered)",
941
+ )
942
+ .option("--yes", "Skip the confirmation prompt")
943
+ .option(
944
+ "--force",
945
+ "Restore even when the assistant is running (unsafe — only use if you know what you're doing)",
946
+ )
947
+ .addHelpText(
948
+ "after",
949
+ `
950
+ Restores a snapshot by writing its contents back into the workspace.
951
+ Encryption is auto-detected from the file extension; encrypted snapshots
952
+ (.vbundle.enc) require the backup key at ~/.vellum/protected/backup.key.
953
+
954
+ Prompts for confirmation unless --yes is passed.
955
+
956
+ --latest selects the newest local snapshot only. Offsite files may not exist
957
+ on a new machine after a workspace migration, so --latest refuses to dig into
958
+ them on purpose.
959
+
960
+ Safety: refuses to run while the assistant is running, because the live
961
+ SQLite handle and cached config/trust rules can corrupt the restored state.
962
+ Stop the assistant first with 'vellum sleep'. Pass --force to override (only
963
+ use this if you understand the risk).
964
+
965
+ Examples:
966
+ $ vellum backup restore --latest --yes
967
+ $ vellum backup restore --path ~/.vellum/backups/local/backup-20260411-093000.vbundle`,
968
+ )
969
+ .action(async (opts: RestoreOptions) => {
970
+ await handleRestore(opts);
971
+ });
972
+
973
+ backup
974
+ .command("verify <path>")
975
+ .description("Verify a backup snapshot without restoring it")
976
+ .addHelpText(
977
+ "after",
978
+ `
979
+ Arguments:
980
+ path Absolute path to a .vbundle or .vbundle.enc snapshot file.
981
+
982
+ Runs the same validation the importer would run but never touches the
983
+ workspace. Encryption is auto-detected from the file extension; encrypted
984
+ snapshots require the backup key at ~/.vellum/protected/backup.key.
985
+
986
+ Examples:
987
+ $ vellum backup verify ~/.vellum/backups/local/backup-20260411-093000.vbundle
988
+ $ vellum backup verify /Volumes/BackupSSD/vellum/backup-20260411-093000.vbundle.enc`,
989
+ )
990
+ .action(async (path: string) => {
991
+ await handleVerify(path);
992
+ });
993
+ }