@vellumai/assistant 0.3.19 → 0.3.21

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 (199) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -1,1057 +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 * as net from 'node:net';
9
-
10
- import { Command } from 'commander';
11
-
12
- import {
13
- createMessageParser,
14
- serialize,
15
- } from '../daemon/ipc-protocol.js';
16
- import {
17
- addToCart,
18
- getDropoffOptions,
19
- getItemDetails,
20
- getPaymentMethods,
21
- getStoreMenu,
22
- listCarts,
23
- placeOrder,
24
- removeFromCart,
25
- retailSearch,
26
- search,
27
- searchItems,
28
- SessionExpiredError,
29
- viewCart,
30
- } from '../doordash/client.js';
31
- import {
32
- extractQueries,
33
- saveQueries,
34
- } from '../doordash/query-extractor.js';
35
- import {
36
- clearSession,
37
- importFromRecording,
38
- loadSession,
39
- } from '../doordash/session.js';
40
- import { NetworkRecorder } from '../tools/browser/network-recorder.js';
41
- import type { SessionRecording } from '../tools/browser/network-recording-types.js';
42
- import {
43
- loadRecording,
44
- saveRecording,
45
- } from '../tools/browser/recording-store.js';
46
- import { getSocketPath, readSessionToken } from '../util/platform.js';
47
-
48
- // ---------------------------------------------------------------------------
49
- // Helpers
50
- // ---------------------------------------------------------------------------
51
-
52
- function output(data: unknown, json: boolean): void {
53
- process.stdout.write(
54
- json ? JSON.stringify(data) + '\n' : JSON.stringify(data, null, 2) + '\n',
55
- );
56
- }
57
-
58
- function outputError(message: string, code = 1): void {
59
- output({ ok: false, error: message }, true);
60
- process.exitCode = code;
61
- }
62
-
63
- function getJson(cmd: Command): boolean {
64
- let c: Command | null = cmd;
65
- while (c) {
66
- if ((c.opts() as { json?: boolean }).json) return true;
67
- c = c.parent;
68
- }
69
- return false;
70
- }
71
-
72
- const SESSION_EXPIRED_MSG =
73
- 'Your DoorDash session has expired. Please sign in to DoorDash in Chrome — ' +
74
- 'the assistant will use Ride Shotgun to capture your session automatically.';
75
-
76
- async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
77
- try {
78
- const result = await fn();
79
- output(
80
- { ok: true, ...(result as Record<string, unknown>) },
81
- getJson(cmd),
82
- );
83
- } catch (err) {
84
- if (err instanceof SessionExpiredError) {
85
- output(
86
- { ok: false, error: 'session_expired', message: SESSION_EXPIRED_MSG },
87
- getJson(cmd),
88
- );
89
- process.exitCode = 1;
90
- return;
91
- }
92
- outputError(err instanceof Error ? err.message : String(err));
93
- }
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Command registration
98
- // ---------------------------------------------------------------------------
99
-
100
- export function registerDoordashCommand(program: Command): void {
101
- const dd = program
102
- .command('doordash')
103
- .description(
104
- 'Order food from DoorDash. Requires a session imported from a Ride Shotgun recording.',
105
- )
106
- .option('--json', 'Machine-readable JSON output');
107
-
108
- // =========================================================================
109
- // login — import session from a recording
110
- // =========================================================================
111
- dd.command('login')
112
- .description('Import a DoorDash session from a Ride Shotgun recording')
113
- .requiredOption(
114
- '--recording <path>',
115
- 'Path to the recording JSON file',
116
- )
117
- .action(async (opts: { recording: string }, cmd: Command) => {
118
- await run(cmd, async () => {
119
- const session = importFromRecording(opts.recording);
120
- return {
121
- message: 'Session imported successfully',
122
- cookieCount: session.cookies.length,
123
- recordingId: session.recordingId,
124
- };
125
- });
126
- });
127
-
128
- // =========================================================================
129
- // logout — clear saved session
130
- // =========================================================================
131
- dd.command('logout')
132
- .description('Clear the saved DoorDash session')
133
- .action((_opts: unknown, cmd: Command) => {
134
- clearSession();
135
- output({ ok: true, message: 'Session cleared' }, getJson(cmd));
136
- });
137
-
138
- // =========================================================================
139
- // refresh — start Ride Shotgun learn to capture fresh cookies
140
- // =========================================================================
141
- dd.command('refresh')
142
- .description(
143
- 'Start a Ride Shotgun learn session to capture fresh DoorDash cookies. ' +
144
- 'Opens doordash.com in a separate Chrome window — sign in when prompted. ' +
145
- 'Your existing Chrome and tabs are not affected.',
146
- )
147
- .option('--duration <seconds>', 'Recording duration in seconds', '180')
148
- .action(async (opts: { duration: string }, cmd: Command) => {
149
- const json = getJson(cmd);
150
- const duration = parseInt(opts.duration, 10);
151
-
152
- try {
153
- // Restore minimized Chrome window so user can see the login page
154
- try { await restoreChromeWindow(); } catch { /* best-effort */ }
155
-
156
- const result = await startLearnSession(duration);
157
- if (result.recordingPath) {
158
- const session = importFromRecording(result.recordingPath);
159
-
160
- // Also extract and save captured queries for self-healing
161
- let queriesCaptured = 0;
162
- try {
163
- const recording = loadRecording(result.recordingId ?? '');
164
- if (recording) {
165
- const queries = extractQueries(recording);
166
- if (queries.length > 0) {
167
- saveQueries(queries);
168
- queriesCaptured = queries.length;
169
- }
170
- }
171
- } catch {
172
- // Non-fatal: query extraction is best-effort
173
- }
174
-
175
- // Best-effort: minimize Chrome window after capturing session
176
- try {
177
- await minimizeChromeWindow();
178
- process.stderr.write('[doordash] Chrome window minimized\n');
179
- } catch {
180
- // Non-fatal: minimizing is best-effort
181
- }
182
-
183
- output(
184
- {
185
- ok: true,
186
- message: 'Session refreshed successfully',
187
- cookieCount: session.cookies.length,
188
- recordingId: result.recordingId,
189
- queriesCaptured,
190
- },
191
- json,
192
- );
193
- } else {
194
- output(
195
- {
196
- ok: false,
197
- error: 'Recording completed but no recording path returned',
198
- recordingId: result.recordingId,
199
- },
200
- json,
201
- );
202
- process.exitCode = 1;
203
- }
204
- } catch (err) {
205
- outputError(err instanceof Error ? err.message : String(err));
206
- }
207
- });
208
-
209
- // =========================================================================
210
- // record — standalone CDP network recording
211
- // =========================================================================
212
- dd.command('record')
213
- .description(
214
- 'Record DoorDash network traffic via CDP. ' +
215
- 'Opens Chrome with CDP debugging, captures GraphQL operations, ' +
216
- 'and saves captured queries for self-healing API support.',
217
- )
218
- .option('--duration <seconds>', 'Max recording duration in seconds', '120')
219
- .option('--stop-on <operationName>', 'Auto-stop when this GraphQL operation is captured (e.g. addCartItem)')
220
- .action(async (opts: { duration: string; stopOn?: string }, cmd: Command) => {
221
- const json = getJson(cmd);
222
- const duration = parseInt(opts.duration, 10);
223
-
224
- try {
225
- await ensureChromeWithCDP();
226
-
227
- const startTime = Date.now() / 1000;
228
- const recorder = new NetworkRecorder('doordash.com');
229
- await recorder.startDirect('http://localhost:9222');
230
-
231
- process.stderr.write('Recording DoorDash network traffic...\n');
232
- if (opts.stopOn) {
233
- process.stderr.write(`Will auto-stop when "${opts.stopOn}" operation is detected.\n`);
234
- }
235
- process.stderr.write(`Timeout: ${duration}s. Press Ctrl+C to stop early.\n`);
236
-
237
- const finishRecording = async () => {
238
- process.stderr.write('\nStopping recording...\n');
239
- const cookies = await recorder.extractCookies('doordash.com');
240
- const entries = await recorder.stop();
241
-
242
- const recording: SessionRecording = {
243
- id: crypto.randomUUID(),
244
- startedAt: startTime,
245
- endedAt: Date.now() / 1000,
246
- targetDomain: 'doordash.com',
247
- networkEntries: entries,
248
- cookies,
249
- observations: [],
250
- };
251
-
252
- const recordingPath = saveRecording(recording);
253
-
254
- // Extract and save queries
255
- const queries = extractQueries(recording);
256
- let queriesPath: string | undefined;
257
- if (queries.length > 0) {
258
- queriesPath = saveQueries(queries);
259
- }
260
-
261
- process.stderr.write(`\nRecording saved: ${recordingPath}\n`);
262
- process.stderr.write(`Network entries: ${entries.length}\n`);
263
- process.stderr.write(`GraphQL operations captured: ${queries.length}\n`);
264
- if (queries.length > 0) {
265
- process.stderr.write('Operations:\n');
266
- for (const q of queries) {
267
- const varsKeys = q.exampleVariables && typeof q.exampleVariables === 'object'
268
- ? Object.keys(q.exampleVariables as Record<string, unknown>).join(', ')
269
- : '(none)';
270
- process.stderr.write(` - ${q.operationName} [vars: ${varsKeys}]\n`);
271
- }
272
- process.stderr.write(`Queries saved: ${queriesPath}\n`);
273
- }
274
-
275
- output(
276
- {
277
- ok: true,
278
- recordingId: recording.id,
279
- recordingPath,
280
- networkEntries: entries.length,
281
- queriesCaptured: queries.length,
282
- operations: queries.map(q => q.operationName),
283
- queriesPath,
284
- },
285
- json,
286
- );
287
- };
288
-
289
- await new Promise<void>((resolve) => {
290
- let poll: ReturnType<typeof setInterval> | undefined;
291
-
292
- // Timeout
293
- const timer = setTimeout(() => {
294
- if (poll) clearInterval(poll);
295
- process.stderr.write(`\nTimeout reached (${duration}s).\n`);
296
- resolve();
297
- }, duration * 1000);
298
-
299
- // Ctrl+C
300
- process.on('SIGINT', () => {
301
- if (poll) clearInterval(poll);
302
- clearTimeout(timer);
303
- resolve();
304
- });
305
-
306
- // --stop-on: poll entries for the target operation
307
- if (opts.stopOn) {
308
- const target = opts.stopOn;
309
- poll = setInterval(() => {
310
- const entries = recorder.getEntries();
311
- const found = entries.some(e => {
312
- if (!e.request.postData) return false;
313
- try {
314
- const body = JSON.parse(e.request.postData) as { operationName?: string };
315
- return body.operationName === target;
316
- } catch { return false; }
317
- });
318
- if (found) {
319
- clearInterval(poll);
320
- clearTimeout(timer);
321
- process.stderr.write(`\nDetected "${target}" operation.\n`);
322
- // Small delay to let the response come back
323
- setTimeout(() => resolve(), 3000);
324
- }
325
- }, 500);
326
- }
327
- });
328
-
329
- await finishRecording();
330
- } catch (err) {
331
- outputError(err instanceof Error ? err.message : String(err));
332
- }
333
- });
334
-
335
- // =========================================================================
336
- // inspect — inspect a recording's GraphQL operations
337
- // =========================================================================
338
- dd.command('inspect')
339
- .description('Inspect GraphQL operations in a recording')
340
- .argument('<recordingId>', 'Recording ID or path to recording JSON file')
341
- .option('--op <operationName>', 'Filter to a specific operation name')
342
- .option('--extract-options', 'Extract item customization options from updateCartItem operations')
343
- .action(async (recordingIdOrPath: string, opts: { op?: string; extractOptions?: boolean }, cmd: Command) => {
344
- const json = getJson(cmd);
345
-
346
- try {
347
- let recording: SessionRecording | null = null;
348
-
349
- // Try as path first, then as recording ID
350
- if (recordingIdOrPath.includes('/') || recordingIdOrPath.endsWith('.json')) {
351
- try {
352
- const { readFileSync } = await import('node:fs');
353
- recording = JSON.parse(readFileSync(recordingIdOrPath, 'utf-8')) as SessionRecording;
354
- } catch {
355
- // Fall through to try as ID
356
- }
357
- }
358
- if (!recording) {
359
- recording = loadRecording(recordingIdOrPath);
360
- }
361
-
362
- if (!recording) {
363
- outputError(`Recording not found: ${recordingIdOrPath}`);
364
- return;
365
- }
366
-
367
- const queries = extractQueries(recording);
368
-
369
- if (opts.extractOptions) {
370
- const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
371
- if (cartOps.length === 0) {
372
- outputError('No updateCartItem operations found in this recording');
373
- return;
374
- }
375
-
376
- const extracted = cartOps.map(q => {
377
- const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
378
- const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
379
- return {
380
- itemId: params.itemId as string | undefined,
381
- itemName: params.itemName as string | undefined,
382
- nestedOptions: params.nestedOptions as string | undefined,
383
- specialInstructions: params.specialInstructions as string | undefined,
384
- unitPrice: params.unitPrice as number | undefined,
385
- menuId: params.menuId as string | undefined,
386
- storeId: params.storeId as string | undefined,
387
- };
388
- });
389
-
390
- if (json) {
391
- output({ ok: true, items: extracted, count: extracted.length }, true);
392
- } else {
393
- for (const item of extracted) {
394
- process.stderr.write(`\nItem: ${item.itemName ?? 'unknown'} (${item.itemId ?? '?'})\n`);
395
- process.stderr.write(` Store: ${item.storeId ?? '?'}, Menu: ${item.menuId ?? '?'}\n`);
396
- process.stderr.write(` Unit Price: ${item.unitPrice ?? '?'}\n`);
397
- if (item.specialInstructions) {
398
- process.stderr.write(` Special Instructions: ${item.specialInstructions}\n`);
399
- }
400
- process.stderr.write(` Options: ${item.nestedOptions ?? '[]'}\n`);
401
- }
402
- }
403
- return;
404
- }
405
-
406
- if (opts.op) {
407
- const match = queries.find(q => q.operationName === opts.op);
408
- if (!match) {
409
- outputError(`Operation "${opts.op}" not found. Available: ${queries.map(q => q.operationName).join(', ')}`);
410
- return;
411
- }
412
-
413
- if (json) {
414
- output({ ok: true, operation: match }, true);
415
- } else {
416
- process.stderr.write(`Operation: ${match.operationName}\n`);
417
- process.stderr.write(`Captured at: ${new Date(match.capturedAt * 1000).toISOString()}\n\n`);
418
- process.stderr.write('--- Query ---\n');
419
- process.stderr.write(match.query + '\n\n');
420
- process.stderr.write('--- Variables ---\n');
421
- process.stderr.write(JSON.stringify(match.exampleVariables, null, 2) + '\n');
422
- }
423
- } else {
424
- if (json) {
425
- output({ ok: true, operations: queries, count: queries.length }, true);
426
- } else {
427
- process.stderr.write(`Recording: ${recording.id}\n`);
428
- process.stderr.write(`Total network entries: ${recording.networkEntries.length}\n`);
429
- process.stderr.write(`GraphQL operations: ${queries.length}\n\n`);
430
-
431
- for (const q of queries) {
432
- const varsKeys = q.exampleVariables && typeof q.exampleVariables === 'object'
433
- ? Object.keys(q.exampleVariables as Record<string, unknown>).join(', ')
434
- : '(none)';
435
- process.stderr.write(` ${q.operationName}\n`);
436
- process.stderr.write(` Variables: ${varsKeys}\n`);
437
- process.stderr.write(` Captured: ${new Date(q.capturedAt * 1000).toISOString()}\n`);
438
- }
439
- }
440
- }
441
- } catch (err) {
442
- outputError(err instanceof Error ? err.message : String(err));
443
- }
444
- });
445
-
446
- // =========================================================================
447
- // status — check session status
448
- // =========================================================================
449
- dd.command('status')
450
- .description('Check if a DoorDash session is active')
451
- .action((_opts: unknown, cmd: Command) => {
452
- const session = loadSession();
453
- if (session) {
454
- output(
455
- {
456
- ok: true,
457
- loggedIn: true,
458
- cookieCount: session.cookies.length,
459
- importedAt: session.importedAt,
460
- recordingId: session.recordingId,
461
- },
462
- getJson(cmd),
463
- );
464
- } else {
465
- output({ ok: true, loggedIn: false }, getJson(cmd));
466
- }
467
- });
468
-
469
- // =========================================================================
470
- // search — search for restaurants/stores
471
- // =========================================================================
472
- dd.command('search')
473
- .description('Search for restaurants on DoorDash')
474
- .argument('<query>', 'Search query (e.g. "pizza", "thai food")')
475
- .action(async (query: string, _opts: unknown, cmd: Command) => {
476
- await run(cmd, async () => {
477
- const results = await search(query);
478
- return { results, count: results.length };
479
- });
480
- });
481
-
482
- // =========================================================================
483
- // store-search — search for items within a specific retail/convenience store
484
- // =========================================================================
485
- dd.command('store-search')
486
- .description('Search for items within a specific store (best for convenience/pharmacy stores)')
487
- .argument('<storeId>', 'DoorDash store ID')
488
- .argument('<query>', 'Search query (e.g. "tylenol", "advil")')
489
- .option('--limit <n>', 'Max results', '30')
490
- .action(async (storeId: string, query: string, opts: { limit: string }, cmd: Command) => {
491
- await run(cmd, async () => {
492
- const result = await retailSearch(storeId, query, { limit: parseInt(opts.limit, 10) });
493
- return result;
494
- });
495
- });
496
-
497
- // =========================================================================
498
- // search-items — search for items across all stores (works for convenience/retail)
499
- // =========================================================================
500
- dd.command('search-items')
501
- .description('Search for items across all stores (works for convenience/retail stores)')
502
- .argument('<query>', 'Search query (e.g. "tylenol", "advil")')
503
- .option('--debug', 'Print raw response to stderr')
504
- .action(async (query: string, opts: { debug?: boolean }, cmd: Command) => {
505
- await run(cmd, async () => {
506
- const results = await searchItems(query, { debug: opts.debug });
507
- return { results, count: results.length };
508
- });
509
- });
510
-
511
- // =========================================================================
512
- // menu — get a store's menu
513
- // =========================================================================
514
- dd.command('menu')
515
- .description("Get a restaurant's menu by store ID")
516
- .argument('<storeId>', 'DoorDash store ID')
517
- .option('--menu-id <menuId>', 'Specific menu ID (optional)')
518
- .option('--debug', 'Print raw response structure to stderr')
519
- .action(
520
- async (
521
- storeId: string,
522
- opts: { menuId?: string; debug?: boolean },
523
- cmd: Command,
524
- ) => {
525
- await run(cmd, async () => {
526
- const store = await getStoreMenu(storeId, opts.menuId, { debug: opts.debug });
527
- return { store };
528
- });
529
- },
530
- );
531
-
532
- // =========================================================================
533
- // item — get item details
534
- // =========================================================================
535
- dd.command('item')
536
- .description('Get details for a specific menu item')
537
- .argument('<storeId>', 'DoorDash store ID')
538
- .argument('<itemId>', 'Menu item ID')
539
- .action(
540
- async (storeId: string, itemId: string, _opts: unknown, cmd: Command) => {
541
- await run(cmd, async () => {
542
- const item = await getItemDetails(storeId, itemId);
543
- return { item };
544
- });
545
- },
546
- );
547
-
548
- // =========================================================================
549
- // cart — cart operations (subcommand group)
550
- // =========================================================================
551
- const cart = dd.command('cart').description('Cart operations');
552
-
553
- // cart add
554
- cart
555
- .command('add')
556
- .description('Add an item to your cart')
557
- .requiredOption('--store-id <storeId>', 'Store ID')
558
- .requiredOption('--menu-id <menuId>', 'Menu ID')
559
- .requiredOption('--item-id <itemId>', 'Item ID')
560
- .requiredOption('--item-name <name>', 'Item name')
561
- .requiredOption('--unit-price <cents>', 'Unit price in cents')
562
- .option('--quantity <n>', 'Quantity', '1')
563
- .option('--cart-id <cartId>', 'Existing cart ID (creates new if omitted)')
564
- .option('--special-instructions <text>', 'Special instructions')
565
- .option('--options <json>', 'Item customization options as JSON array (from item details or recording)')
566
- .action(
567
- async (
568
- opts: {
569
- storeId: string;
570
- menuId: string;
571
- itemId: string;
572
- itemName: string;
573
- unitPrice: string;
574
- quantity: string;
575
- cartId?: string;
576
- specialInstructions?: string;
577
- options?: string;
578
- },
579
- cmd: Command,
580
- ) => {
581
- await run(cmd, async () => {
582
- const result = await addToCart({
583
- storeId: opts.storeId,
584
- menuId: opts.menuId,
585
- itemId: opts.itemId,
586
- itemName: opts.itemName,
587
- unitPrice: parseInt(opts.unitPrice, 10),
588
- quantity: parseInt(opts.quantity, 10),
589
- cartId: opts.cartId,
590
- specialInstructions: opts.specialInstructions,
591
- nestedOptions: opts.options,
592
- });
593
- return { cart: result };
594
- });
595
- },
596
- );
597
-
598
- // cart remove
599
- cart
600
- .command('remove')
601
- .description('Remove an item from your cart')
602
- .requiredOption('--cart-id <cartId>', 'Cart ID')
603
- .requiredOption('--item-id <itemId>', 'Order item ID (from cart view)')
604
- .action(
605
- async (opts: { cartId: string; itemId: string }, cmd: Command) => {
606
- await run(cmd, async () => {
607
- const result = await removeFromCart(opts.cartId, opts.itemId);
608
- return { cart: result };
609
- });
610
- },
611
- );
612
-
613
- // cart view
614
- cart
615
- .command('view')
616
- .description('View cart contents')
617
- .argument('<cartId>', 'Cart ID')
618
- .action(async (cartId: string, _opts: unknown, cmd: Command) => {
619
- await run(cmd, async () => {
620
- const result = await viewCart(cartId);
621
- return { cart: result };
622
- });
623
- });
624
-
625
- // cart list
626
- cart
627
- .command('list')
628
- .description('List all active carts')
629
- .option('--store-id <storeId>', 'Filter by store ID')
630
- .action(async (opts: { storeId?: string }, cmd: Command) => {
631
- await run(cmd, async () => {
632
- const carts = await listCarts(opts.storeId);
633
- return { carts, count: carts.length };
634
- });
635
- });
636
-
637
- // cart learn — capture customization options via CDP recording
638
- cart
639
- .command('learn')
640
- .description(
641
- 'Learn item customization options by recording a browser interaction. ' +
642
- 'Opens Chrome and watches you customize an item — when you add it to cart, ' +
643
- 'the nestedOptions and specialInstructions are extracted and output.',
644
- )
645
- .option('--duration <seconds>', 'Max recording duration in seconds', '120')
646
- .action(async (opts: { duration: string }, cmd: Command) => {
647
- const json = getJson(cmd);
648
- const duration = parseInt(opts.duration, 10);
649
-
650
- try {
651
- await ensureChromeWithCDP();
652
-
653
- const startTime = Date.now() / 1000;
654
- const recorder = new NetworkRecorder('doordash.com');
655
- await recorder.startDirect('http://localhost:9222');
656
-
657
- process.stderr.write('Recording... Navigate to an item, customize it, and add it to cart.\n');
658
- process.stderr.write(`Will auto-stop when "updateCartItem" is detected. Timeout: ${duration}s.\n`);
659
-
660
- await new Promise<void>((resolve) => {
661
- const timer = setTimeout(() => {
662
- if (poll) clearInterval(poll);
663
- process.stderr.write(`\nTimeout reached (${duration}s).\n`);
664
- resolve();
665
- }, duration * 1000);
666
-
667
- process.on('SIGINT', () => {
668
- if (poll) clearInterval(poll);
669
- clearTimeout(timer);
670
- resolve();
671
- });
672
-
673
- const poll = setInterval(() => {
674
- const entries = recorder.getEntries();
675
- const found = entries.some(e => {
676
- if (!e.request.postData) return false;
677
- try {
678
- const body = JSON.parse(e.request.postData) as { operationName?: string };
679
- return body.operationName === 'updateCartItem';
680
- } catch { return false; }
681
- });
682
- if (found) {
683
- clearInterval(poll);
684
- clearTimeout(timer);
685
- process.stderr.write('\nDetected "updateCartItem" operation.\n');
686
- setTimeout(() => resolve(), 3000);
687
- }
688
- }, 500);
689
- });
690
-
691
- process.stderr.write('Stopping recording...\n');
692
- const cookies = await recorder.extractCookies('doordash.com');
693
- const entries = await recorder.stop();
694
-
695
- const recording: SessionRecording = {
696
- id: crypto.randomUUID(),
697
- startedAt: startTime,
698
- endedAt: Date.now() / 1000,
699
- targetDomain: 'doordash.com',
700
- networkEntries: entries,
701
- cookies,
702
- observations: [],
703
- };
704
-
705
- // Extract updateCartItem operations
706
- const queries = extractQueries(recording);
707
- const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
708
-
709
- if (cartOps.length === 0) {
710
- outputError('No updateCartItem operations captured. Did you add an item to cart?');
711
- return;
712
- }
713
-
714
- const extracted = cartOps.map(q => {
715
- const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
716
- const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
717
- return {
718
- itemId: params.itemId as string | undefined,
719
- itemName: params.itemName as string | undefined,
720
- nestedOptions: params.nestedOptions as string | undefined,
721
- specialInstructions: params.specialInstructions as string | undefined,
722
- unitPrice: params.unitPrice as number | undefined,
723
- menuId: params.menuId as string | undefined,
724
- storeId: params.storeId as string | undefined,
725
- };
726
- });
727
-
728
- // Also save the recording for future reference
729
- const recordingPath = saveRecording(recording);
730
-
731
- output(
732
- {
733
- ok: true,
734
- items: extracted,
735
- count: extracted.length,
736
- recordingId: recording.id,
737
- recordingPath,
738
- },
739
- json,
740
- );
741
- } catch (err) {
742
- outputError(err instanceof Error ? err.message : String(err));
743
- }
744
- });
745
-
746
- // =========================================================================
747
- // checkout — get checkout / dropoff options
748
- // =========================================================================
749
- dd.command('checkout')
750
- .description('Get delivery/dropoff options for a cart')
751
- .argument('<cartId>', 'Cart ID')
752
- .option('--address-id <addressId>', 'Delivery address ID')
753
- .action(
754
- async (
755
- cartId: string,
756
- opts: { addressId?: string },
757
- cmd: Command,
758
- ) => {
759
- await run(cmd, async () => {
760
- const options = await getDropoffOptions(cartId, opts.addressId);
761
- return { dropoffOptions: options };
762
- });
763
- },
764
- );
765
-
766
- // =========================================================================
767
- // order — order operations (subcommand group)
768
- // =========================================================================
769
- const order = dd.command('order').description('Order operations');
770
-
771
- // order place
772
- order
773
- .command('place')
774
- .description('Place an order from a cart')
775
- .requiredOption('--cart-id <cartId>', 'Cart ID')
776
- .requiredOption('--store-id <storeId>', 'Store ID')
777
- .requiredOption('--total <cents>', 'Order total in cents')
778
- .option('--tip <cents>', 'Tip amount in cents', '0')
779
- .option('--delivery-option <type>', 'Delivery option type', 'STANDARD')
780
- .option('--dropoff-option <id>', 'Dropoff option ID (from checkout command)')
781
- .option('--payment-uuid <uuid>', 'Payment method UUID (uses default if omitted)')
782
- .option('--payment-type <type>', 'Payment method type', 'Card')
783
- .action(
784
- async (
785
- opts: {
786
- cartId: string;
787
- storeId: string;
788
- total: string;
789
- tip: string;
790
- deliveryOption: string;
791
- dropoffOption?: string;
792
- paymentUuid?: string;
793
- paymentType: string;
794
- },
795
- cmd: Command,
796
- ) => {
797
- await run(cmd, async () => {
798
- const result = await placeOrder({
799
- cartId: opts.cartId,
800
- storeId: opts.storeId,
801
- total: parseInt(opts.total, 10),
802
- tipAmount: parseInt(opts.tip, 10),
803
- deliveryOptionType: opts.deliveryOption,
804
- dropoffOptionId: opts.dropoffOption,
805
- paymentMethodUuid: opts.paymentUuid,
806
- paymentMethodType: opts.paymentType,
807
- });
808
- return { order: result };
809
- });
810
- },
811
- );
812
-
813
- // =========================================================================
814
- // payment-methods — list saved payment methods
815
- // =========================================================================
816
- dd.command('payment-methods')
817
- .description('List saved payment methods')
818
- .action(async (_opts: unknown, cmd: Command) => {
819
- await run(cmd, async () => {
820
- const methods = await getPaymentMethods();
821
- return { methods, count: methods.length };
822
- });
823
- });
824
- }
825
-
826
- // ---------------------------------------------------------------------------
827
- // Chrome CDP restart helper
828
- // ---------------------------------------------------------------------------
829
-
830
- import { spawn as spawnChild } from 'node:child_process';
831
- import { homedir } from 'node:os';
832
- import { join as pathJoin } from 'node:path';
833
-
834
- const CDP_BASE = 'http://localhost:9222';
835
- const CHROME_DATA_DIR = pathJoin(
836
- homedir(),
837
- 'Library/Application Support/Google/Chrome-CDP',
838
- );
839
-
840
- async function isCdpReady(): Promise<boolean> {
841
- try {
842
- const res = await fetch(`${CDP_BASE}/json/version`);
843
- return res.ok;
844
- } catch {
845
- return false;
846
- }
847
- }
848
-
849
- async function ensureChromeWithCDP(): Promise<void> {
850
- // Already running with CDP?
851
- if (await isCdpReady()) return;
852
-
853
- // Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
854
- // Using a dedicated --user-data-dir allows coexistence without killing the user's browser.
855
- const chromeApp =
856
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
857
- spawnChild(chromeApp, [
858
- `--remote-debugging-port=9222`,
859
- `--force-renderer-accessibility`,
860
- `--user-data-dir=${CHROME_DATA_DIR}`,
861
- `https://www.doordash.com/consumer/login/`,
862
- ], {
863
- detached: true,
864
- stdio: 'ignore',
865
- }).unref();
866
-
867
- // Wait for CDP to be ready
868
- for (let i = 0; i < 30; i++) {
869
- await new Promise(r => setTimeout(r, 500));
870
- if (await isCdpReady()) return;
871
- }
872
- throw new Error('Chrome started but CDP endpoint not responding after 15s');
873
- }
874
-
875
- async function minimizeChromeWindow(): Promise<void> {
876
- const res = await fetch(`${CDP_BASE}/json/list`);
877
- const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
878
- const pageTarget = targets.find(t => t.type === 'page');
879
- if (!pageTarget) return;
880
-
881
- const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
882
-
883
- await new Promise<void>((resolve, reject) => {
884
- const timeout = setTimeout(() => {
885
- ws.close();
886
- reject(new Error('CDP minimize timed out'));
887
- }, 5000);
888
-
889
- ws.addEventListener('open', () => {
890
- ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
891
- });
892
-
893
- ws.addEventListener('message', (event) => {
894
- const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
895
- if (msg.id === 1 && msg.result) {
896
- ws.send(JSON.stringify({
897
- id: 2,
898
- method: 'Browser.setWindowBounds',
899
- params: { windowId: msg.result.windowId, bounds: { windowState: 'minimized' } },
900
- }));
901
- } else if (msg.id === 1) {
902
- clearTimeout(timeout);
903
- ws.close();
904
- reject(new Error('Browser.getWindowForTarget failed'));
905
- } else if (msg.id === 2) {
906
- clearTimeout(timeout);
907
- ws.close();
908
- resolve();
909
- }
910
- });
911
-
912
- ws.addEventListener('error', (err) => {
913
- clearTimeout(timeout);
914
- reject(err);
915
- });
916
- });
917
- }
918
-
919
- async function restoreChromeWindow(): Promise<void> {
920
- const res = await fetch(`${CDP_BASE}/json/list`);
921
- const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
922
- const pageTarget = targets.find(t => t.type === 'page');
923
- if (!pageTarget) return;
924
-
925
- const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
926
-
927
- await new Promise<void>((resolve, reject) => {
928
- const timeout = setTimeout(() => {
929
- ws.close();
930
- reject(new Error('CDP restore timed out'));
931
- }, 5000);
932
-
933
- ws.addEventListener('open', () => {
934
- ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
935
- });
936
-
937
- ws.addEventListener('message', (event) => {
938
- const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
939
- if (msg.id === 1 && msg.result) {
940
- ws.send(JSON.stringify({
941
- id: 2,
942
- method: 'Browser.setWindowBounds',
943
- params: { windowId: msg.result.windowId, bounds: { windowState: 'normal' } },
944
- }));
945
- } else if (msg.id === 1) {
946
- clearTimeout(timeout);
947
- ws.close();
948
- reject(new Error('Browser.getWindowForTarget failed'));
949
- } else if (msg.id === 2) {
950
- clearTimeout(timeout);
951
- ws.close();
952
- resolve();
953
- }
954
- });
955
-
956
- ws.addEventListener('error', (err) => {
957
- clearTimeout(timeout);
958
- reject(err);
959
- });
960
- });
961
- }
962
-
963
- // ---------------------------------------------------------------------------
964
- // Ride Shotgun learn session helper
965
- // ---------------------------------------------------------------------------
966
-
967
- interface LearnResult {
968
- recordingId?: string;
969
- recordingPath?: string;
970
- }
971
-
972
- async function startLearnSession(durationSeconds: number): Promise<LearnResult> {
973
- // Step 1: Ensure Chrome is running with CDP
974
- await ensureChromeWithCDP();
975
-
976
- // Step 2: Connect to daemon and start recording
977
- return new Promise((resolve, reject) => {
978
- const socketPath = getSocketPath();
979
- const sessionToken = readSessionToken();
980
- const socket = net.createConnection(socketPath);
981
- const parser = createMessageParser();
982
-
983
- socket.on('error', (err) => {
984
- reject(new Error(`Cannot connect to daemon: ${err.message}. Is the daemon running?`));
985
- });
986
-
987
- // Timeout safety — unref so it doesn't keep process alive
988
- const timeoutHandle = setTimeout(() => {
989
- socket.destroy();
990
- reject(new Error(`Learn session timed out after ${durationSeconds + 30}s`));
991
- }, (durationSeconds + 30) * 1000);
992
- timeoutHandle.unref();
993
-
994
- let authenticated = !sessionToken; // If no token needed, consider already authenticated
995
-
996
- const sendStartCommand = () => {
997
- socket.write(
998
- serialize({
999
- type: 'ride_shotgun_start',
1000
- durationSeconds,
1001
- intervalSeconds: 5,
1002
- mode: 'learn',
1003
- targetDomain: 'doordash.com',
1004
- } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
1005
- );
1006
- };
1007
-
1008
- socket.on('data', (chunk) => {
1009
- const messages = parser.feed(chunk.toString('utf-8'));
1010
- for (const msg of messages) {
1011
- const m = msg as unknown as Record<string, unknown>;
1012
-
1013
- // Handle auth handshake
1014
- if (!authenticated && m.type === 'auth_result') {
1015
- if ((m as { success: boolean }).success) {
1016
- authenticated = true;
1017
- sendStartCommand();
1018
- } else {
1019
- clearTimeout(timeoutHandle);
1020
- socket.destroy();
1021
- reject(new Error('Daemon authentication failed'));
1022
- }
1023
- continue;
1024
- }
1025
-
1026
- // Skip duplicate auth_result after already authenticated
1027
- if (m.type === 'auth_result') {
1028
- continue;
1029
- }
1030
-
1031
- if (m.type === 'ride_shotgun_result') {
1032
- clearTimeout(timeoutHandle);
1033
- socket.destroy();
1034
- resolve({
1035
- recordingId: m.recordingId as string | undefined,
1036
- recordingPath: m.recordingPath as string | undefined,
1037
- });
1038
- }
1039
- }
1040
- });
1041
-
1042
- socket.on('connect', () => {
1043
- if (sessionToken) {
1044
- // Send auth and wait for auth_result before sending the command
1045
- socket.write(
1046
- serialize({
1047
- type: 'auth',
1048
- token: sessionToken,
1049
- } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
1050
- );
1051
- } else {
1052
- // No auth needed, send command immediately
1053
- sendStartCommand();
1054
- }
1055
- });
1056
- });
1057
- }