@vellumai/assistant 0.4.11 → 0.4.13

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