@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.
- package/ARCHITECTURE.md +401 -385
- package/package.json +1 -1
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +75 -61
- package/src/__tests__/registry.test.ts +235 -187
- package/src/__tests__/secure-keys.test.ts +27 -0
- package/src/__tests__/session-agent-loop.test.ts +521 -256
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/skills.test.ts +334 -276
- package/src/__tests__/slack-skill.test.ts +124 -0
- package/src/__tests__/starter-task-flow.test.ts +7 -17
- package/src/agent/loop.ts +10 -3
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +449 -0
- package/src/config/bundled-skills/doordash/SKILL.md +171 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +203 -0
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +164 -0
- package/src/config/bundled-skills/doordash/doordash-cli.ts +1193 -0
- package/src/config/bundled-skills/doordash/doordash-entry.ts +22 -0
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +787 -0
- package/src/config/bundled-skills/doordash/lib/client.ts +1071 -0
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +85 -0
- package/src/config/bundled-skills/doordash/lib/queries.ts +28 -0
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +94 -0
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +203 -0
- package/src/config/bundled-skills/doordash/lib/session.ts +93 -0
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +61 -0
- package/src/config/bundled-skills/doordash/lib/shared/ipc.ts +32 -0
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +380 -0
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +35 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +43 -0
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +49 -0
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +6 -0
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +246 -0
- package/src/config/bundled-skills/doordash/lib/types.ts +367 -0
- package/src/config/bundled-skills/google-calendar/SKILL.md +4 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +41 -41
- package/src/config/bundled-skills/messaging/SKILL.md +59 -42
- package/src/config/bundled-skills/messaging/TOOLS.json +14 -92
- package/src/config/bundled-skills/messaging/tools/gmail-archive-by-query.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/gmail-batch-archive.ts +11 -2
- package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +12 -4
- package/src/config/bundled-skills/messaging/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +5 -2
- package/src/config/bundled-skills/notion/SKILL.md +240 -0
- package/src/config/bundled-skills/notion-oauth-setup/SKILL.md +127 -0
- package/src/config/bundled-skills/oauth-setup/SKILL.md +144 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +76 -45
- package/src/config/bundled-skills/skills-catalog/SKILL.md +32 -29
- package/src/config/bundled-skills/slack/SKILL.md +49 -0
- package/src/config/bundled-skills/slack/TOOLS.json +167 -0
- package/src/config/bundled-skills/slack/tools/shared.ts +23 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-add-reaction.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +33 -0
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +75 -0
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-delete-message.ts +2 -5
- package/src/config/bundled-skills/{messaging → slack}/tools/slack-leave-channel.ts +2 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +193 -0
- package/src/config/{vellum-skills → bundled-skills}/sms-setup/SKILL.md +29 -22
- package/src/config/{vellum-skills → bundled-skills}/telegram-setup/SKILL.md +17 -14
- package/src/config/{vellum-skills → bundled-skills}/twilio-setup/SKILL.md +20 -5
- package/src/config/bundled-tool-registry.ts +292 -267
- package/src/config/schema.ts +1 -1
- package/src/daemon/handlers/skills.ts +334 -234
- package/src/daemon/ipc-contract/messages.ts +2 -0
- package/src/daemon/ipc-contract/surfaces.ts +2 -0
- package/src/daemon/lifecycle.ts +358 -221
- package/src/daemon/response-tier.ts +2 -0
- package/src/daemon/server.ts +453 -193
- package/src/daemon/session-agent-loop-handlers.ts +43 -2
- package/src/daemon/session-agent-loop.ts +3 -0
- package/src/daemon/session-lifecycle.ts +3 -0
- package/src/daemon/session-process.ts +1 -0
- package/src/daemon/session-surfaces.ts +22 -20
- package/src/daemon/session-tool-setup.ts +1 -0
- package/src/daemon/session.ts +5 -2
- package/src/messaging/outreach-classifier.ts +12 -5
- package/src/messaging/provider-types.ts +5 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +11 -5
- package/src/messaging/providers/gmail/client.ts +2 -0
- package/src/messaging/providers/slack/adapter.ts +1 -0
- package/src/messaging/providers/slack/client.ts +8 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/runtime/http-errors.ts +33 -20
- package/src/runtime/http-server.ts +706 -291
- package/src/runtime/http-types.ts +26 -16
- package/src/runtime/routes/secret-routes.ts +57 -2
- package/src/runtime/routes/surface-action-routes.ts +66 -0
- package/src/runtime/routes/trust-rules-routes.ts +140 -0
- package/src/security/keychain-to-encrypted-migration.ts +59 -0
- package/src/security/secure-keys.ts +17 -0
- package/src/skills/frontmatter.ts +9 -7
- package/src/tools/apps/executors.ts +2 -1
- package/src/tools/tool-manifest.ts +44 -42
- package/src/tools/types.ts +9 -0
- package/src/__tests__/skill-mirror-parity.test.ts +0 -176
- package/src/config/vellum-skills/catalog.json +0 -63
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +0 -295
- package/src/skills/vellum-catalog-remote.ts +0 -166
- package/src/tools/skills/vellum-catalog.ts +0 -168
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/chatgpt-import/TOOLS.json +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/deploy-fullstack-vercel/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/document-writer/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/guardian-verify-setup/SKILL.md +0 -0
- /package/src/config/{vellum-skills → bundled-skills}/slack-oauth-setup/SKILL.md +0 -0
- /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
|
+
}
|