vellum 0.2.0 → 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 (361) 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 +161 -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__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -139,8 +139,8 @@ export function registerDoordashCommand(program: Command): void {
139
139
  dd.command('refresh')
140
140
  .description(
141
141
  'Start a Ride Shotgun learn session to capture fresh DoorDash cookies. ' +
142
- 'Opens doordash.com in Chrome — sign in when prompted. ' +
143
- 'NOTE: Chrome will restart with debugging enabled; your tabs will be restored.',
142
+ 'Opens doordash.com in a separate Chrome window — sign in when prompted. ' +
143
+ 'Your existing Chrome and tabs are not affected.',
144
144
  )
145
145
  .option('--duration <seconds>', 'Recording duration in seconds', '180')
146
146
  .action(async (opts: { duration: string }, cmd: Command) => {
@@ -148,6 +148,9 @@ export function registerDoordashCommand(program: Command): void {
148
148
  const duration = parseInt(opts.duration, 10);
149
149
 
150
150
  try {
151
+ // Restore minimized Chrome window so user can see the login page
152
+ try { await restoreChromeWindow(); } catch { /* best-effort */ }
153
+
151
154
  const result = await startLearnSession(duration);
152
155
  if (result.recordingPath) {
153
156
  const session = importFromRecording(result.recordingPath);
@@ -167,6 +170,14 @@ export function registerDoordashCommand(program: Command): void {
167
170
  // Non-fatal: query extraction is best-effort
168
171
  }
169
172
 
173
+ // Best-effort: minimize Chrome window after capturing session
174
+ try {
175
+ await minimizeChromeWindow();
176
+ process.stderr.write('[doordash] Chrome window minimized\n');
177
+ } catch {
178
+ // Non-fatal: minimizing is best-effort
179
+ }
180
+
170
181
  output(
171
182
  {
172
183
  ok: true,
@@ -326,7 +337,8 @@ export function registerDoordashCommand(program: Command): void {
326
337
  .description('Inspect GraphQL operations in a recording')
327
338
  .argument('<recordingId>', 'Recording ID or path to recording JSON file')
328
339
  .option('--op <operationName>', 'Filter to a specific operation name')
329
- .action(async (recordingIdOrPath: string, opts: { op?: string }, cmd: Command) => {
340
+ .option('--extract-options', 'Extract item customization options from updateCartItem operations')
341
+ .action(async (recordingIdOrPath: string, opts: { op?: string; extractOptions?: boolean }, cmd: Command) => {
330
342
  const json = getJson(cmd);
331
343
 
332
344
  try {
@@ -352,6 +364,43 @@ export function registerDoordashCommand(program: Command): void {
352
364
 
353
365
  const queries = extractQueries(recording);
354
366
 
367
+ if (opts.extractOptions) {
368
+ const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
369
+ if (cartOps.length === 0) {
370
+ outputError('No updateCartItem operations found in this recording');
371
+ return;
372
+ }
373
+
374
+ const extracted = cartOps.map(q => {
375
+ const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
376
+ const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
377
+ return {
378
+ itemId: params.itemId as string | undefined,
379
+ itemName: params.itemName as string | undefined,
380
+ nestedOptions: params.nestedOptions as string | undefined,
381
+ specialInstructions: params.specialInstructions as string | undefined,
382
+ unitPrice: params.unitPrice as number | undefined,
383
+ menuId: params.menuId as string | undefined,
384
+ storeId: params.storeId as string | undefined,
385
+ };
386
+ });
387
+
388
+ if (json) {
389
+ output({ ok: true, items: extracted, count: extracted.length }, true);
390
+ } else {
391
+ for (const item of extracted) {
392
+ process.stderr.write(`\nItem: ${item.itemName ?? 'unknown'} (${item.itemId ?? '?'})\n`);
393
+ process.stderr.write(` Store: ${item.storeId ?? '?'}, Menu: ${item.menuId ?? '?'}\n`);
394
+ process.stderr.write(` Unit Price: ${item.unitPrice ?? '?'}\n`);
395
+ if (item.specialInstructions) {
396
+ process.stderr.write(` Special Instructions: ${item.specialInstructions}\n`);
397
+ }
398
+ process.stderr.write(` Options: ${item.nestedOptions ?? '[]'}\n`);
399
+ }
400
+ }
401
+ return;
402
+ }
403
+
355
404
  if (opts.op) {
356
405
  const match = queries.find(q => q.operationName === opts.op);
357
406
  if (!match) {
@@ -511,6 +560,7 @@ export function registerDoordashCommand(program: Command): void {
511
560
  .option('--quantity <n>', 'Quantity', '1')
512
561
  .option('--cart-id <cartId>', 'Existing cart ID (creates new if omitted)')
513
562
  .option('--special-instructions <text>', 'Special instructions')
563
+ .option('--options <json>', 'Item customization options as JSON array (from item details or recording)')
514
564
  .action(
515
565
  async (
516
566
  opts: {
@@ -522,6 +572,7 @@ export function registerDoordashCommand(program: Command): void {
522
572
  quantity: string;
523
573
  cartId?: string;
524
574
  specialInstructions?: string;
575
+ options?: string;
525
576
  },
526
577
  cmd: Command,
527
578
  ) => {
@@ -535,6 +586,7 @@ export function registerDoordashCommand(program: Command): void {
535
586
  quantity: parseInt(opts.quantity, 10),
536
587
  cartId: opts.cartId,
537
588
  specialInstructions: opts.specialInstructions,
589
+ nestedOptions: opts.options,
538
590
  });
539
591
  return { cart: result };
540
592
  });
@@ -580,6 +632,115 @@ export function registerDoordashCommand(program: Command): void {
580
632
  });
581
633
  });
582
634
 
635
+ // cart learn — capture customization options via CDP recording
636
+ cart
637
+ .command('learn')
638
+ .description(
639
+ 'Learn item customization options by recording a browser interaction. ' +
640
+ 'Opens Chrome and watches you customize an item — when you add it to cart, ' +
641
+ 'the nestedOptions and specialInstructions are extracted and output.',
642
+ )
643
+ .option('--duration <seconds>', 'Max recording duration in seconds', '120')
644
+ .action(async (opts: { duration: string }, cmd: Command) => {
645
+ const json = getJson(cmd);
646
+ const duration = parseInt(opts.duration, 10);
647
+
648
+ try {
649
+ await ensureChromeWithCDP();
650
+
651
+ const startTime = Date.now() / 1000;
652
+ const recorder = new NetworkRecorder('doordash.com');
653
+ await recorder.startDirect('http://localhost:9222');
654
+
655
+ process.stderr.write('Recording... Navigate to an item, customize it, and add it to cart.\n');
656
+ process.stderr.write(`Will auto-stop when "updateCartItem" is detected. Timeout: ${duration}s.\n`);
657
+
658
+ await new Promise<void>((resolve) => {
659
+ const timer = setTimeout(() => {
660
+ if (poll) clearInterval(poll);
661
+ process.stderr.write(`\nTimeout reached (${duration}s).\n`);
662
+ resolve();
663
+ }, duration * 1000);
664
+
665
+ process.on('SIGINT', () => {
666
+ if (poll) clearInterval(poll);
667
+ clearTimeout(timer);
668
+ resolve();
669
+ });
670
+
671
+ const poll = setInterval(() => {
672
+ const entries = recorder.getEntries();
673
+ const found = entries.some(e => {
674
+ if (!e.request.postData) return false;
675
+ try {
676
+ const body = JSON.parse(e.request.postData) as { operationName?: string };
677
+ return body.operationName === 'updateCartItem';
678
+ } catch { return false; }
679
+ });
680
+ if (found) {
681
+ clearInterval(poll);
682
+ clearTimeout(timer);
683
+ process.stderr.write('\nDetected "updateCartItem" operation.\n');
684
+ setTimeout(() => resolve(), 3000);
685
+ }
686
+ }, 500);
687
+ });
688
+
689
+ process.stderr.write('Stopping recording...\n');
690
+ const cookies = await recorder.extractCookies('doordash.com');
691
+ const entries = await recorder.stop();
692
+
693
+ const recording: SessionRecording = {
694
+ id: crypto.randomUUID(),
695
+ startedAt: startTime,
696
+ endedAt: Date.now() / 1000,
697
+ targetDomain: 'doordash.com',
698
+ networkEntries: entries,
699
+ cookies,
700
+ observations: [],
701
+ };
702
+
703
+ // Extract updateCartItem operations
704
+ const queries = extractQueries(recording);
705
+ const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
706
+
707
+ if (cartOps.length === 0) {
708
+ outputError('No updateCartItem operations captured. Did you add an item to cart?');
709
+ return;
710
+ }
711
+
712
+ const extracted = cartOps.map(q => {
713
+ const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
714
+ const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
715
+ return {
716
+ itemId: params.itemId as string | undefined,
717
+ itemName: params.itemName as string | undefined,
718
+ nestedOptions: params.nestedOptions as string | undefined,
719
+ specialInstructions: params.specialInstructions as string | undefined,
720
+ unitPrice: params.unitPrice as number | undefined,
721
+ menuId: params.menuId as string | undefined,
722
+ storeId: params.storeId as string | undefined,
723
+ };
724
+ });
725
+
726
+ // Also save the recording for future reference
727
+ const recordingPath = saveRecording(recording);
728
+
729
+ output(
730
+ {
731
+ ok: true,
732
+ items: extracted,
733
+ count: extracted.length,
734
+ recordingId: recording.id,
735
+ recordingPath,
736
+ },
737
+ json,
738
+ );
739
+ } catch (err) {
740
+ outputError(err instanceof Error ? err.message : String(err));
741
+ }
742
+ });
743
+
583
744
  // =========================================================================
584
745
  // checkout — get checkout / dropoff options
585
746
  // =========================================================================
@@ -664,7 +825,7 @@ export function registerDoordashCommand(program: Command): void {
664
825
  // Chrome CDP restart helper
665
826
  // ---------------------------------------------------------------------------
666
827
 
667
- import { execSync, spawn as spawnChild } from 'node:child_process';
828
+ import { spawn as spawnChild } from 'node:child_process';
668
829
  import { homedir } from 'node:os';
669
830
  import { join as pathJoin } from 'node:path';
670
831
 
@@ -687,33 +848,15 @@ async function ensureChromeWithCDP(): Promise<void> {
687
848
  // Already running with CDP?
688
849
  if (await isCdpReady()) return;
689
850
 
690
- // Kill existing Chrome gracefully
691
- try {
692
- execSync('osascript -e \'tell application "Google Chrome" to quit\'', {
693
- timeout: 5000,
694
- stdio: 'ignore',
695
- });
696
- } catch {
697
- // Chrome might not be running
698
- }
699
-
700
- // Wait for Chrome to quit
701
- for (let i = 0; i < 30; i++) {
702
- try {
703
- execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
704
- await new Promise(r => setTimeout(r, 200));
705
- } catch {
706
- break; // Not running
707
- }
708
- }
709
-
710
- // Relaunch Chrome with CDP flags
851
+ // Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
852
+ // Using a dedicated --user-data-dir allows coexistence without killing the user's browser.
711
853
  const chromeApp =
712
854
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
713
855
  spawnChild(chromeApp, [
714
856
  `--remote-debugging-port=9222`,
715
857
  `--force-renderer-accessibility`,
716
858
  `--user-data-dir=${CHROME_DATA_DIR}`,
859
+ `https://www.doordash.com/consumer/login/`,
717
860
  ], {
718
861
  detached: true,
719
862
  stdio: 'ignore',
@@ -727,6 +870,94 @@ async function ensureChromeWithCDP(): Promise<void> {
727
870
  throw new Error('Chrome started but CDP endpoint not responding after 15s');
728
871
  }
729
872
 
873
+ async function minimizeChromeWindow(): Promise<void> {
874
+ const res = await fetch(`${CDP_BASE}/json/list`);
875
+ const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
876
+ const pageTarget = targets.find(t => t.type === 'page');
877
+ if (!pageTarget) return;
878
+
879
+ const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
880
+
881
+ await new Promise<void>((resolve, reject) => {
882
+ const timeout = setTimeout(() => {
883
+ ws.close();
884
+ reject(new Error('CDP minimize timed out'));
885
+ }, 5000);
886
+
887
+ ws.addEventListener('open', () => {
888
+ ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
889
+ });
890
+
891
+ ws.addEventListener('message', (event) => {
892
+ const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
893
+ if (msg.id === 1 && msg.result) {
894
+ ws.send(JSON.stringify({
895
+ id: 2,
896
+ method: 'Browser.setWindowBounds',
897
+ params: { windowId: msg.result.windowId, bounds: { windowState: 'minimized' } },
898
+ }));
899
+ } else if (msg.id === 1) {
900
+ clearTimeout(timeout);
901
+ ws.close();
902
+ reject(new Error('Browser.getWindowForTarget failed'));
903
+ } else if (msg.id === 2) {
904
+ clearTimeout(timeout);
905
+ ws.close();
906
+ resolve();
907
+ }
908
+ });
909
+
910
+ ws.addEventListener('error', (err) => {
911
+ clearTimeout(timeout);
912
+ reject(err);
913
+ });
914
+ });
915
+ }
916
+
917
+ async function restoreChromeWindow(): Promise<void> {
918
+ const res = await fetch(`${CDP_BASE}/json/list`);
919
+ const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
920
+ const pageTarget = targets.find(t => t.type === 'page');
921
+ if (!pageTarget) return;
922
+
923
+ const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
924
+
925
+ await new Promise<void>((resolve, reject) => {
926
+ const timeout = setTimeout(() => {
927
+ ws.close();
928
+ reject(new Error('CDP restore timed out'));
929
+ }, 5000);
930
+
931
+ ws.addEventListener('open', () => {
932
+ ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
933
+ });
934
+
935
+ ws.addEventListener('message', (event) => {
936
+ const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
937
+ if (msg.id === 1 && msg.result) {
938
+ ws.send(JSON.stringify({
939
+ id: 2,
940
+ method: 'Browser.setWindowBounds',
941
+ params: { windowId: msg.result.windowId, bounds: { windowState: 'normal' } },
942
+ }));
943
+ } else if (msg.id === 1) {
944
+ clearTimeout(timeout);
945
+ ws.close();
946
+ reject(new Error('Browser.getWindowForTarget failed'));
947
+ } else if (msg.id === 2) {
948
+ clearTimeout(timeout);
949
+ ws.close();
950
+ resolve();
951
+ }
952
+ });
953
+
954
+ ws.addEventListener('error', (err) => {
955
+ clearTimeout(timeout);
956
+ reject(err);
957
+ });
958
+ });
959
+ }
960
+
730
961
  // ---------------------------------------------------------------------------
731
962
  // Ride Shotgun learn session helper
732
963
  // ---------------------------------------------------------------------------
@@ -0,0 +1,82 @@
1
+ import * as net from 'node:net';
2
+ import { getSocketPath, readSessionToken } from '../util/platform.js';
3
+ import {
4
+ serialize,
5
+ createMessageParser,
6
+ type ClientMessage,
7
+ type ServerMessage,
8
+ } from '../daemon/ipc-protocol.js';
9
+ import { IpcError } from '../util/errors.js';
10
+
11
+ export function sendOneMessage(
12
+ msg: ClientMessage,
13
+ ): Promise<ServerMessage> {
14
+ return new Promise((resolve, reject) => {
15
+ const socket = net.createConnection(getSocketPath());
16
+ const parser = createMessageParser();
17
+ let resolved = false;
18
+ let authenticated = false;
19
+
20
+ socket.on('connect', () => {
21
+ // Authenticate first — the daemon requires a valid session token
22
+ // before it will accept any other messages.
23
+ const token = readSessionToken();
24
+ if (!token) {
25
+ resolved = true;
26
+ reject(new IpcError('Session token not found — is the daemon running?'));
27
+ socket.destroy();
28
+ return;
29
+ }
30
+ socket.write(serialize({ type: 'auth', token }));
31
+ });
32
+
33
+ socket.on('data', (data) => {
34
+ const messages = parser.feed(data.toString()) as ServerMessage[];
35
+ for (const m of messages) {
36
+ // Handle auth handshake
37
+ if (!authenticated) {
38
+ if (m.type === 'auth_result') {
39
+ if ((m as { success: boolean }).success) {
40
+ authenticated = true;
41
+ // Now send the actual message
42
+ socket.write(serialize(msg));
43
+ } else {
44
+ resolved = true;
45
+ reject(new IpcError((m as { message?: string }).message ?? 'Authentication failed'));
46
+ socket.destroy();
47
+ }
48
+ }
49
+ continue;
50
+ }
51
+
52
+ // Skip push messages that aren't responses to our request
53
+ if (m.type === 'daemon_status') {
54
+ continue;
55
+ }
56
+ // On auto-auth sockets the server may send a second auth_result
57
+ // in response to the client's auth message after we're already
58
+ // authenticated — ignore it so it doesn't resolve as the response.
59
+ if (m.type === 'auth_result') {
60
+ continue;
61
+ }
62
+ if (m.type === 'session_info' && msg.type !== 'session_create') {
63
+ continue;
64
+ }
65
+ resolved = true;
66
+ socket.end();
67
+ resolve(m);
68
+ return;
69
+ }
70
+ });
71
+
72
+ socket.on('error', (err) => {
73
+ if (!resolved) reject(err);
74
+ });
75
+
76
+ socket.on('close', () => {
77
+ if (!resolved) {
78
+ reject(new IpcError('Socket closed before receiving a response'));
79
+ }
80
+ });
81
+ });
82
+ }
package/src/cli/map.ts ADDED
@@ -0,0 +1,246 @@
1
+ /**
2
+ * CLI command: `vellum map <domain>`
3
+ *
4
+ * Launches Chrome with CDP, starts a Ride Shotgun learn session to auto-navigate
5
+ * the given domain, then analyzes captured network traffic into a deduplicated API map.
6
+ */
7
+
8
+ import * as net from 'node:net';
9
+ import { spawn as spawnChild } from 'node:child_process';
10
+ import { homedir } from 'node:os';
11
+ import { join as pathJoin } from 'node:path';
12
+ import { Command } from 'commander';
13
+ import { getSocketPath, readSessionToken } from '../util/platform.js';
14
+ import {
15
+ serialize,
16
+ createMessageParser,
17
+ } from '../daemon/ipc-protocol.js';
18
+ import { loadRecording } from '../tools/browser/recording-store.js';
19
+ import { analyzeApiMap, saveApiMap, printApiMapTable } from '../tools/browser/api-map.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Helpers
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function output(data: unknown, json: boolean): void {
26
+ process.stdout.write(
27
+ json ? JSON.stringify(data) + '\n' : JSON.stringify(data, null, 2) + '\n',
28
+ );
29
+ }
30
+
31
+ function outputError(message: string, code = 1): void {
32
+ output({ ok: false, error: message }, true);
33
+ process.exitCode = code;
34
+ }
35
+
36
+ function getJson(cmd: Command): boolean {
37
+ let c: Command | null = cmd;
38
+ while (c) {
39
+ if ((c.opts() as { json?: boolean }).json) return true;
40
+ c = c.parent;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Chrome CDP helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const CDP_BASE = 'http://localhost:9222';
50
+ const CHROME_DATA_DIR = pathJoin(
51
+ homedir(),
52
+ 'Library/Application Support/Google/Chrome-CDP',
53
+ );
54
+
55
+ async function isCdpReady(): Promise<boolean> {
56
+ try {
57
+ const res = await fetch(`${CDP_BASE}/json/version`);
58
+ return res.ok;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async function ensureChromeWithCDP(domain: string): Promise<void> {
65
+ // Already running with CDP?
66
+ if (await isCdpReady()) return;
67
+
68
+ // Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
69
+ const chromeApp =
70
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
71
+ spawnChild(chromeApp, [
72
+ `--remote-debugging-port=9222`,
73
+ `--force-renderer-accessibility`,
74
+ `--user-data-dir=${CHROME_DATA_DIR}`,
75
+ `https://${domain}/`,
76
+ ], {
77
+ detached: true,
78
+ stdio: 'ignore',
79
+ }).unref();
80
+
81
+ // Wait for CDP to be ready
82
+ for (let i = 0; i < 30; i++) {
83
+ await new Promise(r => setTimeout(r, 500));
84
+ if (await isCdpReady()) return;
85
+ }
86
+ throw new Error('Chrome started but CDP endpoint not responding after 15s');
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Ride Shotgun learn session helper
91
+ // ---------------------------------------------------------------------------
92
+
93
+ interface LearnResult {
94
+ recordingId?: string;
95
+ recordingPath?: string;
96
+ }
97
+
98
+ async function startLearnSession(domain: string, durationSeconds: number): Promise<LearnResult> {
99
+ await ensureChromeWithCDP(domain);
100
+
101
+ return new Promise((resolve, reject) => {
102
+ const socketPath = getSocketPath();
103
+ const sessionToken = readSessionToken();
104
+ const socket = net.createConnection(socketPath);
105
+ const parser = createMessageParser();
106
+
107
+ socket.on('error', (err) => {
108
+ reject(new Error(`Cannot connect to daemon: ${err.message}. Is the daemon running?`));
109
+ });
110
+
111
+ const timeoutHandle = setTimeout(() => {
112
+ socket.destroy();
113
+ reject(new Error(`Learn session timed out after ${durationSeconds + 30}s`));
114
+ }, (durationSeconds + 30) * 1000);
115
+ timeoutHandle.unref();
116
+
117
+ let authenticated = !sessionToken;
118
+
119
+ const sendStartCommand = () => {
120
+ socket.write(
121
+ serialize({
122
+ type: 'ride_shotgun_start',
123
+ durationSeconds,
124
+ intervalSeconds: 5,
125
+ mode: 'learn',
126
+ targetDomain: domain,
127
+ autoNavigate: true,
128
+ } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
129
+ );
130
+ };
131
+
132
+ socket.on('data', (chunk) => {
133
+ const messages = parser.feed(chunk.toString('utf-8'));
134
+ for (const msg of messages) {
135
+ const m = msg as unknown as Record<string, unknown>;
136
+
137
+ if (!authenticated && m.type === 'auth_result') {
138
+ if ((m as { success: boolean }).success) {
139
+ authenticated = true;
140
+ sendStartCommand();
141
+ } else {
142
+ clearTimeout(timeoutHandle);
143
+ socket.destroy();
144
+ reject(new Error('Daemon authentication failed'));
145
+ }
146
+ continue;
147
+ }
148
+
149
+ if (m.type === 'auth_result') {
150
+ continue;
151
+ }
152
+
153
+ if (m.type === 'ride_shotgun_result') {
154
+ clearTimeout(timeoutHandle);
155
+ socket.destroy();
156
+ resolve({
157
+ recordingId: m.recordingId as string | undefined,
158
+ recordingPath: m.recordingPath as string | undefined,
159
+ });
160
+ }
161
+ }
162
+ });
163
+
164
+ socket.on('connect', () => {
165
+ if (sessionToken) {
166
+ socket.write(
167
+ serialize({
168
+ type: 'auth',
169
+ token: sessionToken,
170
+ } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
171
+ );
172
+ } else {
173
+ sendStartCommand();
174
+ }
175
+ });
176
+ });
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Command registration
181
+ // ---------------------------------------------------------------------------
182
+
183
+ export function registerMapCommand(program: Command): void {
184
+ program
185
+ .command('map')
186
+ .description(
187
+ 'Auto-navigate a domain and produce a deduplicated API map. ' +
188
+ 'Launches Chrome with CDP, starts a Ride Shotgun learn session, ' +
189
+ 'then analyzes captured network traffic.',
190
+ )
191
+ .argument('<domain>', 'Domain to map (e.g., example.com)')
192
+ .option('--duration <seconds>', 'Recording duration in seconds', '120')
193
+ .option('--json', 'Machine-readable JSON output')
194
+ .action(async (domain: string, opts: { duration: string; json?: boolean }, cmd: Command) => {
195
+ const json = getJson(cmd);
196
+ const duration = parseInt(opts.duration, 10);
197
+
198
+ try {
199
+ // 1. Start learn session (launches Chrome + auto-navigates)
200
+ if (!json) {
201
+ console.log(`Starting API map session for ${domain} (${duration}s)...`);
202
+ }
203
+ const result = await startLearnSession(domain, duration);
204
+
205
+ if (!result.recordingId) {
206
+ outputError('Recording completed but no recording ID returned');
207
+ return;
208
+ }
209
+
210
+ // 2. Load the recording
211
+ const recording = loadRecording(result.recordingId);
212
+ if (!recording) {
213
+ outputError(`Failed to load recording ${result.recordingId}`);
214
+ return;
215
+ }
216
+
217
+ // 3. Analyze the API map
218
+ const apiMap = analyzeApiMap(recording.networkEntries, domain);
219
+
220
+ // 4. Save the API map
221
+ const savedPath = saveApiMap(domain, apiMap);
222
+
223
+ // 5. Display results
224
+ if (!json) {
225
+ printApiMapTable(apiMap);
226
+ console.log(`API map saved to: ${savedPath}`);
227
+ }
228
+
229
+ // 6. Output JSON result
230
+ output(
231
+ {
232
+ ok: true,
233
+ domain,
234
+ recordingId: result.recordingId,
235
+ savedPath,
236
+ totalRequests: apiMap.totalRequests,
237
+ endpointCount: apiMap.endpoints.length,
238
+ apiMap,
239
+ },
240
+ json,
241
+ );
242
+ } catch (err) {
243
+ outputError(err instanceof Error ? err.message : String(err));
244
+ }
245
+ });
246
+ }