vellum 0.2.1 → 0.2.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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Extract a JPEG thumbnail from a video file using ffmpeg.
3
+ *
4
+ * Writes the video to a temp file, runs ffmpeg to extract the first frame
5
+ * as a JPEG, and returns the result as a base64 string.
6
+ */
7
+
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { writeFile, readFile, unlink } from 'node:fs/promises';
11
+ import { randomUUID } from 'node:crypto';
12
+ import { getLogger } from '../util/logger.js';
13
+
14
+ const log = getLogger('video-thumbnail');
15
+
16
+ /**
17
+ * Generate a JPEG thumbnail from base64-encoded video data.
18
+ * Returns null if ffmpeg is unavailable or extraction fails.
19
+ */
20
+ export async function generateVideoThumbnail(dataBase64: string): Promise<string | null> {
21
+ const id = randomUUID();
22
+ const inputPath = join(tmpdir(), `vellum-thumb-in-${id}`);
23
+ const outputPath = join(tmpdir(), `vellum-thumb-out-${id}.jpg`);
24
+
25
+ try {
26
+ const videoBuffer = Buffer.from(dataBase64, 'base64');
27
+ await writeFile(inputPath, videoBuffer);
28
+
29
+ const proc = Bun.spawn([
30
+ 'ffmpeg', '-y',
31
+ '-i', inputPath,
32
+ '-vframes', '1',
33
+ '-vf', 'scale=720:-2',
34
+ '-q:v', '5',
35
+ outputPath,
36
+ ], { stderr: 'pipe' });
37
+
38
+ // Race against a 10s timeout to avoid hanging on slow/stuck ffmpeg
39
+ const exitCode = await Promise.race([
40
+ proc.exited,
41
+ new Promise<never>((_, reject) =>
42
+ setTimeout(() => { proc.kill(); reject(new Error('ffmpeg timed out')); }, 10_000)
43
+ ),
44
+ ]);
45
+
46
+ if (exitCode !== 0) {
47
+ log.warn({ exitCode }, 'ffmpeg thumbnail extraction failed');
48
+ return null;
49
+ }
50
+
51
+ const jpegData = await readFile(outputPath);
52
+ return jpegData.toString('base64');
53
+ } catch (err) {
54
+ log.warn({ error: (err as Error).message }, 'Video thumbnail generation failed');
55
+ return null;
56
+ } finally {
57
+ try { await unlink(inputPath); } catch { /* ignore */ }
58
+ try { await unlink(outputPath); } catch { /* ignore */ }
59
+ }
60
+ }
@@ -24,6 +24,7 @@ import {
24
24
  UPDATE_CART_ITEM_QUERY,
25
25
  } from './queries.js';
26
26
  import { loadCapturedQueries } from './query-extractor.js';
27
+ import { truncate } from '../util/truncate.js';
27
28
 
28
29
  const GRAPHQL_BASE = 'https://www.doordash.com/graphql';
29
30
  const CDP_BASE = 'http://localhost:9222';
@@ -50,6 +51,14 @@ export class SessionExpiredError extends Error {
50
51
  }
51
52
  }
52
53
 
54
+ /** Thrown when DoorDash returns HTTP 403 (rate limited). */
55
+ export class RateLimitError extends Error {
56
+ constructor(reason: string) {
57
+ super(reason);
58
+ this.name = 'RateLimitError';
59
+ }
60
+ }
61
+
53
62
  function requireSession(): DoorDashSession {
54
63
  const session = loadSession();
55
64
  if (!session) {
@@ -152,8 +161,10 @@ async function cdpFetch(wsUrl: string, url: string, body: string): Promise<unkno
152
161
 
153
162
  const parsed = typeof value === 'string' ? JSON.parse(value) : value;
154
163
  if (parsed.__error) {
155
- if (parsed.__status === 403 || parsed.__status === 401) {
164
+ if (parsed.__status === 401) {
156
165
  reject(new SessionExpiredError('DoorDash session has expired.'));
166
+ } else if (parsed.__status === 403) {
167
+ reject(new RateLimitError('DoorDash rate limit hit (HTTP 403).'));
157
168
  } else {
158
169
  reject(new Error(parsed.__message ?? `HTTP ${parsed.__status}: ${parsed.__body ?? ''}`));
159
170
  }
@@ -175,28 +186,54 @@ async function cdpFetch(wsUrl: string, url: string, body: string): Promise<unkno
175
186
  });
176
187
  }
177
188
 
189
+ let lastRequestTime = 0;
190
+
178
191
  async function graphql<T = unknown>(
179
192
  operationName: string,
180
193
  query: string,
181
194
  variables: Record<string, unknown>,
182
195
  _session?: DoorDashSession,
183
196
  ): Promise<T> {
184
- // Still require a session to exist (proves we've logged in)
185
197
  if (!_session) requireSession();
186
198
 
187
199
  const wsUrl = await findDoordashTab();
188
200
  const url = `${GRAPHQL_BASE}/${operationName}?operation=${operationName}`;
189
201
  const body = JSON.stringify({ operationName, variables, query });
190
202
 
191
- const json = (await cdpFetch(wsUrl, url, body)) as GraphQLResponse<T>;
192
- if (json.errors?.length) {
193
- const msgs = json.errors.map(e => e.message || JSON.stringify(e)).join('; ');
194
- throw new Error(`GraphQL errors: ${msgs}`);
195
- }
196
- if (!json.data) {
197
- throw new Error('Empty response from DoorDash API');
203
+ const backoffSchedule = [5000, 10000, 20000];
204
+
205
+ for (let attempt = 0; ; attempt++) {
206
+ // Inter-request delay
207
+ const now = Date.now();
208
+ const elapsed = now - lastRequestTime;
209
+ if (lastRequestTime > 0 && elapsed < 2000) {
210
+ await new Promise(r => setTimeout(r, 2000 - elapsed));
211
+ }
212
+
213
+ try {
214
+ lastRequestTime = Date.now();
215
+ const json = (await cdpFetch(wsUrl, url, body)) as GraphQLResponse<T>;
216
+
217
+ if (json.errors?.length) {
218
+ const msgs = json.errors.map(e => e.message || JSON.stringify(e)).join('; ');
219
+ throw new Error(`Unexpected response from DoorDash API: ${msgs}`);
220
+ }
221
+ if (!json.data) {
222
+ throw new Error('Unexpected response format from DoorDash API');
223
+ }
224
+ return json.data;
225
+ } catch (err) {
226
+ if (err instanceof RateLimitError && attempt < backoffSchedule.length) {
227
+ const delay = backoffSchedule[attempt];
228
+ process.stderr.write(
229
+ `[doordash] Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${backoffSchedule.length})\n`,
230
+ );
231
+ await new Promise(r => setTimeout(r, delay));
232
+ continue;
233
+ }
234
+ throw err;
235
+ }
198
236
  }
199
- return json.data;
200
237
  }
201
238
 
202
239
  // ---------------------------------------------------------------------------
@@ -240,7 +277,7 @@ export async function searchItems(query: string, opts?: { debug?: boolean }): Pr
240
277
  );
241
278
  if (opts?.debug) {
242
279
  process.stderr.write(
243
- `[debug] homePageFacetFeed raw: ${JSON.stringify(data.homePageFacetFeed).substring(0, 3000)}\n`,
280
+ `[debug] homePageFacetFeed raw: ${truncate(JSON.stringify(data.homePageFacetFeed), 3000, '')}\n`,
244
281
  );
245
282
  }
246
283
  return extractSearchResults(data.homePageFacetFeed);
@@ -360,7 +397,7 @@ export async function getStoreMenu(
360
397
  process.stderr.write(
361
398
  `[debug] storepageFeed keys: ${Object.keys(feed).join(', ')}\n` +
362
399
  `[debug] itemLists count: ${rawItemLists.length}, carousels count: ${rawCarousels.length}\n` +
363
- `[debug] menuBook: ${JSON.stringify(menuBook).substring(0, 2000)}\n`,
400
+ `[debug] menuBook: ${truncate(JSON.stringify(menuBook), 2000, '')}\n`,
364
401
  );
365
402
  }
366
403
 
@@ -404,8 +441,8 @@ export async function getRetailStoreMenu(
404
441
  process.stderr.write(
405
442
  `[debug] retailStorePageFeed keys: ${Object.keys(feed).join(', ')}\n` +
406
443
  `[debug] l1Categories count: ${l1Cats.length}, collections count: ${collections.length}\n` +
407
- `[debug] page: ${JSON.stringify(page).substring(0, 500)}\n` +
408
- `[debug] collections sample: ${JSON.stringify(collections.slice(0, 2)).substring(0, 2000)}\n`,
444
+ `[debug] page: ${truncate(JSON.stringify(page), 500, '')}\n` +
445
+ `[debug] collections sample: ${truncate(JSON.stringify(collections.slice(0, 2)), 2000, '')}\n`,
409
446
  );
410
447
  }
411
448
  return extractRetailStoreInfo(data.retailStorePageFeed);
@@ -424,12 +461,31 @@ export interface ItemDetails {
424
461
  id: string;
425
462
  name: string;
426
463
  required: boolean;
464
+ minSelections?: number;
465
+ maxSelections?: number;
427
466
  choices: Array<{
428
467
  id: string;
429
468
  name: string;
430
469
  price?: string;
470
+ unitAmount?: number;
471
+ defaultQuantity?: number;
472
+ nestedOptions?: Array<{
473
+ id: string;
474
+ name: string;
475
+ required: boolean;
476
+ choices: Array<{
477
+ id: string;
478
+ name: string;
479
+ price?: string;
480
+ }>;
481
+ }>;
431
482
  }>;
432
483
  }>;
484
+ specialInstructionsConfig?: {
485
+ maxLength: number;
486
+ placeholderText?: string;
487
+ isEnabled: boolean;
488
+ };
433
489
  }
434
490
 
435
491
  export async function getItemDetails(
@@ -595,7 +651,6 @@ export interface PaymentMethod {
595
651
  id: string;
596
652
  type: string;
597
653
  last4: string;
598
- cardBrand: string;
599
654
  isDefault: boolean;
600
655
  uuid: string;
601
656
  }
@@ -614,7 +669,6 @@ export async function getPaymentMethods(): Promise<PaymentMethod[]> {
614
669
  id: String(p.id ?? ''),
615
670
  type: String(p.type ?? ''),
616
671
  last4: String(p.last4 ?? ''),
617
- cardBrand: String(p.cardBrand ?? ''),
618
672
  isDefault: Boolean(p.isDefault),
619
673
  uuid: String(p.paymentMethodUuid ?? p.uuid ?? ''),
620
674
  }));
@@ -800,11 +854,25 @@ function extractStoreInfo(feed: Record<string, unknown>): StoreInfo {
800
854
  };
801
855
  }
802
856
 
857
+ function extractNestedOptions(extrasList: Array<Record<string, unknown>>): ItemDetails['options'][number]['choices'][number]['nestedOptions'] {
858
+ return extrasList.map(nested => ({
859
+ id: String(nested.id),
860
+ name: String(nested.name ?? ''),
861
+ required: !nested.isOptional,
862
+ choices: ((nested.options ?? []) as Array<Record<string, unknown>>).map(o => ({
863
+ id: String(o.id),
864
+ name: String(o.name ?? ''),
865
+ price: o.displayString as string | undefined,
866
+ })),
867
+ }));
868
+ }
869
+
803
870
  function extractItemDetails(page: Record<string, unknown>): ItemDetails {
804
871
  const header = (page.itemHeader ?? {}) as Record<string, unknown>;
805
872
  const optionLists = (page.optionLists ?? []) as Array<Record<string, unknown>>;
873
+ const itemPreferences = page.itemPreferences as Record<string, unknown> | undefined;
806
874
 
807
- return {
875
+ const result: ItemDetails = {
808
876
  id: String(header.id ?? ''),
809
877
  name: String(header.name ?? ''),
810
878
  description: header.description as string | undefined,
@@ -813,17 +881,43 @@ function extractItemDetails(page: Record<string, unknown>): ItemDetails {
813
881
  currency: header.currency as string | undefined,
814
882
  imageUrl: header.imgUrl as string | undefined,
815
883
  menuId: header.menuId as string | undefined,
816
- options: optionLists.map(ol => ({
817
- id: String(ol.id),
818
- name: String(ol.name ?? ''),
819
- required: !ol.isOptional,
820
- choices: ((ol.options ?? []) as Array<Record<string, unknown>>).map(o => ({
821
- id: String(o.id),
822
- name: String(o.name ?? ''),
823
- price: o.displayString as string | undefined,
824
- })),
825
- })),
884
+ options: optionLists.map(ol => {
885
+ const choices = ((ol.options ?? []) as Array<Record<string, unknown>>).map(o => {
886
+ const choice: ItemDetails['options'][number]['choices'][number] = {
887
+ id: String(o.id),
888
+ name: String(o.name ?? ''),
889
+ price: o.displayString as string | undefined,
890
+ unitAmount: o.unitAmount as number | undefined,
891
+ defaultQuantity: o.defaultQuantity as number | undefined,
892
+ };
893
+ const nestedExtrasList = (o.nestedExtrasList ?? []) as Array<Record<string, unknown>>;
894
+ if (nestedExtrasList.length > 0) {
895
+ choice.nestedOptions = extractNestedOptions(nestedExtrasList);
896
+ }
897
+ return choice;
898
+ });
899
+
900
+ return {
901
+ id: String(ol.id),
902
+ name: String(ol.name ?? ''),
903
+ required: !ol.isOptional,
904
+ minSelections: ol.minNumOptions as number | undefined,
905
+ maxSelections: ol.maxNumOptions as number | undefined,
906
+ choices,
907
+ };
908
+ }),
826
909
  };
910
+
911
+ if (itemPreferences) {
912
+ const specialInstructions = (itemPreferences.specialInstructions ?? {}) as Record<string, unknown>;
913
+ result.specialInstructionsConfig = {
914
+ maxLength: Number(specialInstructions.characterMaxLength ?? 500),
915
+ placeholderText: specialInstructions.placeholderText as string | undefined,
916
+ isEnabled: specialInstructions.isEnabled !== false,
917
+ };
918
+ }
919
+
920
+ return result;
827
921
  }
828
922
 
829
923
  function extractRetailStoreInfo(feed: Record<string, unknown>): StoreInfo {
@@ -1304,9 +1304,8 @@ query paymentMethodQuery {
1304
1304
  id
1305
1305
  type
1306
1306
  last4
1307
- cardBrand
1308
1307
  isDefault
1309
- uuid
1308
+ paymentMethodUuid
1310
1309
  __typename
1311
1310
  }
1312
1311
  }`;
@@ -2,6 +2,8 @@
2
2
  * Conversation export formatters for markdown and JSON.
3
3
  */
4
4
 
5
+ import { truncate } from '../util/truncate.js';
6
+
5
7
  interface ContentBlock {
6
8
  type: string;
7
9
  text?: string;
@@ -44,7 +46,7 @@ function extractText(blocks: ContentBlock[]): string {
44
46
  if (block.is_error) {
45
47
  parts.push(`[Error: ${block.content ?? ''}]`);
46
48
  } else {
47
- parts.push(`[Result: ${(block.content ?? '').slice(0, 500)}]`);
49
+ parts.push(`[Result: ${truncate(block.content ?? '', 500)}]`);
48
50
  }
49
51
  break;
50
52
  }
@@ -7,6 +7,7 @@ import type { FollowUp, FollowUpCreateInput, FollowUpStatus } from './types.js';
7
7
  // ── Helpers ──────────────────────────────────────────────────────────
8
8
 
9
9
  function parseFollowUp(row: typeof followups.$inferSelect): FollowUp {
10
+ const scheduleId = row.reminderCronId;
10
11
  return {
11
12
  id: row.id,
12
13
  channel: row.channel,
@@ -15,7 +16,8 @@ function parseFollowUp(row: typeof followups.$inferSelect): FollowUp {
15
16
  sentAt: row.sentAt,
16
17
  expectedResponseBy: row.expectedResponseBy,
17
18
  status: row.status as FollowUpStatus,
18
- reminderCronId: row.reminderCronId,
19
+ reminderScheduleId: scheduleId,
20
+ reminderCronId: scheduleId,
19
21
  createdAt: row.createdAt,
20
22
  updatedAt: row.updatedAt,
21
23
  };
@@ -36,7 +38,7 @@ export function createFollowUp(input: FollowUpCreateInput): FollowUp {
36
38
  sentAt: input.sentAt ?? now,
37
39
  expectedResponseBy: input.expectedResponseBy ?? null,
38
40
  status: 'pending',
39
- reminderCronId: input.reminderCronId ?? null,
41
+ reminderCronId: input.reminderScheduleId ?? input.reminderCronId ?? null,
40
42
  createdAt: now,
41
43
  updatedAt: now,
42
44
  }).run();
@@ -8,6 +8,9 @@ export interface FollowUp {
8
8
  sentAt: number;
9
9
  expectedResponseBy: number | null;
10
10
  status: FollowUpStatus;
11
+ /** Canonical field — the recurrence schedule ID linked to this follow-up. */
12
+ reminderScheduleId: string | null;
13
+ /** @deprecated Use {@link reminderScheduleId}. Kept for migration compatibility. */
11
14
  reminderCronId: string | null;
12
15
  createdAt: number;
13
16
  updatedAt: number;
@@ -19,5 +22,8 @@ export interface FollowUpCreateInput {
19
22
  contactId?: string | null;
20
23
  sentAt?: number;
21
24
  expectedResponseBy?: number | null;
25
+ /** Canonical field — the recurrence schedule ID to link. */
26
+ reminderScheduleId?: string | null;
27
+ /** @deprecated Use {@link reminderScheduleId}. Kept for migration compatibility. */
22
28
  reminderCronId?: string | null;
23
29
  }
@@ -45,7 +45,7 @@ export function installTemplates(): void {
45
45
  log.info({ hook: entry.name }, 'Installed hook template (disabled by default)');
46
46
  } catch (err) {
47
47
  // Clean up partially-copied directory so the next restart can retry
48
- try { rmSync(targetDir, { recursive: true, force: true }); } catch {}
48
+ try { rmSync(targetDir, { recursive: true, force: true }); } catch (e) { log.debug({ err: e }, 'Cleanup of partial hook template directory failed'); }
49
49
  log.warn({ err, hook: entry.name }, 'Failed to install hook template');
50
50
  }
51
51
  }