@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -1,1081 +0,0 @@
1
- /**
2
- * CLI command group: `vellum doordash`
3
- *
4
- * Order food from DoorDash via the command line.
5
- * All commands output JSON to stdout. Use --json for machine-readable output.
6
- */
7
-
8
- import { statSync } from "node:fs";
9
- import { join } from "node:path";
10
-
11
- import { Command } from "commander";
12
-
13
- import {
14
- ensureChromeWithCdp,
15
- minimizeChromeWindow,
16
- restoreChromeWindow,
17
- } from "../../../tools/browser/chrome-cdp.js";
18
- import {
19
- addToCart,
20
- getDropoffOptions,
21
- getItemDetails,
22
- getPaymentMethods,
23
- getStoreMenu,
24
- listCarts,
25
- placeOrder,
26
- removeFromCart,
27
- retailSearch,
28
- search,
29
- searchItems,
30
- SessionExpiredError,
31
- viewCart,
32
- } from "./lib/client.js";
33
- import { extractQueries, saveQueries } from "./lib/query-extractor.js";
34
- import {
35
- clearSession,
36
- importFromCredentialStore,
37
- loadSession,
38
- } from "./lib/session.js";
39
- import { NetworkRecorder } from "./lib/shared/network-recorder.js";
40
- import {
41
- buildDaemonUrl,
42
- getDataDir,
43
- getHttpPort,
44
- readHttpToken,
45
- } from "./lib/shared/platform.js";
46
- import { loadRecording, saveRecording } from "./lib/shared/recording-store.js";
47
- import type { SessionRecording } from "./lib/shared/recording-types.js";
48
-
49
- // ---------------------------------------------------------------------------
50
- // Helpers
51
- // ---------------------------------------------------------------------------
52
-
53
- function output(data: unknown, json: boolean): void {
54
- process.stdout.write(
55
- json ? JSON.stringify(data) + "\n" : JSON.stringify(data, null, 2) + "\n",
56
- );
57
- }
58
-
59
- function outputError(message: string, code = 1): void {
60
- output({ ok: false, error: message }, true);
61
- process.exitCode = code;
62
- }
63
-
64
- function getJson(cmd: Command): boolean {
65
- let c: Command | null = cmd;
66
- while (c) {
67
- if ((c.opts() as { json?: boolean }).json) return true;
68
- c = c.parent;
69
- }
70
- return false;
71
- }
72
-
73
- const SESSION_EXPIRED_MSG =
74
- "Your DoorDash session has expired. Please sign in to DoorDash in Chrome — " +
75
- "the assistant will use Ride Shotgun to capture your session automatically.";
76
-
77
- async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
78
- try {
79
- const result = await fn();
80
- output({ ok: true, ...(result as Record<string, unknown>) }, getJson(cmd));
81
- } catch (err) {
82
- if (err instanceof SessionExpiredError) {
83
- output(
84
- { ok: false, error: "session_expired", message: SESSION_EXPIRED_MSG },
85
- getJson(cmd),
86
- );
87
- process.exitCode = 1;
88
- return;
89
- }
90
- outputError(err instanceof Error ? err.message : String(err));
91
- }
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // Command registration
96
- // ---------------------------------------------------------------------------
97
-
98
- export function registerDoordashCommand(program: Command): void {
99
- const dd = program
100
- .command("doordash")
101
- .description(
102
- 'Order food from DoorDash. Requires an active session (use "refresh" to authenticate).',
103
- )
104
- .option("--json", "Machine-readable JSON output");
105
-
106
- // =========================================================================
107
- // logout — clear saved session
108
- // =========================================================================
109
- dd.command("logout")
110
- .description("Clear the saved DoorDash session")
111
- .action((_opts: unknown, cmd: Command) => {
112
- clearSession();
113
- output({ ok: true, message: "Session cleared" }, getJson(cmd));
114
- });
115
-
116
- // =========================================================================
117
- // refresh — start Ride Shotgun learn to capture fresh cookies
118
- // =========================================================================
119
- dd.command("refresh")
120
- .description(
121
- "Start a Ride Shotgun learn session to capture fresh DoorDash cookies. " +
122
- "Opens doordash.com in a separate Chrome window — sign in when prompted. " +
123
- "Your existing Chrome and tabs are not affected.",
124
- )
125
- .option("--duration <seconds>", "Recording duration in seconds", "180")
126
- .action(async (opts: { duration: string }, cmd: Command) => {
127
- const json = getJson(cmd);
128
- const duration = parseInt(opts.duration, 10);
129
-
130
- try {
131
- // Restore minimized Chrome window so user can see the login page
132
- try {
133
- await restoreChromeWindow();
134
- } catch {
135
- /* best-effort */
136
- }
137
-
138
- const result = await startLearnSession(duration);
139
- if (result.recordingId) {
140
- const session = await importFromCredentialStore("doordash.com");
141
-
142
- // Also extract and save captured queries for self-healing
143
- let queriesCaptured = 0;
144
- try {
145
- const recording = loadRecording(result.recordingId);
146
- if (recording) {
147
- const queries = extractQueries(recording);
148
- if (queries.length > 0) {
149
- saveQueries(queries);
150
- queriesCaptured = queries.length;
151
- }
152
- }
153
- } catch {
154
- // Non-fatal: query extraction is best-effort
155
- }
156
-
157
- // Best-effort: minimize Chrome window after capturing session
158
- try {
159
- await minimizeChromeWindow();
160
- process.stderr.write("[doordash] Chrome window minimized\n");
161
- } catch {
162
- // Non-fatal: minimizing is best-effort
163
- }
164
-
165
- output(
166
- {
167
- ok: true,
168
- message: "Session refreshed successfully",
169
- cookieCount: session.cookies.length,
170
- recordingId: result.recordingId,
171
- queriesCaptured,
172
- },
173
- json,
174
- );
175
- } else {
176
- output(
177
- {
178
- ok: false,
179
- error: "Recording completed but no recording ID returned",
180
- recordingId: result.recordingId,
181
- },
182
- json,
183
- );
184
- process.exitCode = 1;
185
- }
186
- } catch (err) {
187
- outputError(err instanceof Error ? err.message : String(err));
188
- }
189
- });
190
-
191
- // =========================================================================
192
- // record — standalone CDP network recording
193
- // =========================================================================
194
- dd.command("record")
195
- .description(
196
- "Record DoorDash network traffic via CDP. " +
197
- "Opens Chrome with CDP debugging, captures GraphQL operations, " +
198
- "and saves captured queries for self-healing API support.",
199
- )
200
- .option("--duration <seconds>", "Max recording duration in seconds", "120")
201
- .option(
202
- "--stop-on <operationName>",
203
- "Auto-stop when this GraphQL operation is captured (e.g. addCartItem)",
204
- )
205
- .action(
206
- async (opts: { duration: string; stopOn?: string }, cmd: Command) => {
207
- const json = getJson(cmd);
208
- const duration = parseInt(opts.duration, 10);
209
-
210
- try {
211
- const cdp = await ensureChromeWithCdp({
212
- startUrl: "https://www.doordash.com",
213
- });
214
-
215
- const startTime = Date.now() / 1000;
216
- const recorder = new NetworkRecorder("doordash.com");
217
- await recorder.startDirect(cdp.baseUrl);
218
-
219
- process.stderr.write("Recording DoorDash network traffic...\n");
220
- if (opts.stopOn) {
221
- process.stderr.write(
222
- `Will auto-stop when "${opts.stopOn}" operation is detected.\n`,
223
- );
224
- }
225
- process.stderr.write(
226
- `Timeout: ${duration}s. Press Ctrl+C to stop early.\n`,
227
- );
228
-
229
- const finishRecording = async () => {
230
- process.stderr.write("\nStopping recording...\n");
231
- const cookies = await recorder.extractCookies("doordash.com");
232
- const entries = await recorder.stop();
233
-
234
- const recording: SessionRecording = {
235
- id: crypto.randomUUID(),
236
- startedAt: startTime,
237
- endedAt: Date.now() / 1000,
238
- targetDomain: "doordash.com",
239
- networkEntries: entries,
240
- cookies,
241
- observations: [],
242
- };
243
-
244
- const recordingPath = saveRecording(recording);
245
-
246
- // Extract and save queries
247
- const queries = extractQueries(recording);
248
- let queriesPath: string | undefined;
249
- if (queries.length > 0) {
250
- queriesPath = saveQueries(queries);
251
- }
252
-
253
- process.stderr.write(`\nRecording saved: ${recordingPath}\n`);
254
- process.stderr.write(`Network entries: ${entries.length}\n`);
255
- process.stderr.write(
256
- `GraphQL operations captured: ${queries.length}\n`,
257
- );
258
- if (queries.length > 0) {
259
- process.stderr.write("Operations:\n");
260
- for (const q of queries) {
261
- const varsKeys =
262
- q.exampleVariables && typeof q.exampleVariables === "object"
263
- ? Object.keys(
264
- q.exampleVariables as Record<string, unknown>,
265
- ).join(", ")
266
- : "(none)";
267
- process.stderr.write(
268
- ` - ${q.operationName} [vars: ${varsKeys}]\n`,
269
- );
270
- }
271
- process.stderr.write(`Queries saved: ${queriesPath}\n`);
272
- }
273
-
274
- output(
275
- {
276
- ok: true,
277
- recordingId: recording.id,
278
- recordingPath,
279
- networkEntries: entries.length,
280
- queriesCaptured: queries.length,
281
- operations: queries.map((q) => q.operationName),
282
- queriesPath,
283
- },
284
- json,
285
- );
286
- };
287
-
288
- await new Promise<void>((resolve) => {
289
- let poll: ReturnType<typeof setInterval> | undefined;
290
-
291
- // Timeout
292
- const timer = setTimeout(() => {
293
- if (poll) clearInterval(poll);
294
- process.stderr.write(`\nTimeout reached (${duration}s).\n`);
295
- resolve();
296
- }, duration * 1000);
297
-
298
- // Ctrl+C
299
- process.on("SIGINT", () => {
300
- if (poll) clearInterval(poll);
301
- clearTimeout(timer);
302
- resolve();
303
- });
304
-
305
- // --stop-on: poll entries for the target operation
306
- if (opts.stopOn) {
307
- const target = opts.stopOn;
308
- poll = setInterval(() => {
309
- const entries = recorder.getEntries();
310
- const found = entries.some((e) => {
311
- if (!e.request.postData) return false;
312
- try {
313
- const body = JSON.parse(e.request.postData) as {
314
- operationName?: string;
315
- };
316
- return body.operationName === target;
317
- } catch {
318
- return false;
319
- }
320
- });
321
- if (found) {
322
- clearInterval(poll);
323
- clearTimeout(timer);
324
- process.stderr.write(`\nDetected "${target}" operation.\n`);
325
- // Small delay to let the response come back
326
- setTimeout(() => resolve(), 3000);
327
- }
328
- }, 500);
329
- }
330
- });
331
-
332
- await finishRecording();
333
- } catch (err) {
334
- outputError(err instanceof Error ? err.message : String(err));
335
- }
336
- },
337
- );
338
-
339
- // =========================================================================
340
- // inspect — inspect a recording's GraphQL operations
341
- // =========================================================================
342
- dd.command("inspect")
343
- .description("Inspect GraphQL operations in a recording")
344
- .argument("<recordingId>", "Recording ID or path to recording JSON file")
345
- .option("--op <operationName>", "Filter to a specific operation name")
346
- .option(
347
- "--extract-options",
348
- "Extract item customization options from updateCartItem operations",
349
- )
350
- .action(
351
- async (
352
- recordingIdOrPath: string,
353
- opts: { op?: string; extractOptions?: boolean },
354
- cmd: Command,
355
- ) => {
356
- const json = getJson(cmd);
357
-
358
- try {
359
- let recording: SessionRecording | null = null;
360
-
361
- // Try as path first, then as recording ID
362
- if (
363
- recordingIdOrPath.includes("/") ||
364
- recordingIdOrPath.endsWith(".json")
365
- ) {
366
- try {
367
- const { readFileSync } = await import("node:fs");
368
- recording = JSON.parse(
369
- readFileSync(recordingIdOrPath, "utf-8"),
370
- ) as SessionRecording;
371
- } catch {
372
- // Fall through to try as ID
373
- }
374
- }
375
- if (!recording) {
376
- recording = loadRecording(recordingIdOrPath);
377
- }
378
-
379
- if (!recording) {
380
- outputError(`Recording not found: ${recordingIdOrPath}`);
381
- return;
382
- }
383
-
384
- const queries = extractQueries(recording);
385
-
386
- if (opts.extractOptions) {
387
- const cartOps = queries.filter(
388
- (q) => q.operationName === "updateCartItem",
389
- );
390
- if (cartOps.length === 0) {
391
- outputError(
392
- "No updateCartItem operations found in this recording",
393
- );
394
- return;
395
- }
396
-
397
- const extracted = cartOps.map((q) => {
398
- const vars = (q.exampleVariables ?? {}) as Record<
399
- string,
400
- unknown
401
- >;
402
- const params = (vars.updateCartItemApiParams ?? {}) as Record<
403
- string,
404
- unknown
405
- >;
406
- return {
407
- itemId: params.itemId as string | undefined,
408
- itemName: params.itemName as string | undefined,
409
- nestedOptions: params.nestedOptions as string | undefined,
410
- specialInstructions: params.specialInstructions as
411
- | string
412
- | undefined,
413
- unitPrice: params.unitPrice as number | undefined,
414
- menuId: params.menuId as string | undefined,
415
- storeId: params.storeId as string | undefined,
416
- };
417
- });
418
-
419
- if (json) {
420
- output(
421
- { ok: true, items: extracted, count: extracted.length },
422
- true,
423
- );
424
- } else {
425
- for (const item of extracted) {
426
- process.stderr.write(
427
- `\nItem: ${item.itemName ?? "unknown"} (${
428
- item.itemId ?? "?"
429
- })\n`,
430
- );
431
- process.stderr.write(
432
- ` Store: ${item.storeId ?? "?"}, Menu: ${
433
- item.menuId ?? "?"
434
- }\n`,
435
- );
436
- process.stderr.write(
437
- ` Unit Price: ${item.unitPrice ?? "?"}\n`,
438
- );
439
- if (item.specialInstructions) {
440
- process.stderr.write(
441
- ` Special Instructions: ${item.specialInstructions}\n`,
442
- );
443
- }
444
- process.stderr.write(
445
- ` Options: ${item.nestedOptions ?? "[]"}\n`,
446
- );
447
- }
448
- }
449
- return;
450
- }
451
-
452
- if (opts.op) {
453
- const match = queries.find((q) => q.operationName === opts.op);
454
- if (!match) {
455
- outputError(
456
- `Operation "${opts.op}" not found. Available: ${queries
457
- .map((q) => q.operationName)
458
- .join(", ")}`,
459
- );
460
- return;
461
- }
462
-
463
- if (json) {
464
- output({ ok: true, operation: match }, true);
465
- } else {
466
- process.stderr.write(`Operation: ${match.operationName}\n`);
467
- process.stderr.write(
468
- `Captured at: ${new Date(
469
- match.capturedAt * 1000,
470
- ).toISOString()}\n\n`,
471
- );
472
- process.stderr.write("--- Query ---\n");
473
- process.stderr.write(match.query + "\n\n");
474
- process.stderr.write("--- Variables ---\n");
475
- process.stderr.write(
476
- JSON.stringify(match.exampleVariables, null, 2) + "\n",
477
- );
478
- }
479
- } else {
480
- if (json) {
481
- output(
482
- { ok: true, operations: queries, count: queries.length },
483
- true,
484
- );
485
- } else {
486
- process.stderr.write(`Recording: ${recording.id}\n`);
487
- process.stderr.write(
488
- `Total network entries: ${recording.networkEntries.length}\n`,
489
- );
490
- process.stderr.write(`GraphQL operations: ${queries.length}\n\n`);
491
-
492
- for (const q of queries) {
493
- const varsKeys =
494
- q.exampleVariables && typeof q.exampleVariables === "object"
495
- ? Object.keys(
496
- q.exampleVariables as Record<string, unknown>,
497
- ).join(", ")
498
- : "(none)";
499
- process.stderr.write(` ${q.operationName}\n`);
500
- process.stderr.write(` Variables: ${varsKeys}\n`);
501
- process.stderr.write(
502
- ` Captured: ${new Date(
503
- q.capturedAt * 1000,
504
- ).toISOString()}\n`,
505
- );
506
- }
507
- }
508
- }
509
- } catch (err) {
510
- outputError(err instanceof Error ? err.message : String(err));
511
- }
512
- },
513
- );
514
-
515
- // =========================================================================
516
- // status — check session status
517
- // =========================================================================
518
- dd.command("status")
519
- .description("Check if a DoorDash session is active")
520
- .action((_opts: unknown, cmd: Command) => {
521
- const session = loadSession();
522
- if (session) {
523
- output(
524
- {
525
- ok: true,
526
- loggedIn: true,
527
- cookieCount: session.cookies.length,
528
- importedAt: session.importedAt,
529
- recordingId: session.recordingId,
530
- },
531
- getJson(cmd),
532
- );
533
- } else {
534
- output({ ok: true, loggedIn: false }, getJson(cmd));
535
- }
536
- });
537
-
538
- // =========================================================================
539
- // search — search for restaurants/stores
540
- // =========================================================================
541
- dd.command("search")
542
- .description("Search for restaurants on DoorDash")
543
- .argument("<query>", 'Search query (e.g. "pizza", "thai food")')
544
- .action(async (query: string, _opts: unknown, cmd: Command) => {
545
- await run(cmd, async () => {
546
- const results = await search(query);
547
- return { results, count: results.length };
548
- });
549
- });
550
-
551
- // =========================================================================
552
- // store-search — search for items within a specific retail/convenience store
553
- // =========================================================================
554
- dd.command("store-search")
555
- .description(
556
- "Search for items within a specific store (best for convenience/pharmacy stores)",
557
- )
558
- .argument("<storeId>", "DoorDash store ID")
559
- .argument("<query>", 'Search query (e.g. "tylenol", "advil")')
560
- .option("--limit <n>", "Max results", "30")
561
- .action(
562
- async (
563
- storeId: string,
564
- query: string,
565
- opts: { limit: string },
566
- cmd: Command,
567
- ) => {
568
- await run(cmd, async () => {
569
- const result = await retailSearch(storeId, query, {
570
- limit: parseInt(opts.limit, 10),
571
- });
572
- return result;
573
- });
574
- },
575
- );
576
-
577
- // =========================================================================
578
- // search-items — search for items across all stores (works for convenience/retail)
579
- // =========================================================================
580
- dd.command("search-items")
581
- .description(
582
- "Search for items across all stores (works for convenience/retail stores)",
583
- )
584
- .argument("<query>", 'Search query (e.g. "tylenol", "advil")')
585
- .option("--debug", "Print raw response to stderr")
586
- .action(async (query: string, opts: { debug?: boolean }, cmd: Command) => {
587
- await run(cmd, async () => {
588
- const results = await searchItems(query, { debug: opts.debug });
589
- return { results, count: results.length };
590
- });
591
- });
592
-
593
- // =========================================================================
594
- // menu — get a store's menu
595
- // =========================================================================
596
- dd.command("menu")
597
- .description("Get a restaurant's menu by store ID")
598
- .argument("<storeId>", "DoorDash store ID")
599
- .option("--menu-id <menuId>", "Specific menu ID (optional)")
600
- .option("--debug", "Print raw response structure to stderr")
601
- .action(
602
- async (
603
- storeId: string,
604
- opts: { menuId?: string; debug?: boolean },
605
- cmd: Command,
606
- ) => {
607
- await run(cmd, async () => {
608
- const store = await getStoreMenu(storeId, opts.menuId, {
609
- debug: opts.debug,
610
- });
611
- return { store };
612
- });
613
- },
614
- );
615
-
616
- // =========================================================================
617
- // item — get item details
618
- // =========================================================================
619
- dd.command("item")
620
- .description("Get details for a specific menu item")
621
- .argument("<storeId>", "DoorDash store ID")
622
- .argument("<itemId>", "Menu item ID")
623
- .action(
624
- async (storeId: string, itemId: string, _opts: unknown, cmd: Command) => {
625
- await run(cmd, async () => {
626
- const item = await getItemDetails(storeId, itemId);
627
- return { item };
628
- });
629
- },
630
- );
631
-
632
- // =========================================================================
633
- // cart — cart operations (subcommand group)
634
- // =========================================================================
635
- const cart = dd.command("cart").description("Cart operations");
636
-
637
- // cart add
638
- cart
639
- .command("add")
640
- .description("Add an item to your cart")
641
- .requiredOption("--store-id <storeId>", "Store ID")
642
- .requiredOption("--menu-id <menuId>", "Menu ID")
643
- .requiredOption("--item-id <itemId>", "Item ID")
644
- .requiredOption("--item-name <name>", "Item name")
645
- .requiredOption("--unit-price <cents>", "Unit price in cents")
646
- .option("--quantity <n>", "Quantity", "1")
647
- .option("--cart-id <cartId>", "Existing cart ID (creates new if omitted)")
648
- .option("--special-instructions <text>", "Special instructions")
649
- .option(
650
- "--options <json>",
651
- "Item customization options as JSON array (from item details or recording)",
652
- )
653
- .action(
654
- async (
655
- opts: {
656
- storeId: string;
657
- menuId: string;
658
- itemId: string;
659
- itemName: string;
660
- unitPrice: string;
661
- quantity: string;
662
- cartId?: string;
663
- specialInstructions?: string;
664
- options?: string;
665
- },
666
- cmd: Command,
667
- ) => {
668
- await run(cmd, async () => {
669
- const result = await addToCart({
670
- storeId: opts.storeId,
671
- menuId: opts.menuId,
672
- itemId: opts.itemId,
673
- itemName: opts.itemName,
674
- unitPrice: parseInt(opts.unitPrice, 10),
675
- quantity: parseInt(opts.quantity, 10),
676
- cartId: opts.cartId,
677
- specialInstructions: opts.specialInstructions,
678
- nestedOptions: opts.options,
679
- });
680
- return { cart: result };
681
- });
682
- },
683
- );
684
-
685
- // cart remove
686
- cart
687
- .command("remove")
688
- .description("Remove an item from your cart")
689
- .requiredOption("--cart-id <cartId>", "Cart ID")
690
- .requiredOption("--item-id <itemId>", "Order item ID (from cart view)")
691
- .action(async (opts: { cartId: string; itemId: string }, cmd: Command) => {
692
- await run(cmd, async () => {
693
- const result = await removeFromCart(opts.cartId, opts.itemId);
694
- return { cart: result };
695
- });
696
- });
697
-
698
- // cart view
699
- cart
700
- .command("view")
701
- .description("View cart contents")
702
- .argument("<cartId>", "Cart ID")
703
- .action(async (cartId: string, _opts: unknown, cmd: Command) => {
704
- await run(cmd, async () => {
705
- const result = await viewCart(cartId);
706
- return { cart: result };
707
- });
708
- });
709
-
710
- // cart list
711
- cart
712
- .command("list")
713
- .description("List all active carts")
714
- .option("--store-id <storeId>", "Filter by store ID")
715
- .action(async (opts: { storeId?: string }, cmd: Command) => {
716
- await run(cmd, async () => {
717
- const carts = await listCarts(opts.storeId);
718
- return { carts, count: carts.length };
719
- });
720
- });
721
-
722
- // cart learn — capture customization options via CDP recording
723
- cart
724
- .command("learn")
725
- .description(
726
- "Learn item customization options by recording a browser interaction. " +
727
- "Opens Chrome and watches you customize an item — when you add it to cart, " +
728
- "the nestedOptions and specialInstructions are extracted and output.",
729
- )
730
- .option("--duration <seconds>", "Max recording duration in seconds", "120")
731
- .action(async (opts: { duration: string }, cmd: Command) => {
732
- const json = getJson(cmd);
733
- const duration = parseInt(opts.duration, 10);
734
-
735
- try {
736
- const cdp = await ensureChromeWithCdp({
737
- startUrl: "https://www.doordash.com",
738
- });
739
-
740
- const startTime = Date.now() / 1000;
741
- const recorder = new NetworkRecorder("doordash.com");
742
- await recorder.startDirect(cdp.baseUrl);
743
-
744
- process.stderr.write(
745
- "Recording... Navigate to an item, customize it, and add it to cart.\n",
746
- );
747
- process.stderr.write(
748
- `Will auto-stop when "updateCartItem" is detected. Timeout: ${duration}s.\n`,
749
- );
750
-
751
- await new Promise<void>((resolve) => {
752
- const timer = setTimeout(() => {
753
- if (poll) clearInterval(poll);
754
- process.stderr.write(`\nTimeout reached (${duration}s).\n`);
755
- resolve();
756
- }, duration * 1000);
757
-
758
- process.on("SIGINT", () => {
759
- if (poll) clearInterval(poll);
760
- clearTimeout(timer);
761
- resolve();
762
- });
763
-
764
- const poll = setInterval(() => {
765
- const entries = recorder.getEntries();
766
- const found = entries.some((e) => {
767
- if (!e.request.postData) return false;
768
- try {
769
- const body = JSON.parse(e.request.postData) as {
770
- operationName?: string;
771
- };
772
- return body.operationName === "updateCartItem";
773
- } catch {
774
- return false;
775
- }
776
- });
777
- if (found) {
778
- clearInterval(poll);
779
- clearTimeout(timer);
780
- process.stderr.write('\nDetected "updateCartItem" operation.\n');
781
- setTimeout(() => resolve(), 3000);
782
- }
783
- }, 500);
784
- });
785
-
786
- process.stderr.write("Stopping recording...\n");
787
- const cookies = await recorder.extractCookies("doordash.com");
788
- const entries = await recorder.stop();
789
-
790
- const recording: SessionRecording = {
791
- id: crypto.randomUUID(),
792
- startedAt: startTime,
793
- endedAt: Date.now() / 1000,
794
- targetDomain: "doordash.com",
795
- networkEntries: entries,
796
- cookies,
797
- observations: [],
798
- };
799
-
800
- // Extract updateCartItem operations
801
- const queries = extractQueries(recording);
802
- const cartOps = queries.filter(
803
- (q) => q.operationName === "updateCartItem",
804
- );
805
-
806
- if (cartOps.length === 0) {
807
- outputError(
808
- "No updateCartItem operations captured. Did you add an item to cart?",
809
- );
810
- return;
811
- }
812
-
813
- const extracted = cartOps.map((q) => {
814
- const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
815
- const params = (vars.updateCartItemApiParams ?? {}) as Record<
816
- string,
817
- unknown
818
- >;
819
- return {
820
- itemId: params.itemId as string | undefined,
821
- itemName: params.itemName as string | undefined,
822
- nestedOptions: params.nestedOptions as string | undefined,
823
- specialInstructions: params.specialInstructions as
824
- | string
825
- | undefined,
826
- unitPrice: params.unitPrice as number | undefined,
827
- menuId: params.menuId as string | undefined,
828
- storeId: params.storeId as string | undefined,
829
- };
830
- });
831
-
832
- // Also save the recording for future reference
833
- const recordingPath = saveRecording(recording);
834
-
835
- output(
836
- {
837
- ok: true,
838
- items: extracted,
839
- count: extracted.length,
840
- recordingId: recording.id,
841
- recordingPath,
842
- },
843
- json,
844
- );
845
- } catch (err) {
846
- outputError(err instanceof Error ? err.message : String(err));
847
- }
848
- });
849
-
850
- // =========================================================================
851
- // checkout — get checkout / dropoff options
852
- // =========================================================================
853
- dd.command("checkout")
854
- .description("Get delivery/dropoff options for a cart")
855
- .argument("<cartId>", "Cart ID")
856
- .option("--address-id <addressId>", "Delivery address ID")
857
- .action(
858
- async (cartId: string, opts: { addressId?: string }, cmd: Command) => {
859
- await run(cmd, async () => {
860
- const options = await getDropoffOptions(cartId, opts.addressId);
861
- return { dropoffOptions: options };
862
- });
863
- },
864
- );
865
-
866
- // =========================================================================
867
- // order — order operations (subcommand group)
868
- // =========================================================================
869
- const order = dd.command("order").description("Order operations");
870
-
871
- // order place
872
- order
873
- .command("place")
874
- .description("Place an order from a cart")
875
- .requiredOption("--cart-id <cartId>", "Cart ID")
876
- .requiredOption("--store-id <storeId>", "Store ID")
877
- .requiredOption("--total <cents>", "Order total in cents")
878
- .option("--tip <cents>", "Tip amount in cents", "0")
879
- .option("--delivery-option <type>", "Delivery option type", "STANDARD")
880
- .option(
881
- "--dropoff-option <id>",
882
- "Dropoff option ID (from checkout command)",
883
- )
884
- .option(
885
- "--payment-uuid <uuid>",
886
- "Payment method UUID (uses default if omitted)",
887
- )
888
- .option("--payment-type <type>", "Payment method type", "Card")
889
- .action(
890
- async (
891
- opts: {
892
- cartId: string;
893
- storeId: string;
894
- total: string;
895
- tip: string;
896
- deliveryOption: string;
897
- dropoffOption?: string;
898
- paymentUuid?: string;
899
- paymentType: string;
900
- },
901
- cmd: Command,
902
- ) => {
903
- await run(cmd, async () => {
904
- const result = await placeOrder({
905
- cartId: opts.cartId,
906
- storeId: opts.storeId,
907
- total: parseInt(opts.total, 10),
908
- tipAmount: parseInt(opts.tip, 10),
909
- deliveryOptionType: opts.deliveryOption,
910
- dropoffOptionId: opts.dropoffOption,
911
- paymentMethodUuid: opts.paymentUuid,
912
- paymentMethodType: opts.paymentType,
913
- });
914
- return { order: result };
915
- });
916
- },
917
- );
918
-
919
- // =========================================================================
920
- // payment-methods — list saved payment methods
921
- // =========================================================================
922
- dd.command("payment-methods")
923
- .description("List saved payment methods")
924
- .action(async (_opts: unknown, cmd: Command) => {
925
- await run(cmd, async () => {
926
- const methods = await getPaymentMethods();
927
- return { methods, count: methods.length };
928
- });
929
- });
930
- }
931
-
932
- // ---------------------------------------------------------------------------
933
- // Ride Shotgun learn session helper
934
- // ---------------------------------------------------------------------------
935
-
936
- interface LearnResult {
937
- recordingId?: string;
938
- recordingPath?: string;
939
- }
940
-
941
- async function startLearnSession(
942
- durationSeconds: number,
943
- ): Promise<LearnResult> {
944
- // Step 1: Ensure Chrome is running with CDP
945
- await ensureChromeWithCdp({
946
- startUrl: "https://www.doordash.com/consumer/login/",
947
- });
948
-
949
- // Step 2: Start ride-shotgun learn session via HTTP
950
- const port = getHttpPort();
951
- const baseUrl = buildDaemonUrl(port);
952
- const token = readHttpToken();
953
-
954
- const headers: Record<string, string> = {
955
- "Content-Type": "application/json",
956
- };
957
- if (token) {
958
- headers["Authorization"] = `Bearer ${token}`;
959
- }
960
-
961
- const startResponse = await fetch(
962
- `${baseUrl}/v1/computer-use/ride-shotgun/start`,
963
- {
964
- method: "POST",
965
- headers,
966
- body: JSON.stringify({
967
- durationSeconds,
968
- intervalSeconds: 5,
969
- mode: "learn",
970
- targetDomain: "doordash.com",
971
- }),
972
- },
973
- );
974
-
975
- if (!startResponse.ok) {
976
- const errorBody = await startResponse.text();
977
- throw new Error(
978
- `Failed to start ride-shotgun session (HTTP ${startResponse.status}): ${errorBody}`,
979
- );
980
- }
981
-
982
- const startResult = (await startResponse.json()) as {
983
- watchId: string;
984
- sessionId: string;
985
- };
986
-
987
- if (!startResult.watchId) {
988
- throw new Error("Ride-shotgun start response missing watchId");
989
- }
990
-
991
- // Step 3: Poll session status endpoint for completion or failure, then
992
- // look for the correlated recording file using the recordingId from the session.
993
- const { watchId } = startResult;
994
- const statusUrl = `${baseUrl}/v1/computer-use/ride-shotgun/status/${watchId}`;
995
- const timeoutMs = (durationSeconds + 30) * 1000;
996
- const pollIntervalMs = 2000;
997
- const startTime = Date.now();
998
-
999
- return new Promise<LearnResult>((resolve, reject) => {
1000
- const pollOnce = async () => {
1001
- if (Date.now() - startTime > timeoutMs) {
1002
- reject(
1003
- new Error(`Learn session timed out after ${durationSeconds + 30}s`),
1004
- );
1005
- return;
1006
- }
1007
-
1008
- // Poll session status to detect failures early
1009
- try {
1010
- const fetchAbort = AbortSignal.timeout(10_000);
1011
- const statusRes = await fetch(statusUrl, {
1012
- headers,
1013
- signal: fetchAbort,
1014
- });
1015
- if (statusRes.ok) {
1016
- const status = (await statusRes.json()) as {
1017
- status: string;
1018
- recordingId?: string;
1019
- savedRecordingPath?: string;
1020
- bootstrapFailureReason?: string;
1021
- };
1022
-
1023
- // Session failed without producing a recording
1024
- if (status.bootstrapFailureReason) {
1025
- reject(
1026
- new Error(
1027
- `Learn session failed: ${status.bootstrapFailureReason}`,
1028
- ),
1029
- );
1030
- return;
1031
- }
1032
-
1033
- // Session completed — check for the correlated recording file
1034
- if (status.status === "completed") {
1035
- if (status.savedRecordingPath && status.recordingId) {
1036
- resolve({
1037
- recordingId: status.recordingId,
1038
- recordingPath: status.savedRecordingPath,
1039
- });
1040
- return;
1041
- }
1042
-
1043
- // Recording path not in status — fall back to filesystem lookup
1044
- // using the recordingId for correlation
1045
- if (status.recordingId) {
1046
- const recordingsDir = join(getDataDir(), "recordings");
1047
- const expectedPath = join(
1048
- recordingsDir,
1049
- `${status.recordingId}.json`,
1050
- );
1051
- try {
1052
- statSync(expectedPath);
1053
- resolve({
1054
- recordingId: status.recordingId,
1055
- recordingPath: expectedPath,
1056
- });
1057
- return;
1058
- } catch {
1059
- // Recording file not yet written — continue polling
1060
- setTimeout(pollOnce, pollIntervalMs);
1061
- return;
1062
- }
1063
- }
1064
-
1065
- // Completed but no recordingId — cannot correlate
1066
- reject(
1067
- new Error("Learn session completed but no recording was saved."),
1068
- );
1069
- return;
1070
- }
1071
- }
1072
- } catch {
1073
- // Status endpoint not reachable — continue polling
1074
- }
1075
-
1076
- setTimeout(pollOnce, pollIntervalMs);
1077
- };
1078
-
1079
- setTimeout(pollOnce, pollIntervalMs);
1080
- });
1081
- }