amazon-personal-shopping-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/index.js +2671 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2671 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/env.ts
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
import { config } from "dotenv";
|
|
7
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
config({ path: resolve(__dirname, "..", ".env") });
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
+
|
|
13
|
+
// src/server.ts
|
|
14
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
|
+
|
|
16
|
+
// src/tools/search.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/errors.ts
|
|
20
|
+
var BrowserError = class extends Error {
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.name = "BrowserError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var BrowserNotInstalledError = class extends BrowserError {
|
|
27
|
+
constructor(message = "Chromium is not installed. Run: npx playwright install chromium") {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "BrowserNotInstalledError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var BrowserLaunchError = class extends BrowserError {
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
|
+
this.name = "BrowserLaunchError";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var SessionExpiredError = class extends BrowserError {
|
|
39
|
+
constructor(message = "Session expired. Please call amazon-login to re-authenticate.") {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "SessionExpiredError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var AuthRequiredError = class extends BrowserError {
|
|
45
|
+
constructor(message = "Not logged in. Please call amazon-login to authenticate.") {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "AuthRequiredError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ContextInUseError = class extends BrowserError {
|
|
51
|
+
constructor(message = "Browser context is already in use by another process. Close other instances first.") {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "ContextInUseError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var ScrapingError = class extends BrowserError {
|
|
57
|
+
constructor(message, options) {
|
|
58
|
+
super(message, options);
|
|
59
|
+
this.name = "ScrapingError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var CaptchaRequiredError = class extends BrowserError {
|
|
63
|
+
constructor(message = "CAPTCHA required - please solve in browser window", options) {
|
|
64
|
+
super(message, options);
|
|
65
|
+
this.name = "CaptchaRequiredError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/tools/search.ts
|
|
70
|
+
function registerSearchTools(server2, provider2) {
|
|
71
|
+
server2.tool(
|
|
72
|
+
"search-products",
|
|
73
|
+
"Search for products on Amazon. Returns structured product data including ASIN, title, price, rating, brand, and Prime eligibility. Requires an active Amazon session (call amazon-login first).",
|
|
74
|
+
{
|
|
75
|
+
query: z.string().describe("Search query for Amazon products"),
|
|
76
|
+
category: z.string().optional().describe("Product category to filter by (e.g., 'electronics')"),
|
|
77
|
+
maxResults: z.number().int().min(1).max(50).optional().describe("Maximum number of results to return (default: 20)")
|
|
78
|
+
},
|
|
79
|
+
async ({ query, category, maxResults }) => {
|
|
80
|
+
try {
|
|
81
|
+
const products = await provider2.searchProducts(query, category, maxResults);
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: "text", text: JSON.stringify(products, null, 2) }],
|
|
84
|
+
isError: false
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
93
|
+
}
|
|
94
|
+
],
|
|
95
|
+
isError: true
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: `Search failed: ${message}` }],
|
|
101
|
+
isError: true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/tools/product.ts
|
|
109
|
+
import { z as z2 } from "zod";
|
|
110
|
+
function registerProductTools(server2, provider2) {
|
|
111
|
+
server2.tool(
|
|
112
|
+
"get-product-details",
|
|
113
|
+
"Get detailed product information for an Amazon product by ASIN. Returns title, description, features, price, rating, availability, images, and more. Requires an active Amazon session (call amazon-login first).",
|
|
114
|
+
{
|
|
115
|
+
asin: z2.string().describe("Amazon Standard Identification Number (ASIN) of the product")
|
|
116
|
+
},
|
|
117
|
+
async ({ asin }) => {
|
|
118
|
+
try {
|
|
119
|
+
const { details, primaryImageBase64 } = await provider2.getProductDetails(asin);
|
|
120
|
+
const content = [{ type: "text", text: JSON.stringify(details, null, 2) }];
|
|
121
|
+
if (primaryImageBase64) {
|
|
122
|
+
content.push({
|
|
123
|
+
type: "image",
|
|
124
|
+
data: primaryImageBase64,
|
|
125
|
+
mimeType: "image/jpeg"
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return { content, isError: false };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
isError: true
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: `Failed to get product details: ${message}` }],
|
|
144
|
+
isError: true
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/tools/reviews.ts
|
|
152
|
+
import { z as z3 } from "zod";
|
|
153
|
+
function registerReviewTools(server2, provider2) {
|
|
154
|
+
server2.tool(
|
|
155
|
+
"get-reviews",
|
|
156
|
+
"Get customer reviews for an Amazon product by ASIN. Returns individual reviews with author, rating, title, body, date, and verified purchase status, plus summary statistics with average rating and star distribution. Supports sorting by relevance or recency, and filtering by star rating. Requires an active Amazon session (call amazon-login first).",
|
|
157
|
+
{
|
|
158
|
+
asin: z3.string().describe("ASIN of the product to get reviews for"),
|
|
159
|
+
maxReviews: z3.number().int().min(1).max(50).optional().describe("Maximum number of reviews to return (default: all from first page)"),
|
|
160
|
+
sortBy: z3.enum(["relevant", "recent"]).optional().describe("Sort reviews by relevance or most recent"),
|
|
161
|
+
filterByStars: z3.number().int().min(1).max(5).optional().describe("Filter reviews by star rating (1-5)")
|
|
162
|
+
},
|
|
163
|
+
async ({ asin, maxReviews, sortBy, filterByStars }) => {
|
|
164
|
+
try {
|
|
165
|
+
const result = await provider2.getReviews(asin, maxReviews, sortBy, filterByStars);
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
168
|
+
isError: false
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
isError: true
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: `Failed to get reviews: ${message}` }],
|
|
185
|
+
isError: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/tools/cart.ts
|
|
193
|
+
import { z as z4 } from "zod";
|
|
194
|
+
function registerCartTools(server2, provider2) {
|
|
195
|
+
server2.tool(
|
|
196
|
+
"add-to-cart",
|
|
197
|
+
"Add a product to your Amazon cart. Before using this tool, confirm with the user which product and quantity they want to add. Requires an active Amazon session (call amazon-login first).",
|
|
198
|
+
{
|
|
199
|
+
asin: z4.string().describe("ASIN of the product to add to cart"),
|
|
200
|
+
quantity: z4.number().int().positive().optional().describe("Quantity to add (default: 1)")
|
|
201
|
+
},
|
|
202
|
+
async ({ asin, quantity }) => {
|
|
203
|
+
try {
|
|
204
|
+
const result = await provider2.addToCart(asin, quantity);
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: JSON.stringify(result, null, 2)
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
isError: !result.success
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
isError: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text", text: `Failed to add to cart: ${message}` }],
|
|
229
|
+
isError: true
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
server2.tool(
|
|
235
|
+
"view-cart",
|
|
236
|
+
"View the contents of your Amazon cart including items, quantities, prices, and subtotal. Requires an active Amazon session (call amazon-login first).",
|
|
237
|
+
{},
|
|
238
|
+
async () => {
|
|
239
|
+
try {
|
|
240
|
+
const cart = await provider2.getCart();
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: JSON.stringify(cart, null, 2)
|
|
246
|
+
}
|
|
247
|
+
],
|
|
248
|
+
isError: false
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
isError: true
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
+
return {
|
|
264
|
+
content: [{ type: "text", text: `Failed to view cart: ${message}` }],
|
|
265
|
+
isError: true
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
server2.tool(
|
|
271
|
+
"remove-from-cart",
|
|
272
|
+
"Remove a specific item from your Amazon cart. Before using this tool, confirm with the user which item they want to remove. Requires an active Amazon session (call amazon-login first).",
|
|
273
|
+
{
|
|
274
|
+
asin: z4.string().describe("ASIN of the product to remove from cart")
|
|
275
|
+
},
|
|
276
|
+
async ({ asin }) => {
|
|
277
|
+
try {
|
|
278
|
+
const result = await provider2.removeFromCart(asin);
|
|
279
|
+
return {
|
|
280
|
+
content: [
|
|
281
|
+
{
|
|
282
|
+
type: "text",
|
|
283
|
+
text: JSON.stringify(result, null, 2)
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
isError: !result.success
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
isError: true
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
301
|
+
return {
|
|
302
|
+
content: [{ type: "text", text: `Failed to remove from cart: ${message}` }],
|
|
303
|
+
isError: true
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
server2.tool(
|
|
309
|
+
"clear-cart",
|
|
310
|
+
"Remove all items from your Amazon cart. Before using this tool, confirm with the user that they want to clear their entire cart. This action cannot be undone. Requires an active Amazon session (call amazon-login first).",
|
|
311
|
+
{},
|
|
312
|
+
async () => {
|
|
313
|
+
try {
|
|
314
|
+
await provider2.clearCart();
|
|
315
|
+
return {
|
|
316
|
+
content: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: "Cart cleared successfully"
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
isError: false
|
|
323
|
+
};
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
isError: true
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text", text: `Failed to clear cart: ${message}` }],
|
|
339
|
+
isError: true
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/tools/orders.ts
|
|
347
|
+
import { z as z5 } from "zod";
|
|
348
|
+
function registerOrderTools(server2, provider2) {
|
|
349
|
+
server2.tool(
|
|
350
|
+
"get-order-history",
|
|
351
|
+
"View your Amazon order history. Returns a list of past orders with order ID, date, total, status, and item count. Supports pagination and optional date filtering. Requires an active Amazon session (call amazon-login first).",
|
|
352
|
+
{
|
|
353
|
+
limit: z5.number().int().min(1).max(50).optional().describe("Maximum number of orders to return (1-50, default: 10)"),
|
|
354
|
+
page: z5.number().int().min(1).optional().describe("Page number for pagination (default: 1)"),
|
|
355
|
+
startDate: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").optional().describe("Filter orders from this date (YYYY-MM-DD format)"),
|
|
356
|
+
endDate: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").optional().describe("Filter orders until this date (YYYY-MM-DD format)")
|
|
357
|
+
},
|
|
358
|
+
async ({ limit, page, startDate, endDate }) => {
|
|
359
|
+
try {
|
|
360
|
+
const result = await provider2.getOrderHistory({
|
|
361
|
+
limit,
|
|
362
|
+
page,
|
|
363
|
+
startDate,
|
|
364
|
+
endDate
|
|
365
|
+
});
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{
|
|
369
|
+
type: "text",
|
|
370
|
+
text: JSON.stringify(result, null, 2)
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
isError: false
|
|
374
|
+
};
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
377
|
+
return {
|
|
378
|
+
content: [
|
|
379
|
+
{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
382
|
+
}
|
|
383
|
+
],
|
|
384
|
+
isError: true
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text", text: `Failed to get order history: ${message}` }],
|
|
390
|
+
isError: true
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
server2.tool(
|
|
396
|
+
"get-order-details",
|
|
397
|
+
"View detailed information about a specific Amazon order including all items, prices, quantities, and delivery status. Requires an active Amazon session (call amazon-login first).",
|
|
398
|
+
{
|
|
399
|
+
orderId: z5.string().describe("The Amazon order ID (e.g., 112-3456789-0123456)")
|
|
400
|
+
},
|
|
401
|
+
async ({ orderId }) => {
|
|
402
|
+
try {
|
|
403
|
+
const order = await provider2.getOrderDetails(orderId);
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{
|
|
407
|
+
type: "text",
|
|
408
|
+
text: JSON.stringify(order, null, 2)
|
|
409
|
+
}
|
|
410
|
+
],
|
|
411
|
+
isError: false
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
415
|
+
return {
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: "text",
|
|
419
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
isError: true
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: `Failed to get order details: ${message}` }],
|
|
428
|
+
isError: true
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/tools/purchase.ts
|
|
436
|
+
import { z as z6 } from "zod";
|
|
437
|
+
function registerPurchaseTools(server2, provider2) {
|
|
438
|
+
server2.tool(
|
|
439
|
+
"perform-purchase",
|
|
440
|
+
`Complete the checkout process and place an order for all items currently in your Amazon cart.
|
|
441
|
+
|
|
442
|
+
**IMPORTANT - AI SAFETY REQUIREMENT**: Before calling this tool, you MUST:
|
|
443
|
+
1. Call view-cart to show the user exactly what items are in their cart
|
|
444
|
+
2. Display the items, quantities, and total price to the user
|
|
445
|
+
3. Ask the user to explicitly confirm they want to purchase these items
|
|
446
|
+
4. Only call this tool after receiving clear confirmation (e.g., "yes", "confirm", "proceed")
|
|
447
|
+
|
|
448
|
+
This tool will:
|
|
449
|
+
- Navigate to Amazon checkout
|
|
450
|
+
- Use the default shipping address and payment method
|
|
451
|
+
- Log all items and totals before placing the order
|
|
452
|
+
- Complete the purchase and return the order ID
|
|
453
|
+
|
|
454
|
+
Requires an active Amazon session with a valid payment method on file (call amazon-login first).`,
|
|
455
|
+
{
|
|
456
|
+
confirmationToken: z6.string().min(1).describe(
|
|
457
|
+
"A non-empty string confirming the user has approved this purchase. Can be any confirmation like 'yes', 'confirmed', or the user's explicit approval text."
|
|
458
|
+
)
|
|
459
|
+
},
|
|
460
|
+
async ({ confirmationToken }) => {
|
|
461
|
+
try {
|
|
462
|
+
const result = await provider2.purchase(confirmationToken);
|
|
463
|
+
return {
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: JSON.stringify(result, null, 2)
|
|
468
|
+
}
|
|
469
|
+
],
|
|
470
|
+
isError: !result.success
|
|
471
|
+
};
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (error instanceof SessionExpiredError || error instanceof AuthRequiredError) {
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: "text",
|
|
478
|
+
text: "Not logged in or session expired. Please call amazon-login first."
|
|
479
|
+
}
|
|
480
|
+
],
|
|
481
|
+
isError: true
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
485
|
+
return {
|
|
486
|
+
content: [{ type: "text", text: `Failed to complete purchase: ${message}` }],
|
|
487
|
+
isError: true
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/tools/auth.ts
|
|
495
|
+
import { z as z7 } from "zod";
|
|
496
|
+
|
|
497
|
+
// src/providers/playwright/browser.ts
|
|
498
|
+
import { chromium } from "playwright";
|
|
499
|
+
|
|
500
|
+
// src/utils/paths.ts
|
|
501
|
+
import { homedir, platform } from "os";
|
|
502
|
+
import { join } from "path";
|
|
503
|
+
import { mkdirSync, accessSync, constants } from "fs";
|
|
504
|
+
var APP_NAME = "amazon-shopping-mcp";
|
|
505
|
+
function getDataDir() {
|
|
506
|
+
const envOverride = process.env.AMAZON_MCP_DATA_DIR;
|
|
507
|
+
if (envOverride) {
|
|
508
|
+
return join(envOverride, APP_NAME);
|
|
509
|
+
}
|
|
510
|
+
const os = platform();
|
|
511
|
+
const home = homedir();
|
|
512
|
+
if (os === "darwin") {
|
|
513
|
+
return join(home, "Library", "Application Support", APP_NAME);
|
|
514
|
+
}
|
|
515
|
+
if (os === "linux") {
|
|
516
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
517
|
+
if (xdgDataHome) {
|
|
518
|
+
return join(xdgDataHome, APP_NAME);
|
|
519
|
+
}
|
|
520
|
+
return join(home, ".local", "share", APP_NAME);
|
|
521
|
+
}
|
|
522
|
+
throw new Error(`Unsupported platform: ${os}. Only macOS and Linux are supported.`);
|
|
523
|
+
}
|
|
524
|
+
function ensureDataDir() {
|
|
525
|
+
const dir = getDataDir();
|
|
526
|
+
mkdirSync(dir, { recursive: true });
|
|
527
|
+
validateDirWritable(dir);
|
|
528
|
+
return dir;
|
|
529
|
+
}
|
|
530
|
+
function validateDirWritable(dir) {
|
|
531
|
+
try {
|
|
532
|
+
accessSync(dir, constants.W_OK);
|
|
533
|
+
} catch {
|
|
534
|
+
throw new Error(`Data directory is not writable: ${dir}. Check file permissions.`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/providers/playwright/stealth.ts
|
|
539
|
+
var STEALTH_ARGS = [
|
|
540
|
+
"--disable-blink-features=AutomationControlled",
|
|
541
|
+
"--disable-features=AutomationControlled",
|
|
542
|
+
"--disable-dev-shm-usage",
|
|
543
|
+
"--disable-infobars",
|
|
544
|
+
"--disable-background-networking",
|
|
545
|
+
"--disable-default-apps",
|
|
546
|
+
"--disable-extensions",
|
|
547
|
+
"--disable-sync",
|
|
548
|
+
"--no-first-run",
|
|
549
|
+
"--no-default-browser-check",
|
|
550
|
+
"--disable-component-update",
|
|
551
|
+
"--disable-hang-monitor"
|
|
552
|
+
];
|
|
553
|
+
var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
554
|
+
var VIEWPORT_PRESETS = [
|
|
555
|
+
{ width: 1920, height: 1080 },
|
|
556
|
+
{ width: 1536, height: 864 },
|
|
557
|
+
{ width: 1440, height: 900 },
|
|
558
|
+
{ width: 1366, height: 768 },
|
|
559
|
+
{ width: 1280, height: 800 }
|
|
560
|
+
];
|
|
561
|
+
function randomViewport() {
|
|
562
|
+
const preset = VIEWPORT_PRESETS[Math.floor(Math.random() * VIEWPORT_PRESETS.length)];
|
|
563
|
+
return { width: preset.width, height: preset.height };
|
|
564
|
+
}
|
|
565
|
+
var STEALTH_INIT_SCRIPT = `
|
|
566
|
+
// Hide navigator.webdriver
|
|
567
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
568
|
+
get: () => false,
|
|
569
|
+
configurable: true,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Mock window.chrome.runtime (missing in headless automation)
|
|
573
|
+
if (!window.chrome) {
|
|
574
|
+
window.chrome = {};
|
|
575
|
+
}
|
|
576
|
+
if (!window.chrome.runtime) {
|
|
577
|
+
window.chrome.runtime = {
|
|
578
|
+
connect: function() {},
|
|
579
|
+
sendMessage: function() {},
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Realistic navigator.plugins (Chrome PDF plugins)
|
|
584
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
585
|
+
get: () => {
|
|
586
|
+
const plugins = [
|
|
587
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
588
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
589
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
590
|
+
];
|
|
591
|
+
plugins.length = 3;
|
|
592
|
+
return plugins;
|
|
593
|
+
},
|
|
594
|
+
configurable: true,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Set realistic languages
|
|
598
|
+
Object.defineProperty(navigator, 'languages', {
|
|
599
|
+
get: () => ['en-US', 'en'],
|
|
600
|
+
configurable: true,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Mask automation artifacts in permissions API
|
|
604
|
+
const originalQuery = navigator.permissions.query.bind(navigator.permissions);
|
|
605
|
+
navigator.permissions.query = async (params) => {
|
|
606
|
+
if (params.name === 'notifications') {
|
|
607
|
+
return { state: 'prompt', onchange: null };
|
|
608
|
+
}
|
|
609
|
+
return originalQuery(params);
|
|
610
|
+
};
|
|
611
|
+
`;
|
|
612
|
+
async function applyStealthToContext(context) {
|
|
613
|
+
await context.addInitScript(STEALTH_INIT_SCRIPT);
|
|
614
|
+
}
|
|
615
|
+
function randomDelay(min, max) {
|
|
616
|
+
const ms = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
617
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
618
|
+
}
|
|
619
|
+
async function simulateMouseMovement(page) {
|
|
620
|
+
const viewport = page.viewportSize();
|
|
621
|
+
if (!viewport) return;
|
|
622
|
+
const steps = 8 + Math.floor(Math.random() * 8);
|
|
623
|
+
let x = Math.random() * viewport.width * 0.8;
|
|
624
|
+
let y = Math.random() * viewport.height * 0.8;
|
|
625
|
+
for (let i = 0; i < steps; i++) {
|
|
626
|
+
x += (Math.random() - 0.5) * 120;
|
|
627
|
+
y += (Math.random() - 0.5) * 80;
|
|
628
|
+
x = Math.max(10, Math.min(viewport.width - 10, x));
|
|
629
|
+
y = Math.max(10, Math.min(viewport.height - 10, y));
|
|
630
|
+
await page.mouse.move(x, y, { steps: 3 });
|
|
631
|
+
await randomDelay(10, 50);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function simulateScroll(page) {
|
|
635
|
+
const scrollCount = 2 + Math.floor(Math.random() * 3);
|
|
636
|
+
for (let i = 0; i < scrollCount; i++) {
|
|
637
|
+
const deltaY = 100 + Math.floor(Math.random() * 200);
|
|
638
|
+
await page.mouse.wheel(0, deltaY);
|
|
639
|
+
await randomDelay(200, 600);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
var lastNavigationTime = 0;
|
|
643
|
+
async function throttleNavigation() {
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
const elapsed = now - lastNavigationTime;
|
|
646
|
+
const minGap = 2e3 + Math.floor(Math.random() * 3e3);
|
|
647
|
+
if (lastNavigationTime > 0 && elapsed < minGap) {
|
|
648
|
+
await new Promise((resolve2) => setTimeout(resolve2, minGap - elapsed));
|
|
649
|
+
}
|
|
650
|
+
lastNavigationTime = Date.now();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/utils/logger.ts
|
|
654
|
+
var LOG_LEVEL_NAMES = {
|
|
655
|
+
[0 /* DEBUG */]: "DEBUG",
|
|
656
|
+
[1 /* INFO */]: "INFO",
|
|
657
|
+
[2 /* WARN */]: "WARN",
|
|
658
|
+
[3 /* ERROR */]: "ERROR"
|
|
659
|
+
};
|
|
660
|
+
var globalLogLevel = 1 /* INFO */;
|
|
661
|
+
if (process.env.AMAZON_MCP_DEBUG === "true" || process.env.AMAZON_MCP_DEBUG === "1") {
|
|
662
|
+
globalLogLevel = 0 /* DEBUG */;
|
|
663
|
+
}
|
|
664
|
+
function formatLogMessage(level, module, message, data) {
|
|
665
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
const levelName = LOG_LEVEL_NAMES[level];
|
|
667
|
+
let logLine = `${timestamp} [${levelName}] [${module}] ${message}`;
|
|
668
|
+
if (data && Object.keys(data).length > 0) {
|
|
669
|
+
logLine += ` ${JSON.stringify(data)}`;
|
|
670
|
+
}
|
|
671
|
+
return logLine;
|
|
672
|
+
}
|
|
673
|
+
function log(level, module, message, data) {
|
|
674
|
+
if (level < globalLogLevel) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const formattedMessage = formatLogMessage(level, module, message, data);
|
|
678
|
+
console.error(formattedMessage);
|
|
679
|
+
}
|
|
680
|
+
function createLogger(module) {
|
|
681
|
+
return {
|
|
682
|
+
debug(message, data) {
|
|
683
|
+
log(0 /* DEBUG */, module, message, data);
|
|
684
|
+
},
|
|
685
|
+
info(message, data) {
|
|
686
|
+
log(1 /* INFO */, module, message, data);
|
|
687
|
+
},
|
|
688
|
+
warn(message, data) {
|
|
689
|
+
log(2 /* WARN */, module, message, data);
|
|
690
|
+
},
|
|
691
|
+
error(message, data) {
|
|
692
|
+
log(3 /* ERROR */, module, message, data);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/providers/playwright/browser.ts
|
|
698
|
+
var AMAZON_BASE_URL = "https://www.amazon.com";
|
|
699
|
+
var browserContext = null;
|
|
700
|
+
var activePage = null;
|
|
701
|
+
var logger = createLogger("browser");
|
|
702
|
+
function log2(message) {
|
|
703
|
+
logger.info(message);
|
|
704
|
+
}
|
|
705
|
+
function getHeadlessMode() {
|
|
706
|
+
const env = process.env.AMAZON_MCP_HEADLESS;
|
|
707
|
+
if (env === void 0 || env === "") {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
if (env === "true") {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (env === "false") {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
log2(`Warning: Invalid AMAZON_MCP_HEADLESS value "${env}", defaulting to true`);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
async function installBrowserIfNeeded() {
|
|
720
|
+
try {
|
|
721
|
+
const browsers = chromium.executablePath();
|
|
722
|
+
if (browsers) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
} catch {
|
|
726
|
+
}
|
|
727
|
+
log2("Chromium not found. Installing...");
|
|
728
|
+
try {
|
|
729
|
+
const { execSync } = await import("child_process");
|
|
730
|
+
execSync("npx playwright install chromium", {
|
|
731
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
732
|
+
timeout: 3e5
|
|
733
|
+
});
|
|
734
|
+
log2("Chromium installed successfully");
|
|
735
|
+
} catch {
|
|
736
|
+
throw new BrowserNotInstalledError(
|
|
737
|
+
"Failed to auto-install Chromium. Run manually: npx playwright install chromium"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function ensureBrowser() {
|
|
742
|
+
if (browserContext) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
await installBrowserIfNeeded();
|
|
746
|
+
log2("Browser ready");
|
|
747
|
+
}
|
|
748
|
+
async function getBrowserContext(options) {
|
|
749
|
+
if (browserContext) {
|
|
750
|
+
return browserContext;
|
|
751
|
+
}
|
|
752
|
+
await installBrowserIfNeeded();
|
|
753
|
+
const dataDir = ensureDataDir();
|
|
754
|
+
const headless = options?.headless ?? getHeadlessMode();
|
|
755
|
+
log2(`Launching browser (headless: ${headless}, data: ${dataDir})`);
|
|
756
|
+
try {
|
|
757
|
+
browserContext = await chromium.launchPersistentContext(dataDir, {
|
|
758
|
+
headless,
|
|
759
|
+
args: [...STEALTH_ARGS],
|
|
760
|
+
userAgent: REALISTIC_USER_AGENT,
|
|
761
|
+
viewport: randomViewport(),
|
|
762
|
+
locale: "en-US",
|
|
763
|
+
timezoneId: "America/New_York"
|
|
764
|
+
});
|
|
765
|
+
await applyStealthToContext(browserContext);
|
|
766
|
+
browserContext.on("close", () => {
|
|
767
|
+
log2("Browser context closed");
|
|
768
|
+
browserContext = null;
|
|
769
|
+
activePage = null;
|
|
770
|
+
});
|
|
771
|
+
log2("Browser context launched");
|
|
772
|
+
return browserContext;
|
|
773
|
+
} catch (error) {
|
|
774
|
+
browserContext = null;
|
|
775
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
776
|
+
if (message.includes("lock") || message.includes("already")) {
|
|
777
|
+
throw new ContextInUseError();
|
|
778
|
+
}
|
|
779
|
+
throw new BrowserLaunchError(`Failed to launch browser: ${message}`, {
|
|
780
|
+
cause: error
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async function getPage(options) {
|
|
785
|
+
if (activePage && !activePage.isClosed()) {
|
|
786
|
+
return activePage;
|
|
787
|
+
}
|
|
788
|
+
const context = await getBrowserContext(options);
|
|
789
|
+
const pages = context.pages();
|
|
790
|
+
activePage = pages.length > 0 ? pages[0] : await context.newPage();
|
|
791
|
+
return activePage;
|
|
792
|
+
}
|
|
793
|
+
async function closeBrowser() {
|
|
794
|
+
if (browserContext) {
|
|
795
|
+
log2("Closing browser context...");
|
|
796
|
+
try {
|
|
797
|
+
await browserContext.close();
|
|
798
|
+
} catch {
|
|
799
|
+
log2("Browser context already closed");
|
|
800
|
+
}
|
|
801
|
+
browserContext = null;
|
|
802
|
+
activePage = null;
|
|
803
|
+
log2("Browser closed");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/providers/playwright/auth.ts
|
|
808
|
+
var LOGIN_URL = `${AMAZON_BASE_URL}/ap/signin`;
|
|
809
|
+
var ACCOUNT_URL = `${AMAZON_BASE_URL}/gp/css/homepage.html`;
|
|
810
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
811
|
+
var LOGIN_POLL_INTERVAL_MS = 1e3;
|
|
812
|
+
var logger2 = createLogger("auth");
|
|
813
|
+
function log3(message) {
|
|
814
|
+
logger2.info(message);
|
|
815
|
+
}
|
|
816
|
+
function isLoginPage(page) {
|
|
817
|
+
const url = page.url();
|
|
818
|
+
return url.includes("/ap/signin") || url.includes("/ap/mfa") || url.includes("/ap/forgotpassword");
|
|
819
|
+
}
|
|
820
|
+
async function assertSessionValid(page) {
|
|
821
|
+
if (isLoginPage(page)) {
|
|
822
|
+
throw new SessionExpiredError();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function login(force) {
|
|
826
|
+
if (!force) {
|
|
827
|
+
const status = await checkSession();
|
|
828
|
+
if (status.loggedIn) {
|
|
829
|
+
return {
|
|
830
|
+
success: true,
|
|
831
|
+
message: "Already logged in. Use force: true to re-login."
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
log3("Opening headed browser for Amazon login...");
|
|
836
|
+
await closeBrowser();
|
|
837
|
+
const context = await getBrowserContext({ headless: false });
|
|
838
|
+
const pages = context.pages();
|
|
839
|
+
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
840
|
+
try {
|
|
841
|
+
await page.goto(LOGIN_URL, { waitUntil: "domcontentloaded" });
|
|
842
|
+
log3("Navigated to Amazon sign-in page. Waiting for user to complete login...");
|
|
843
|
+
const startTime = Date.now();
|
|
844
|
+
while (Date.now() - startTime < LOGIN_TIMEOUT_MS) {
|
|
845
|
+
await new Promise((resolve2) => setTimeout(resolve2, LOGIN_POLL_INTERVAL_MS));
|
|
846
|
+
if (page.isClosed()) {
|
|
847
|
+
return {
|
|
848
|
+
success: false,
|
|
849
|
+
message: "Browser was closed before login was completed."
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
if (!isLoginPage(page)) {
|
|
853
|
+
log3("Login detected \u2014 user navigated away from sign-in page");
|
|
854
|
+
return {
|
|
855
|
+
success: true,
|
|
856
|
+
message: "Successfully logged in to Amazon."
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
success: false,
|
|
862
|
+
message: "Login timed out after 5 minutes. Please try again."
|
|
863
|
+
};
|
|
864
|
+
} catch (error) {
|
|
865
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
866
|
+
log3(`Login error: ${message}`);
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
message: `Login failed: ${message}`
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function checkSession() {
|
|
874
|
+
try {
|
|
875
|
+
const page = await getPage();
|
|
876
|
+
await page.goto(ACCOUNT_URL, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
877
|
+
if (isLoginPage(page)) {
|
|
878
|
+
return {
|
|
879
|
+
loggedIn: false,
|
|
880
|
+
message: "Not logged in. Please call amazon-login to authenticate."
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
loggedIn: true,
|
|
885
|
+
message: "Logged in to Amazon."
|
|
886
|
+
};
|
|
887
|
+
} catch {
|
|
888
|
+
return {
|
|
889
|
+
loggedIn: false,
|
|
890
|
+
message: "Unable to verify session. Please call amazon-login to authenticate."
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/tools/auth.ts
|
|
896
|
+
function registerAuthTools(server2) {
|
|
897
|
+
server2.tool(
|
|
898
|
+
"amazon-login",
|
|
899
|
+
"Log in to Amazon. Opens a headed browser window for you to enter your credentials. Supports 2FA. Call this before using any other Amazon tools.",
|
|
900
|
+
{
|
|
901
|
+
force: z7.boolean().optional().describe("Force re-login even if a session already exists")
|
|
902
|
+
},
|
|
903
|
+
async ({ force }) => {
|
|
904
|
+
const result = await login(force);
|
|
905
|
+
return {
|
|
906
|
+
content: [{ type: "text", text: result.message }],
|
|
907
|
+
isError: !result.success
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
server2.tool(
|
|
912
|
+
"amazon-session-status",
|
|
913
|
+
"Check if you are currently logged in to Amazon and whether the session is valid.",
|
|
914
|
+
{},
|
|
915
|
+
async () => {
|
|
916
|
+
const status = await checkSession();
|
|
917
|
+
return {
|
|
918
|
+
content: [
|
|
919
|
+
{
|
|
920
|
+
type: "text",
|
|
921
|
+
text: status.message
|
|
922
|
+
}
|
|
923
|
+
]
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// src/server.ts
|
|
930
|
+
function createServer(provider2) {
|
|
931
|
+
const server2 = new McpServer({
|
|
932
|
+
name: "amazon-shopping-mcp",
|
|
933
|
+
version: "0.1.0"
|
|
934
|
+
});
|
|
935
|
+
registerAuthTools(server2);
|
|
936
|
+
registerSearchTools(server2, provider2);
|
|
937
|
+
registerProductTools(server2, provider2);
|
|
938
|
+
registerReviewTools(server2, provider2);
|
|
939
|
+
registerCartTools(server2, provider2);
|
|
940
|
+
registerOrderTools(server2, provider2);
|
|
941
|
+
registerPurchaseTools(server2, provider2);
|
|
942
|
+
return server2;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/providers/playwright/scraper.ts
|
|
946
|
+
import * as cheerio from "cheerio";
|
|
947
|
+
|
|
948
|
+
// src/providers/types.ts
|
|
949
|
+
import { z as z8 } from "zod";
|
|
950
|
+
var ProductSchema = z8.object({
|
|
951
|
+
asin: z8.string().min(1),
|
|
952
|
+
title: z8.string().min(1),
|
|
953
|
+
price: z8.number().positive().nullable(),
|
|
954
|
+
currency: z8.string().min(1),
|
|
955
|
+
rating: z8.number().min(0).max(5).nullable(),
|
|
956
|
+
reviewCount: z8.number().int().nonnegative(),
|
|
957
|
+
brand: z8.string().nullable(),
|
|
958
|
+
isPrime: z8.boolean(),
|
|
959
|
+
imageUrl: z8.string().url().nullable(),
|
|
960
|
+
url: z8.string().url()
|
|
961
|
+
});
|
|
962
|
+
var ProductDetailsSchema = z8.object({
|
|
963
|
+
asin: z8.string().min(1),
|
|
964
|
+
title: z8.string().min(1),
|
|
965
|
+
price: z8.number().positive().nullable(),
|
|
966
|
+
currency: z8.string().min(1),
|
|
967
|
+
rating: z8.number().min(0).max(5).nullable(),
|
|
968
|
+
reviewCount: z8.number().int().nonnegative(),
|
|
969
|
+
brand: z8.string().nullable(),
|
|
970
|
+
isPrime: z8.boolean(),
|
|
971
|
+
description: z8.string(),
|
|
972
|
+
features: z8.array(z8.string()),
|
|
973
|
+
imageUrls: z8.array(z8.string().url()),
|
|
974
|
+
availability: z8.string(),
|
|
975
|
+
url: z8.string().url()
|
|
976
|
+
});
|
|
977
|
+
var ReviewSchema = z8.object({
|
|
978
|
+
id: z8.string().min(1),
|
|
979
|
+
author: z8.string().min(1),
|
|
980
|
+
rating: z8.number().int().min(1).max(5),
|
|
981
|
+
title: z8.string(),
|
|
982
|
+
body: z8.string(),
|
|
983
|
+
date: z8.string().min(1),
|
|
984
|
+
verified: z8.boolean()
|
|
985
|
+
});
|
|
986
|
+
var ReviewSummarySchema = z8.object({
|
|
987
|
+
averageRating: z8.number().min(0).max(5),
|
|
988
|
+
totalCount: z8.number().int().nonnegative(),
|
|
989
|
+
starDistribution: z8.object({
|
|
990
|
+
1: z8.number().int().nonnegative(),
|
|
991
|
+
2: z8.number().int().nonnegative(),
|
|
992
|
+
3: z8.number().int().nonnegative(),
|
|
993
|
+
4: z8.number().int().nonnegative(),
|
|
994
|
+
5: z8.number().int().nonnegative()
|
|
995
|
+
})
|
|
996
|
+
});
|
|
997
|
+
var CartItemSchema = z8.object({
|
|
998
|
+
asin: z8.string().min(1),
|
|
999
|
+
title: z8.string().min(1),
|
|
1000
|
+
price: z8.number().positive().nullable(),
|
|
1001
|
+
quantity: z8.number().int().positive()
|
|
1002
|
+
});
|
|
1003
|
+
var CartSchema = z8.object({
|
|
1004
|
+
items: z8.array(CartItemSchema),
|
|
1005
|
+
subtotal: z8.number().nonnegative().nullable(),
|
|
1006
|
+
currency: z8.string().min(1)
|
|
1007
|
+
});
|
|
1008
|
+
var OrderItemSchema = z8.object({
|
|
1009
|
+
asin: z8.string().min(1),
|
|
1010
|
+
title: z8.string().min(1),
|
|
1011
|
+
price: z8.number().positive().nullable(),
|
|
1012
|
+
quantity: z8.number().int().positive(),
|
|
1013
|
+
deliveryStatus: z8.string().optional()
|
|
1014
|
+
});
|
|
1015
|
+
var OrderSchema = z8.object({
|
|
1016
|
+
orderId: z8.string().min(1),
|
|
1017
|
+
date: z8.string().min(1),
|
|
1018
|
+
total: z8.number().nonnegative(),
|
|
1019
|
+
currency: z8.string().min(1),
|
|
1020
|
+
status: z8.string().min(1),
|
|
1021
|
+
itemCount: z8.number().int().nonnegative().optional(),
|
|
1022
|
+
items: z8.array(OrderItemSchema)
|
|
1023
|
+
});
|
|
1024
|
+
var OrderHistoryParamsSchema = z8.object({
|
|
1025
|
+
limit: z8.number().int().min(1).max(50).optional().default(10),
|
|
1026
|
+
page: z8.number().int().min(1).optional().default(1),
|
|
1027
|
+
startDate: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").optional(),
|
|
1028
|
+
endDate: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format").optional()
|
|
1029
|
+
});
|
|
1030
|
+
var OrderHistoryResponseSchema = z8.object({
|
|
1031
|
+
orders: z8.array(OrderSchema),
|
|
1032
|
+
hasMore: z8.boolean(),
|
|
1033
|
+
nextPage: z8.number().int().positive().nullable()
|
|
1034
|
+
});
|
|
1035
|
+
var PurchaseResultSchema = z8.object({
|
|
1036
|
+
success: z8.boolean(),
|
|
1037
|
+
orderId: z8.string().min(1).nullable(),
|
|
1038
|
+
message: z8.string(),
|
|
1039
|
+
orderTotal: z8.string().nullable()
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// src/providers/playwright/scraper.ts
|
|
1043
|
+
var logger3 = createLogger("scraper");
|
|
1044
|
+
function log4(message) {
|
|
1045
|
+
logger3.info(message);
|
|
1046
|
+
}
|
|
1047
|
+
function parsePrice(priceText) {
|
|
1048
|
+
const cleaned = priceText.replace(/[^0-9.]/g, "");
|
|
1049
|
+
const value = parseFloat(cleaned);
|
|
1050
|
+
return Number.isNaN(value) ? null : value;
|
|
1051
|
+
}
|
|
1052
|
+
function parseRating(ratingText) {
|
|
1053
|
+
const match = ratingText.match(/([\d.]+)\s*out of\s*5/);
|
|
1054
|
+
if (!match) return null;
|
|
1055
|
+
const value = parseFloat(match[1]);
|
|
1056
|
+
return Number.isNaN(value) ? null : value;
|
|
1057
|
+
}
|
|
1058
|
+
function parseReviewCount(countText) {
|
|
1059
|
+
const cleaned = countText.replace(/[^0-9]/g, "");
|
|
1060
|
+
const value = parseInt(cleaned, 10);
|
|
1061
|
+
return Number.isNaN(value) ? 0 : value;
|
|
1062
|
+
}
|
|
1063
|
+
function parseSearchResults(html) {
|
|
1064
|
+
const $ = cheerio.load(html);
|
|
1065
|
+
const products = [];
|
|
1066
|
+
const resultCards = $('[data-component-type="s-search-result"]');
|
|
1067
|
+
if (resultCards.length === 0) {
|
|
1068
|
+
log4("No search result cards found in HTML");
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
resultCards.each((_index, element) => {
|
|
1072
|
+
try {
|
|
1073
|
+
const card = $(element);
|
|
1074
|
+
const asin = card.attr("data-asin");
|
|
1075
|
+
if (!asin || asin.trim() === "") return;
|
|
1076
|
+
const title = card.find("h2 a span").text().trim() || card.find("h2 span").text().trim();
|
|
1077
|
+
if (!title) return;
|
|
1078
|
+
const href = card.find("h2 a").attr("href") ?? card.find('[data-cy="title-recipe"] a').attr("href") ?? card.find("h2").closest("a").attr("href") ?? "";
|
|
1079
|
+
const url = href.startsWith("http") ? href : `https://www.amazon.com${href}`;
|
|
1080
|
+
const priceText = card.find(".a-price .a-offscreen").first().text().trim();
|
|
1081
|
+
const price = priceText ? parsePrice(priceText) : null;
|
|
1082
|
+
const currency = "USD";
|
|
1083
|
+
const ratingText = card.find("i.a-icon-star-small span.a-icon-alt").first().text().trim() || card.find("i.a-icon-star-mini span").first().text().trim() || card.find('[data-cy="reviews-ratings-slot"]').text().trim();
|
|
1084
|
+
const rating = ratingText ? parseRating(ratingText) : null;
|
|
1085
|
+
let reviewCountText = card.find('[data-cy="reviews-ratings-count"]').text().trim() || card.find("a.s-underline-link-text span.s-underline-text").text().trim();
|
|
1086
|
+
if (!reviewCountText) {
|
|
1087
|
+
card.find('[data-cy="reviews-block"] span').each((_i, el) => {
|
|
1088
|
+
const t = $(el).text().trim();
|
|
1089
|
+
if (t.match(/^\([\d.,]+[KkMm]?\)$/)) {
|
|
1090
|
+
reviewCountText = t;
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const reviewCount = reviewCountText ? parseReviewCount(reviewCountText) : 0;
|
|
1096
|
+
const brandText = card.find(".a-row .a-size-base-plus").first().text().trim() || card.find(".a-row .a-size-base.s-underline-text").first().text().trim();
|
|
1097
|
+
const brand = brandText || null;
|
|
1098
|
+
const isPrime = card.find("i.a-icon-prime").length > 0 || card.find('[data-cy="delivery-recipe"]').text().includes("FREE delivery");
|
|
1099
|
+
const imageUrl = card.find("img.s-image").attr("src") ?? null;
|
|
1100
|
+
const rawProduct = {
|
|
1101
|
+
asin,
|
|
1102
|
+
title,
|
|
1103
|
+
price,
|
|
1104
|
+
currency,
|
|
1105
|
+
rating,
|
|
1106
|
+
reviewCount,
|
|
1107
|
+
brand,
|
|
1108
|
+
isPrime,
|
|
1109
|
+
imageUrl,
|
|
1110
|
+
url
|
|
1111
|
+
};
|
|
1112
|
+
const parsed = ProductSchema.safeParse(rawProduct);
|
|
1113
|
+
if (parsed.success) {
|
|
1114
|
+
products.push(parsed.data);
|
|
1115
|
+
} else {
|
|
1116
|
+
log4(`Skipping product ${asin}: validation failed \u2014 ${parsed.error.message}`);
|
|
1117
|
+
}
|
|
1118
|
+
} catch {
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
log4(`Parsed ${products.length} products from ${resultCards.length} cards`);
|
|
1122
|
+
return products;
|
|
1123
|
+
}
|
|
1124
|
+
function extractBrand($) {
|
|
1125
|
+
const bylineText = $("#bylineInfo").text().trim();
|
|
1126
|
+
if (bylineText) {
|
|
1127
|
+
const visitMatch = bylineText.match(/Visit the (.+?) Store/);
|
|
1128
|
+
if (visitMatch) return visitMatch[1];
|
|
1129
|
+
const brandMatch = bylineText.match(/Brand:\s*(.+)/);
|
|
1130
|
+
if (brandMatch) return brandMatch[1].trim();
|
|
1131
|
+
return bylineText;
|
|
1132
|
+
}
|
|
1133
|
+
const brandLink = $("a#brand").text().trim();
|
|
1134
|
+
return brandLink || null;
|
|
1135
|
+
}
|
|
1136
|
+
function parseProductDetails(html) {
|
|
1137
|
+
const $ = cheerio.load(html);
|
|
1138
|
+
const asin = $('input[name="ASIN"]').val()?.toString() ?? $('link[rel="canonical"]').attr("href")?.match(/\/dp\/([A-Z0-9]+)/)?.[1] ?? "";
|
|
1139
|
+
if (!asin) {
|
|
1140
|
+
log4("No ASIN found in product page HTML");
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
const title = $("#productTitle").text().trim();
|
|
1144
|
+
if (!title) {
|
|
1145
|
+
log4("No product title found in HTML");
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
const priceText = $(".a-price .a-offscreen").first().text().trim();
|
|
1149
|
+
const price = priceText ? parsePrice(priceText) : null;
|
|
1150
|
+
const currency = "USD";
|
|
1151
|
+
const ratingText = $("#acrPopover span.a-icon-alt").first().text().trim();
|
|
1152
|
+
const rating = ratingText ? parseRating(ratingText) : null;
|
|
1153
|
+
const reviewCountText = $("#acrCustomerReviewText").text().trim();
|
|
1154
|
+
const reviewCount = reviewCountText ? parseReviewCount(reviewCountText) : 0;
|
|
1155
|
+
const brand = extractBrand($);
|
|
1156
|
+
const isPrime = $("i.a-icon-prime").length > 0 || $("#mir-layout-DELIVERY_BLOCK, #deliveryBlockMessage").text().includes("FREE delivery");
|
|
1157
|
+
const description = $("#productDescription p").text().trim() || "";
|
|
1158
|
+
const features = [];
|
|
1159
|
+
$("#feature-bullets ul li span.a-list-item").each((_i, el) => {
|
|
1160
|
+
const text = $(el).text().trim();
|
|
1161
|
+
if (text) features.push(text);
|
|
1162
|
+
});
|
|
1163
|
+
const imageUrls = [];
|
|
1164
|
+
const primaryImage = $("#landingImage").attr("src") ?? $("#imgTagWrapperId img").attr("src");
|
|
1165
|
+
if (primaryImage) imageUrls.push(primaryImage);
|
|
1166
|
+
$("#altImages ul li img").each((_i, el) => {
|
|
1167
|
+
const src = $(el).attr("src");
|
|
1168
|
+
if (src && !imageUrls.includes(src) && src.includes("/images/I/")) {
|
|
1169
|
+
imageUrls.push(src);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
const availability = $("#availability span").first().text().trim() || "Unknown";
|
|
1173
|
+
const url = $('link[rel="canonical"]').attr("href") ?? `https://www.amazon.com/dp/${asin}`;
|
|
1174
|
+
const raw = {
|
|
1175
|
+
asin,
|
|
1176
|
+
title,
|
|
1177
|
+
price,
|
|
1178
|
+
currency,
|
|
1179
|
+
rating,
|
|
1180
|
+
reviewCount,
|
|
1181
|
+
brand,
|
|
1182
|
+
isPrime,
|
|
1183
|
+
description,
|
|
1184
|
+
features,
|
|
1185
|
+
imageUrls,
|
|
1186
|
+
availability,
|
|
1187
|
+
url
|
|
1188
|
+
};
|
|
1189
|
+
const parsed = ProductDetailsSchema.safeParse(raw);
|
|
1190
|
+
if (!parsed.success) {
|
|
1191
|
+
log4(`Product details validation failed: ${parsed.error.message}`);
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
log4(`Parsed product details for ${asin}`);
|
|
1195
|
+
return parsed.data;
|
|
1196
|
+
}
|
|
1197
|
+
function parseReviews(html) {
|
|
1198
|
+
const $ = cheerio.load(html);
|
|
1199
|
+
const ratingOutOfText = $('span[data-hook="rating-out-of-text"]').text().trim();
|
|
1200
|
+
const averageRating = ratingOutOfText ? parseRating(ratingOutOfText) ?? 0 : 0;
|
|
1201
|
+
const totalCountText = $('div[data-hook="total-review-count"] span').text().trim();
|
|
1202
|
+
const totalCount = totalCountText ? parseReviewCount(totalCountText) : 0;
|
|
1203
|
+
const starDistribution = {
|
|
1204
|
+
1: 0,
|
|
1205
|
+
2: 0,
|
|
1206
|
+
3: 0,
|
|
1207
|
+
4: 0,
|
|
1208
|
+
5: 0
|
|
1209
|
+
};
|
|
1210
|
+
$("#histogramTable a[aria-label]").each((_i, el) => {
|
|
1211
|
+
const label = $(el).attr("aria-label") ?? "";
|
|
1212
|
+
const match = label.match(/(\d)\s*stars?\s*represent\s*(\d+)%/);
|
|
1213
|
+
if (match) {
|
|
1214
|
+
const star = parseInt(match[1], 10);
|
|
1215
|
+
if (star >= 1 && star <= 5) {
|
|
1216
|
+
starDistribution[star] = parseInt(match[2], 10);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
if (starDistribution[1] === 0 && starDistribution[5] === 0) {
|
|
1221
|
+
$("#histogramTable tbody tr").each((_i, el) => {
|
|
1222
|
+
const row = $(el);
|
|
1223
|
+
const starText = row.find("td.a-text-right a").text().trim();
|
|
1224
|
+
const percentText = row.find("td.a-text-right span.a-size-base").text().trim();
|
|
1225
|
+
const starMatch = starText.match(/(\d)\s*star/);
|
|
1226
|
+
const percentMatch = percentText.match(/(\d+)%/);
|
|
1227
|
+
if (starMatch && percentMatch) {
|
|
1228
|
+
const star = parseInt(starMatch[1], 10);
|
|
1229
|
+
if (star >= 1 && star <= 5) {
|
|
1230
|
+
starDistribution[star] = parseInt(percentMatch[1], 10);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
const summary = { averageRating, totalCount, starDistribution };
|
|
1236
|
+
const reviews = [];
|
|
1237
|
+
$('[data-hook="review"]').each((_i, el) => {
|
|
1238
|
+
try {
|
|
1239
|
+
const reviewEl = $(el);
|
|
1240
|
+
const id = reviewEl.attr("id") ?? "";
|
|
1241
|
+
if (!id) return;
|
|
1242
|
+
const author = reviewEl.find("span.a-profile-name").first().text().trim();
|
|
1243
|
+
const ratingAlt = reviewEl.find('i[data-hook="review-star-rating"] span.a-icon-alt').text().trim();
|
|
1244
|
+
const rating = ratingAlt ? parseRating(ratingAlt) ?? 0 : 0;
|
|
1245
|
+
const titleAnchor = reviewEl.find('a[data-hook="review-title"]');
|
|
1246
|
+
let title = titleAnchor.find("> span:not(.a-letter-space)").text().trim();
|
|
1247
|
+
if (!title) {
|
|
1248
|
+
title = titleAnchor.find("span").first().text().trim();
|
|
1249
|
+
}
|
|
1250
|
+
const body = reviewEl.find('span[data-hook="review-body"] span').text().trim();
|
|
1251
|
+
const date = reviewEl.find('span[data-hook="review-date"]').text().trim();
|
|
1252
|
+
const verified = reviewEl.find('span[data-hook="avp-badge"]').length > 0;
|
|
1253
|
+
const raw = { id, author, rating, title, body, date, verified };
|
|
1254
|
+
const parsed = ReviewSchema.safeParse(raw);
|
|
1255
|
+
if (parsed.success) {
|
|
1256
|
+
reviews.push(parsed.data);
|
|
1257
|
+
} else {
|
|
1258
|
+
log4(`Skipping review ${id}: validation failed \u2014 ${parsed.error.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
} catch {
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
log4(`Parsed ${reviews.length} reviews, summary: ${averageRating}/5 (${totalCount} total)`);
|
|
1264
|
+
return { reviews, summary };
|
|
1265
|
+
}
|
|
1266
|
+
function parseCart(html) {
|
|
1267
|
+
const $ = cheerio.load(html);
|
|
1268
|
+
const items = [];
|
|
1269
|
+
$(".sc-list-item[data-asin]").each((_index, element) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const item = $(element);
|
|
1272
|
+
const asin = item.attr("data-asin");
|
|
1273
|
+
if (!asin || asin.trim() === "") return;
|
|
1274
|
+
const title = item.find(".sc-product-title").text().trim() || item.find(".sc-product-link").text().trim();
|
|
1275
|
+
if (!title) {
|
|
1276
|
+
log4(`Skipping cart item ${asin}: no title found`);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const priceText = item.find(".sc-product-price .a-offscreen").first().text().trim();
|
|
1280
|
+
const price = priceText ? parsePrice(priceText) : null;
|
|
1281
|
+
let quantity = 1;
|
|
1282
|
+
const selectedOption = item.find('select[name="quantity"] option[selected]');
|
|
1283
|
+
if (selectedOption.length > 0) {
|
|
1284
|
+
quantity = parseInt(selectedOption.attr("value") ?? selectedOption.text().trim(), 10);
|
|
1285
|
+
} else {
|
|
1286
|
+
const quantityInput = item.find('input[name="quantity"]');
|
|
1287
|
+
if (quantityInput.length > 0) {
|
|
1288
|
+
quantity = parseInt(quantityInput.attr("value") ?? "1", 10);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (Number.isNaN(quantity) || quantity < 1) quantity = 1;
|
|
1292
|
+
const rawItem = { asin, title, price, quantity };
|
|
1293
|
+
const parsed2 = CartItemSchema.safeParse(rawItem);
|
|
1294
|
+
if (parsed2.success) {
|
|
1295
|
+
items.push(parsed2.data);
|
|
1296
|
+
} else {
|
|
1297
|
+
log4(`Skipping cart item ${asin}: validation failed \u2014 ${parsed2.error.message}`);
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
const subtotalText = $("#sc-subtotal-amount-activecart").text().trim();
|
|
1303
|
+
const subtotal = subtotalText ? parsePrice(subtotalText) : null;
|
|
1304
|
+
const currency = "USD";
|
|
1305
|
+
const rawCart = { items, subtotal, currency };
|
|
1306
|
+
const parsed = CartSchema.safeParse(rawCart);
|
|
1307
|
+
if (!parsed.success) {
|
|
1308
|
+
log4(`Cart validation failed: ${parsed.error.message}`);
|
|
1309
|
+
return { items: [], subtotal: null, currency: "USD" };
|
|
1310
|
+
}
|
|
1311
|
+
log4(`Parsed ${items.length} cart items, subtotal: ${subtotal ?? "N/A"}`);
|
|
1312
|
+
return parsed.data;
|
|
1313
|
+
}
|
|
1314
|
+
function parseOrderDate(dateText) {
|
|
1315
|
+
const months = {
|
|
1316
|
+
January: "01",
|
|
1317
|
+
February: "02",
|
|
1318
|
+
March: "03",
|
|
1319
|
+
April: "04",
|
|
1320
|
+
May: "05",
|
|
1321
|
+
June: "06",
|
|
1322
|
+
July: "07",
|
|
1323
|
+
August: "08",
|
|
1324
|
+
September: "09",
|
|
1325
|
+
October: "10",
|
|
1326
|
+
November: "11",
|
|
1327
|
+
December: "12"
|
|
1328
|
+
};
|
|
1329
|
+
const match = dateText.match(/(\w+)\s+(\d+),?\s+(\d{4})/);
|
|
1330
|
+
if (match) {
|
|
1331
|
+
const month = months[match[1]] ?? "01";
|
|
1332
|
+
const day = match[2].padStart(2, "0");
|
|
1333
|
+
const year = match[3];
|
|
1334
|
+
return `${year}-${month}-${day}`;
|
|
1335
|
+
}
|
|
1336
|
+
return dateText;
|
|
1337
|
+
}
|
|
1338
|
+
function parseOrderHistory(html) {
|
|
1339
|
+
const $ = cheerio.load(html);
|
|
1340
|
+
const orders = [];
|
|
1341
|
+
$(".order-card.js-order-card").each((_index, element) => {
|
|
1342
|
+
try {
|
|
1343
|
+
const orderEl = $(element);
|
|
1344
|
+
const orderId = orderEl.find('.yohtmlc-order-id span[dir="ltr"]').text().trim();
|
|
1345
|
+
if (!orderId || orderId.trim() === "") return;
|
|
1346
|
+
const dateText = orderEl.find(".order-header .a-size-base.a-color-secondary.aok-break-word").first().text().trim();
|
|
1347
|
+
const date = parseOrderDate(dateText);
|
|
1348
|
+
let total = 0;
|
|
1349
|
+
orderEl.find(".order-header__header-list-item").each((_i, item) => {
|
|
1350
|
+
const $item = $(item);
|
|
1351
|
+
const text = $item.text();
|
|
1352
|
+
if (text.includes("Total")) {
|
|
1353
|
+
const priceMatch = text.match(/\$[\d,]+\.?\d*/);
|
|
1354
|
+
if (priceMatch) {
|
|
1355
|
+
total = parsePrice(priceMatch[0]) ?? 0;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
const currency = "USD";
|
|
1360
|
+
const status = orderEl.find(".delivery-box__primary-text").first().text().trim() || "Unknown";
|
|
1361
|
+
const items = [];
|
|
1362
|
+
const seenAsins = /* @__PURE__ */ new Set();
|
|
1363
|
+
const itemLinks = orderEl.find('a[href*="/gp/product/"], a[href*="/dp/"]');
|
|
1364
|
+
itemLinks.each((_i, link) => {
|
|
1365
|
+
try {
|
|
1366
|
+
const $link = $(link);
|
|
1367
|
+
const href = $link.attr("href") || "";
|
|
1368
|
+
const asinMatch = href.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/);
|
|
1369
|
+
if (!asinMatch) return;
|
|
1370
|
+
const asin = asinMatch[1];
|
|
1371
|
+
if (seenAsins.has(asin)) return;
|
|
1372
|
+
seenAsins.add(asin);
|
|
1373
|
+
let title = $link.text().trim();
|
|
1374
|
+
if (!title || title.length < 3) {
|
|
1375
|
+
title = $link.find("img").attr("alt") || "";
|
|
1376
|
+
}
|
|
1377
|
+
if (!title || title.length < 3) return;
|
|
1378
|
+
const price = null;
|
|
1379
|
+
const quantity = 1;
|
|
1380
|
+
const rawItem = { asin, title, price, quantity };
|
|
1381
|
+
const parsed2 = OrderItemSchema.safeParse(rawItem);
|
|
1382
|
+
if (parsed2.success) {
|
|
1383
|
+
items.push(parsed2.data);
|
|
1384
|
+
}
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
const itemCount = items.length || void 0;
|
|
1389
|
+
const rawOrder = {
|
|
1390
|
+
orderId,
|
|
1391
|
+
date,
|
|
1392
|
+
total,
|
|
1393
|
+
currency,
|
|
1394
|
+
status,
|
|
1395
|
+
itemCount,
|
|
1396
|
+
items
|
|
1397
|
+
};
|
|
1398
|
+
const parsed = OrderSchema.safeParse(rawOrder);
|
|
1399
|
+
if (parsed.success) {
|
|
1400
|
+
orders.push(parsed.data);
|
|
1401
|
+
} else {
|
|
1402
|
+
log4(`Skipping order ${orderId}: validation failed \u2014 ${parsed.error.message}`);
|
|
1403
|
+
}
|
|
1404
|
+
} catch {
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
log4(`Parsed ${orders.length} orders from order history`);
|
|
1408
|
+
return orders;
|
|
1409
|
+
}
|
|
1410
|
+
function parseOrderDetails(html) {
|
|
1411
|
+
const $ = cheerio.load(html);
|
|
1412
|
+
let orderId = "";
|
|
1413
|
+
$("span").each((_i, el) => {
|
|
1414
|
+
const text = $(el).text().trim();
|
|
1415
|
+
if (/^\d{3}-\d{7}-\d{7}$/.test(text)) {
|
|
1416
|
+
orderId = text;
|
|
1417
|
+
return false;
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
if (!orderId) {
|
|
1421
|
+
log4("No order ID found in order details HTML");
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
let date = "";
|
|
1425
|
+
$("span").each((_i, el) => {
|
|
1426
|
+
const text = $(el).text().trim();
|
|
1427
|
+
if (text === "Order placed") {
|
|
1428
|
+
const parent = $(el).parent();
|
|
1429
|
+
const dateSpan = parent.find("span").filter((_j, s) => {
|
|
1430
|
+
const t = $(s).text().trim();
|
|
1431
|
+
return /\w+\s+\d+,\s+\d{4}/.test(t);
|
|
1432
|
+
}).first();
|
|
1433
|
+
if (dateSpan.length) {
|
|
1434
|
+
date = parseOrderDate(dateSpan.text().trim());
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
if (!date) {
|
|
1440
|
+
const bodyText = $("body").text();
|
|
1441
|
+
const dateMatch = bodyText.match(
|
|
1442
|
+
/(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},\s+\d{4}/
|
|
1443
|
+
);
|
|
1444
|
+
if (dateMatch) {
|
|
1445
|
+
date = parseOrderDate(dateMatch[0]);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
let total = 0;
|
|
1449
|
+
const allText = $("body").text();
|
|
1450
|
+
const grandTotalMatch = allText.match(/Grand Total[:\s]*\$?([\d,]+\.?\d*)/i);
|
|
1451
|
+
if (grandTotalMatch) {
|
|
1452
|
+
total = parsePrice(`$${grandTotalMatch[1]}`) ?? 0;
|
|
1453
|
+
} else {
|
|
1454
|
+
const orderTotalMatch = allText.match(/Order Total[:\s]*\$?([\d,]+\.?\d*)/i);
|
|
1455
|
+
if (orderTotalMatch) {
|
|
1456
|
+
total = parsePrice(`$${orderTotalMatch[1]}`) ?? 0;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const currency = "USD";
|
|
1460
|
+
let status = "Unknown";
|
|
1461
|
+
const deliveredEl = $(".a-text-bold").filter((_i, el) => {
|
|
1462
|
+
return $(el).text().trim().toLowerCase().includes("delivered");
|
|
1463
|
+
}).first();
|
|
1464
|
+
if (deliveredEl.length) {
|
|
1465
|
+
const nextSpan = deliveredEl.next("span");
|
|
1466
|
+
status = `Delivered ${nextSpan.text().trim()}`.trim();
|
|
1467
|
+
}
|
|
1468
|
+
const items = [];
|
|
1469
|
+
const seenAsins = /* @__PURE__ */ new Set();
|
|
1470
|
+
$('a[href*="/dp/"]').each((_index, element) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const link = $(element);
|
|
1473
|
+
const href = link.attr("href") || "";
|
|
1474
|
+
const asinMatch = href.match(/\/dp\/([A-Z0-9]{10})/);
|
|
1475
|
+
if (!asinMatch) return;
|
|
1476
|
+
const asin = asinMatch[1];
|
|
1477
|
+
if (seenAsins.has(asin)) return;
|
|
1478
|
+
seenAsins.add(asin);
|
|
1479
|
+
let title = link.text().trim();
|
|
1480
|
+
if (!title || title.length < 3) {
|
|
1481
|
+
title = link.find("img").attr("alt") || "";
|
|
1482
|
+
}
|
|
1483
|
+
if (!title || title.length < 3) return;
|
|
1484
|
+
let price = null;
|
|
1485
|
+
const container = link.closest(".a-row, .a-section, div").parent();
|
|
1486
|
+
const priceEl = container.find(".a-offscreen").first();
|
|
1487
|
+
if (priceEl.length) {
|
|
1488
|
+
const priceText = priceEl.text().trim();
|
|
1489
|
+
if (priceText.startsWith("$")) {
|
|
1490
|
+
price = parsePrice(priceText);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const quantity = 1;
|
|
1494
|
+
const rawItem = { asin, title, price, quantity };
|
|
1495
|
+
const parsed2 = OrderItemSchema.safeParse(rawItem);
|
|
1496
|
+
if (parsed2.success) {
|
|
1497
|
+
items.push(parsed2.data);
|
|
1498
|
+
} else {
|
|
1499
|
+
log4(`Skipping order item ${asin}: validation failed \u2014 ${parsed2.error.message}`);
|
|
1500
|
+
}
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
const itemCount = items.length;
|
|
1505
|
+
const rawOrder = {
|
|
1506
|
+
orderId,
|
|
1507
|
+
date,
|
|
1508
|
+
total,
|
|
1509
|
+
currency,
|
|
1510
|
+
status,
|
|
1511
|
+
itemCount,
|
|
1512
|
+
items
|
|
1513
|
+
};
|
|
1514
|
+
const parsed = OrderSchema.safeParse(rawOrder);
|
|
1515
|
+
if (!parsed.success) {
|
|
1516
|
+
log4(`Order details validation failed: ${parsed.error.message}`);
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
log4(`Parsed order details for ${orderId} with ${items.length} items`);
|
|
1520
|
+
return parsed.data;
|
|
1521
|
+
}
|
|
1522
|
+
function parseCartForCheckout(html) {
|
|
1523
|
+
const $ = cheerio.load(html);
|
|
1524
|
+
const emptyCartText = $("h1, h2, .a-section").text().toLowerCase();
|
|
1525
|
+
if (emptyCartText.includes("your cart is empty") || emptyCartText.includes("your amazon cart is empty") || emptyCartText.includes("your shopping cart is empty")) {
|
|
1526
|
+
log4("Cart is empty");
|
|
1527
|
+
return { isEmpty: true, itemCount: 0 };
|
|
1528
|
+
}
|
|
1529
|
+
const itemCount = $(".sc-list-item[data-asin]").length;
|
|
1530
|
+
log4(`Cart has ${itemCount} items`);
|
|
1531
|
+
return { isEmpty: itemCount === 0, itemCount };
|
|
1532
|
+
}
|
|
1533
|
+
function parseCheckoutPage(html) {
|
|
1534
|
+
const $ = cheerio.load(html);
|
|
1535
|
+
const errorAlert = $(".a-alert-error, .a-box.a-alert-error").text().toLowerCase();
|
|
1536
|
+
const warningAlert = $(".a-alert-warning, .a-box.a-alert-warning").text().toLowerCase();
|
|
1537
|
+
const pageText = $("body").text().toLowerCase();
|
|
1538
|
+
if (errorAlert.includes("payment") || errorAlert.includes("declined") || pageText.includes("payment method was declined") || pageText.includes("payment was declined")) {
|
|
1539
|
+
log4("Checkout error: payment declined");
|
|
1540
|
+
return {
|
|
1541
|
+
summary: null,
|
|
1542
|
+
error: {
|
|
1543
|
+
type: "payment_declined",
|
|
1544
|
+
message: "Your payment method was declined. Please update your payment method or use a different one."
|
|
1545
|
+
},
|
|
1546
|
+
hasPlaceOrderButton: false
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
if (warningAlert.includes("out of stock") || warningAlert.includes("no longer available") || pageText.includes("no longer available") || pageText.includes("currently unavailable")) {
|
|
1550
|
+
const affectedItems = [];
|
|
1551
|
+
$(".unavailable-item, [data-asin]").filter((_i, el) => {
|
|
1552
|
+
const text = $(el).text().toLowerCase();
|
|
1553
|
+
return text.includes("unavailable") || text.includes("out of stock");
|
|
1554
|
+
}).each((_i, el) => {
|
|
1555
|
+
const title = $(el).find(".item-title, .item-name").text().trim();
|
|
1556
|
+
if (title) affectedItems.push(title);
|
|
1557
|
+
});
|
|
1558
|
+
log4(`Checkout error: out of stock (${affectedItems.length} items)`);
|
|
1559
|
+
return {
|
|
1560
|
+
summary: null,
|
|
1561
|
+
error: {
|
|
1562
|
+
type: "out_of_stock",
|
|
1563
|
+
message: "Some items in your cart are no longer available.",
|
|
1564
|
+
affectedItems: affectedItems.length > 0 ? affectedItems : void 0
|
|
1565
|
+
},
|
|
1566
|
+
hasPlaceOrderButton: false
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
if (errorAlert.includes("address") || pageText.includes("address is not valid") || pageText.includes("unable to ship")) {
|
|
1570
|
+
log4("Checkout error: address invalid");
|
|
1571
|
+
return {
|
|
1572
|
+
summary: null,
|
|
1573
|
+
error: {
|
|
1574
|
+
type: "address_invalid",
|
|
1575
|
+
message: "The shipping address is not valid or we cannot ship to this location."
|
|
1576
|
+
},
|
|
1577
|
+
hasPlaceOrderButton: false
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
const items = [];
|
|
1581
|
+
$(".checkout-item, .order-summary-content .checkout-item, [data-asin]").each((_i, el) => {
|
|
1582
|
+
const $item = $(el);
|
|
1583
|
+
const asin = $item.attr("data-asin") ?? "";
|
|
1584
|
+
if (!asin) return;
|
|
1585
|
+
const title = $item.find(".item-title, .item-name, .sc-product-title").text().trim() || $item.find("a").first().text().trim();
|
|
1586
|
+
const price = $item.find(".item-price, .a-price .a-offscreen").first().text().trim();
|
|
1587
|
+
const qtyText = $item.find(".item-quantity, [name='quantity']").text().trim();
|
|
1588
|
+
const qtyMatch = qtyText.match(/\d+/);
|
|
1589
|
+
const quantity = qtyMatch ? parseInt(qtyMatch[0], 10) : 1;
|
|
1590
|
+
if (title) {
|
|
1591
|
+
items.push({ asin, title, price, quantity });
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
const getText = (selector) => $(selector).text().trim();
|
|
1595
|
+
const findTotal = (label) => {
|
|
1596
|
+
let value = "";
|
|
1597
|
+
$(".order-totals div, .a-row").each((_i, el) => {
|
|
1598
|
+
const text = $(el).text();
|
|
1599
|
+
if (text.toLowerCase().includes(label.toLowerCase())) {
|
|
1600
|
+
const priceMatch = text.match(/\$[\d,]+\.?\d*/);
|
|
1601
|
+
if (priceMatch) {
|
|
1602
|
+
value = priceMatch[0];
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
return value;
|
|
1608
|
+
};
|
|
1609
|
+
const subtotal = findTotal("items") || findTotal("subtotal") || "";
|
|
1610
|
+
const shipping = findTotal("shipping") || "$0.00";
|
|
1611
|
+
const tax = findTotal("tax") || "$0.00";
|
|
1612
|
+
const total = getText(".grand-total-price") || getText(".order-total-value") || findTotal("order total") || findTotal("grand total") || "";
|
|
1613
|
+
const hasPlaceOrderButton = $('input[name="placeYourOrder1"], #submitOrderButtonId, [name*="placeOrder"]').length > 0;
|
|
1614
|
+
if (items.length === 0 && !total) {
|
|
1615
|
+
log4("Could not parse checkout summary");
|
|
1616
|
+
return {
|
|
1617
|
+
summary: null,
|
|
1618
|
+
error: { type: "unknown", message: "Could not parse checkout page" },
|
|
1619
|
+
hasPlaceOrderButton
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
const summary = {
|
|
1623
|
+
items,
|
|
1624
|
+
subtotal,
|
|
1625
|
+
shipping,
|
|
1626
|
+
tax,
|
|
1627
|
+
total
|
|
1628
|
+
};
|
|
1629
|
+
log4(`Parsed checkout: ${items.length} items, total ${total}`);
|
|
1630
|
+
return { summary, error: null, hasPlaceOrderButton };
|
|
1631
|
+
}
|
|
1632
|
+
function parseOrderConfirmation(html) {
|
|
1633
|
+
const $ = cheerio.load(html);
|
|
1634
|
+
const successAlert = $(".a-alert-success, .a-box.a-alert-success").length > 0;
|
|
1635
|
+
const thankYouText = $("body").text().toLowerCase();
|
|
1636
|
+
const hasThankYou = thankYouText.includes("thanks") || thankYouText.includes("order placed") || thankYouText.includes("order confirmed");
|
|
1637
|
+
if (!successAlert && !hasThankYou) {
|
|
1638
|
+
log4("Order confirmation page does not show success");
|
|
1639
|
+
const raw2 = {
|
|
1640
|
+
success: false,
|
|
1641
|
+
orderId: null,
|
|
1642
|
+
message: "Order placement failed - confirmation not found",
|
|
1643
|
+
orderTotal: null
|
|
1644
|
+
};
|
|
1645
|
+
const parsed2 = PurchaseResultSchema.safeParse(raw2);
|
|
1646
|
+
return parsed2.success ? parsed2.data : raw2;
|
|
1647
|
+
}
|
|
1648
|
+
let orderId = null;
|
|
1649
|
+
const orderIdEl = $(".order-id, #orderDetails .order-id, bdi.order-id").first();
|
|
1650
|
+
if (orderIdEl.length) {
|
|
1651
|
+
orderId = orderIdEl.text().trim();
|
|
1652
|
+
}
|
|
1653
|
+
if (!orderId) {
|
|
1654
|
+
$("bdi").each((_i, el) => {
|
|
1655
|
+
const text = $(el).text().trim();
|
|
1656
|
+
if (/^\d{3}-\d{7}-\d{7}$/.test(text)) {
|
|
1657
|
+
orderId = text;
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
if (!orderId) {
|
|
1663
|
+
const orderMatch = $("body").text().match(/Order\s*#?\s*(\d{3}-\d{7}-\d{7})/i);
|
|
1664
|
+
if (orderMatch) {
|
|
1665
|
+
orderId = orderMatch[1];
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
let orderTotal = null;
|
|
1669
|
+
const totalEl = $(".order-total-value, .order-total").first();
|
|
1670
|
+
if (totalEl.length) {
|
|
1671
|
+
orderTotal = totalEl.text().trim();
|
|
1672
|
+
}
|
|
1673
|
+
if (!orderTotal) {
|
|
1674
|
+
const totalMatch = $("body").text().match(/Order total[:\s]*(\$[\d,]+\.?\d*)/i);
|
|
1675
|
+
if (totalMatch) {
|
|
1676
|
+
orderTotal = totalMatch[1];
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
const message = orderId ? `Order placed successfully. Order ID: ${orderId}` : "Order placed successfully, but order ID could not be extracted.";
|
|
1680
|
+
const raw = {
|
|
1681
|
+
success: true,
|
|
1682
|
+
orderId,
|
|
1683
|
+
message,
|
|
1684
|
+
orderTotal
|
|
1685
|
+
};
|
|
1686
|
+
const parsed = PurchaseResultSchema.safeParse(raw);
|
|
1687
|
+
if (!parsed.success) {
|
|
1688
|
+
log4(`Order confirmation validation failed: ${parsed.error.message}`);
|
|
1689
|
+
return raw;
|
|
1690
|
+
}
|
|
1691
|
+
log4(`Parsed order confirmation: ${orderId}, total ${orderTotal}`);
|
|
1692
|
+
return parsed.data;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/providers/serpapi.ts
|
|
1696
|
+
import { getJson } from "serpapi";
|
|
1697
|
+
function log5(message) {
|
|
1698
|
+
console.error(`[serpapi] ${message}`);
|
|
1699
|
+
}
|
|
1700
|
+
function parsePrice2(raw) {
|
|
1701
|
+
if (!raw) return { price: null, currency: "USD" };
|
|
1702
|
+
const match = raw.match(/([£€$])?([\d,]+\.?\d*)/);
|
|
1703
|
+
if (!match) return { price: null, currency: "USD" };
|
|
1704
|
+
const currencyMap = { $: "USD", "\xA3": "GBP", "\u20AC": "EUR" };
|
|
1705
|
+
return {
|
|
1706
|
+
price: parseFloat(match[2].replace(/,/g, "")),
|
|
1707
|
+
currency: currencyMap[match[1]] ?? "USD"
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
function createSerpApiClient(apiKey) {
|
|
1711
|
+
return {
|
|
1712
|
+
async searchProducts(query, category, maxResults = 20) {
|
|
1713
|
+
log5(`Searching for "${query}"${category ? ` in ${category}` : ""}`);
|
|
1714
|
+
try {
|
|
1715
|
+
const params = {
|
|
1716
|
+
engine: "amazon",
|
|
1717
|
+
amazon_domain: "amazon.com",
|
|
1718
|
+
search_term: query,
|
|
1719
|
+
api_key: apiKey
|
|
1720
|
+
};
|
|
1721
|
+
if (category) {
|
|
1722
|
+
params.amazon_url = `https://www.amazon.com/s?k=${encodeURIComponent(query)}&i=${encodeURIComponent(category)}`;
|
|
1723
|
+
}
|
|
1724
|
+
const json = await getJson(params);
|
|
1725
|
+
const results = json.organic_results ?? [];
|
|
1726
|
+
const products = results.slice(0, maxResults).map((r) => {
|
|
1727
|
+
const { price, currency } = parsePrice2(r.price?.raw ?? r.price?.value?.toString());
|
|
1728
|
+
return {
|
|
1729
|
+
asin: r.asin ?? "",
|
|
1730
|
+
title: r.title ?? "",
|
|
1731
|
+
price,
|
|
1732
|
+
currency,
|
|
1733
|
+
rating: r.rating ?? null,
|
|
1734
|
+
reviewCount: r.reviews ?? 0,
|
|
1735
|
+
brand: r.brand ?? null,
|
|
1736
|
+
isPrime: r.is_prime ?? false,
|
|
1737
|
+
imageUrl: r.thumbnail ?? null,
|
|
1738
|
+
url: r.link ?? `https://www.amazon.com/dp/${r.asin}`
|
|
1739
|
+
};
|
|
1740
|
+
});
|
|
1741
|
+
log5(`Found ${products.length} products`);
|
|
1742
|
+
return products;
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1745
|
+
log5(`Search error: ${message}`);
|
|
1746
|
+
return [];
|
|
1747
|
+
}
|
|
1748
|
+
},
|
|
1749
|
+
async getProductDetails(asin) {
|
|
1750
|
+
log5(`Fetching product details for ${asin}`);
|
|
1751
|
+
try {
|
|
1752
|
+
const json = await getJson({
|
|
1753
|
+
engine: "amazon_product",
|
|
1754
|
+
asin,
|
|
1755
|
+
amazon_domain: "amazon.com",
|
|
1756
|
+
api_key: apiKey
|
|
1757
|
+
});
|
|
1758
|
+
const p = json.product_results ?? json.product_result;
|
|
1759
|
+
if (!p) {
|
|
1760
|
+
log5(`No product result for ${asin}`);
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
const { price, currency } = parsePrice2(p.price?.raw ?? p.price?.value?.toString());
|
|
1764
|
+
const details = {
|
|
1765
|
+
asin: p.asin ?? asin,
|
|
1766
|
+
title: p.title ?? "",
|
|
1767
|
+
price,
|
|
1768
|
+
currency,
|
|
1769
|
+
rating: p.rating ?? null,
|
|
1770
|
+
reviewCount: p.reviews_total ?? p.reviews ?? 0,
|
|
1771
|
+
brand: p.brand ?? null,
|
|
1772
|
+
isPrime: p.is_prime ?? false,
|
|
1773
|
+
description: json.product_description?.text ?? p.description ?? "",
|
|
1774
|
+
features: json.about_item?.map((item) => item.text ?? item) ?? p.feature_bullets ?? [],
|
|
1775
|
+
imageUrls: p.images?.map((img) => img.link ?? img) ?? [],
|
|
1776
|
+
availability: p.availability?.raw ?? p.availability ?? "Unknown",
|
|
1777
|
+
url: p.link ?? `https://www.amazon.com/dp/${asin}`
|
|
1778
|
+
};
|
|
1779
|
+
log5(`Got details for "${details.title}"`);
|
|
1780
|
+
return details;
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1783
|
+
log5(`Product details error: ${message}`);
|
|
1784
|
+
return null;
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
async getReviews(asin, maxReviews, _sortBy, _filterByStars) {
|
|
1788
|
+
log5(`Fetching reviews for ${asin}`);
|
|
1789
|
+
try {
|
|
1790
|
+
const json = await getJson({
|
|
1791
|
+
engine: "amazon_product",
|
|
1792
|
+
asin,
|
|
1793
|
+
amazon_domain: "amazon.com",
|
|
1794
|
+
api_key: apiKey
|
|
1795
|
+
});
|
|
1796
|
+
const reviewsInfo = json.reviews_information ?? {};
|
|
1797
|
+
const rawReviews = reviewsInfo.authors_reviews ?? [];
|
|
1798
|
+
if (rawReviews.length === 0) {
|
|
1799
|
+
log5(`No reviews found for ${asin}`);
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
const reviews = rawReviews.slice(0, maxReviews ?? rawReviews.length).map((r, i) => ({
|
|
1803
|
+
id: r.id ?? `serpapi-${asin}-${i}`,
|
|
1804
|
+
author: r.author ?? "Unknown",
|
|
1805
|
+
rating: r.rating ?? 0,
|
|
1806
|
+
title: r.title ?? "",
|
|
1807
|
+
body: r.text ?? r.body ?? "",
|
|
1808
|
+
date: r.date ?? "",
|
|
1809
|
+
verified: r.verified_purchase ?? false
|
|
1810
|
+
}));
|
|
1811
|
+
const customerReviews = reviewsInfo.summary?.customer_reviews ?? {};
|
|
1812
|
+
const product = json.product_results ?? {};
|
|
1813
|
+
const starDist = {
|
|
1814
|
+
1: customerReviews["1 star"] ?? 0,
|
|
1815
|
+
2: customerReviews["2 star"] ?? 0,
|
|
1816
|
+
3: customerReviews["3 star"] ?? 0,
|
|
1817
|
+
4: customerReviews["4 star"] ?? 0,
|
|
1818
|
+
5: customerReviews["5 star"] ?? 0
|
|
1819
|
+
};
|
|
1820
|
+
const totalFromStars = starDist[1] + starDist[2] + starDist[3] + starDist[4] + starDist[5];
|
|
1821
|
+
const totalCount = product.reviews_total ?? (totalFromStars > 0 ? totalFromStars : 0);
|
|
1822
|
+
const summary = {
|
|
1823
|
+
averageRating: product.rating ?? 0,
|
|
1824
|
+
totalCount,
|
|
1825
|
+
starDistribution: starDist
|
|
1826
|
+
};
|
|
1827
|
+
log5(`Got ${reviews.length} reviews (avg ${summary.averageRating}/5)`);
|
|
1828
|
+
return { reviews, summary };
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1831
|
+
log5(`Reviews error: ${message}`);
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/cache/index.ts
|
|
1839
|
+
import { createHash } from "crypto";
|
|
1840
|
+
import {
|
|
1841
|
+
existsSync,
|
|
1842
|
+
readFileSync,
|
|
1843
|
+
writeFileSync,
|
|
1844
|
+
unlinkSync,
|
|
1845
|
+
mkdirSync as mkdirSync2,
|
|
1846
|
+
readdirSync
|
|
1847
|
+
} from "fs";
|
|
1848
|
+
import { join as join2 } from "path";
|
|
1849
|
+
var DEFAULT_TTL_SECONDS = 600;
|
|
1850
|
+
function getCacheDir() {
|
|
1851
|
+
return join2(getDataDir(), "cache");
|
|
1852
|
+
}
|
|
1853
|
+
function getCacheFilePath(key) {
|
|
1854
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
1855
|
+
return join2(getCacheDir(), `${hash}.json`);
|
|
1856
|
+
}
|
|
1857
|
+
function getCached(key, _options) {
|
|
1858
|
+
const filePath = getCacheFilePath(key);
|
|
1859
|
+
if (!existsSync(filePath)) {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1864
|
+
const entry = JSON.parse(content);
|
|
1865
|
+
if (Date.now() > entry.expiresAt) {
|
|
1866
|
+
try {
|
|
1867
|
+
unlinkSync(filePath);
|
|
1868
|
+
} catch {
|
|
1869
|
+
}
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
return entry.value;
|
|
1873
|
+
} catch {
|
|
1874
|
+
try {
|
|
1875
|
+
unlinkSync(filePath);
|
|
1876
|
+
} catch {
|
|
1877
|
+
}
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function setCache(key, value, options) {
|
|
1882
|
+
const cacheDir = getCacheDir();
|
|
1883
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
1884
|
+
const ttlSeconds = options?.ttlSeconds ?? getDefaultTTL();
|
|
1885
|
+
const filePath = getCacheFilePath(key);
|
|
1886
|
+
const entry = {
|
|
1887
|
+
value,
|
|
1888
|
+
expiresAt: Date.now() + ttlSeconds * 1e3,
|
|
1889
|
+
metadata: {
|
|
1890
|
+
namespace: options?.namespace,
|
|
1891
|
+
createdAt: Date.now()
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
writeFileSync(filePath, JSON.stringify(entry), "utf-8");
|
|
1895
|
+
}
|
|
1896
|
+
function getDefaultTTL() {
|
|
1897
|
+
const envTTL = process.env.AMAZON_MCP_CACHE_TTL;
|
|
1898
|
+
if (envTTL) {
|
|
1899
|
+
const parsed = parseInt(envTTL, 10);
|
|
1900
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
1901
|
+
return parsed;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return DEFAULT_TTL_SECONDS;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/providers/playwright/captcha.ts
|
|
1908
|
+
var logger4 = createLogger("captcha");
|
|
1909
|
+
var CAPTCHA_PATTERNS = [
|
|
1910
|
+
// Amazon text CAPTCHA prompts (visible text)
|
|
1911
|
+
/enter the characters you see/i,
|
|
1912
|
+
/type the characters/i,
|
|
1913
|
+
/sorry,?\s*we just need to make sure you'?re not a robot/i,
|
|
1914
|
+
/to continue, please type the characters/i,
|
|
1915
|
+
// Amazon CAPTCHA form elements (specific IDs/classes)
|
|
1916
|
+
/id="auth-captcha-image"/i,
|
|
1917
|
+
/id="auth-captcha-guess"/i,
|
|
1918
|
+
/id="captchacharacters"/i,
|
|
1919
|
+
// reCAPTCHA iframe/div (specific attributes)
|
|
1920
|
+
/class="g-recaptcha"/i,
|
|
1921
|
+
/data-sitekey=/i
|
|
1922
|
+
];
|
|
1923
|
+
var ELEMENT_PATTERNS = [
|
|
1924
|
+
"#auth-captcha-image",
|
|
1925
|
+
"#auth-captcha-guess",
|
|
1926
|
+
"#captchacharacters"
|
|
1927
|
+
];
|
|
1928
|
+
function detectCaptcha(html) {
|
|
1929
|
+
if (!html || html.length < 50) {
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
const lowerHtml = html.toLowerCase();
|
|
1933
|
+
for (const pattern of CAPTCHA_PATTERNS) {
|
|
1934
|
+
if (pattern.test(lowerHtml)) {
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
for (const selector of ELEMENT_PATTERNS) {
|
|
1939
|
+
const selectorParts = selector.replace(/[[\]#.]/g, " ").split(/\s+/).filter((p) => p.length > 3);
|
|
1940
|
+
const matchCount = selectorParts.filter(
|
|
1941
|
+
(part) => lowerHtml.includes(part.toLowerCase())
|
|
1942
|
+
).length;
|
|
1943
|
+
if (matchCount >= Math.ceil(selectorParts.length * 0.5)) {
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
async function detectCaptchaOnPage(page) {
|
|
1950
|
+
const html = await page.content();
|
|
1951
|
+
return detectCaptcha(html);
|
|
1952
|
+
}
|
|
1953
|
+
async function handleCaptcha(currentUrl) {
|
|
1954
|
+
logger4.info("CAPTCHA detected - switching to headed browser mode");
|
|
1955
|
+
await closeBrowser();
|
|
1956
|
+
const page = await getPage({ headless: false });
|
|
1957
|
+
logger4.info("Headed browser opened - please solve the CAPTCHA in the browser window");
|
|
1958
|
+
await page.goto(currentUrl, { waitUntil: "domcontentloaded" });
|
|
1959
|
+
const resolved = await waitForCaptchaResolution(page);
|
|
1960
|
+
if (!resolved) {
|
|
1961
|
+
throw new CaptchaRequiredError("CAPTCHA timeout. The CAPTCHA was not solved within 5 minutes.");
|
|
1962
|
+
}
|
|
1963
|
+
logger4.info("CAPTCHA resolved - continuing operation");
|
|
1964
|
+
return page;
|
|
1965
|
+
}
|
|
1966
|
+
async function waitForCaptchaResolution(page, timeoutMs = 3e5) {
|
|
1967
|
+
const startTime = Date.now();
|
|
1968
|
+
const pollIntervalMs = 2e3;
|
|
1969
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
1970
|
+
const hasCaptcha = await detectCaptchaOnPage(page);
|
|
1971
|
+
if (!hasCaptcha) {
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollIntervalMs));
|
|
1975
|
+
}
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/utils/retry.ts
|
|
1980
|
+
var DEFAULT_OPTIONS = {
|
|
1981
|
+
maxRetries: 3,
|
|
1982
|
+
initialDelayMs: 1e3,
|
|
1983
|
+
maxDelayMs: 16e3,
|
|
1984
|
+
backoffFactor: 2,
|
|
1985
|
+
jitter: true
|
|
1986
|
+
};
|
|
1987
|
+
var TRANSIENT_ERROR_PATTERNS = [
|
|
1988
|
+
/timeout/i,
|
|
1989
|
+
/ECONNRESET/i,
|
|
1990
|
+
/ENOTFOUND/i,
|
|
1991
|
+
/ETIMEDOUT/i,
|
|
1992
|
+
/ECONNREFUSED/i,
|
|
1993
|
+
/EPIPE/i,
|
|
1994
|
+
/ENETUNREACH/i,
|
|
1995
|
+
/EHOSTUNREACH/i,
|
|
1996
|
+
/socket hang up/i,
|
|
1997
|
+
/network/i
|
|
1998
|
+
];
|
|
1999
|
+
function isTransientError(error) {
|
|
2000
|
+
const message = error.message || "";
|
|
2001
|
+
for (const pattern of TRANSIENT_ERROR_PATTERNS) {
|
|
2002
|
+
if (pattern.test(message)) {
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
async function retryWithBackoff(fn, options) {
|
|
2009
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
2010
|
+
let lastError = null;
|
|
2011
|
+
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
|
|
2012
|
+
try {
|
|
2013
|
+
return await fn();
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2016
|
+
if (!isTransientError(lastError)) {
|
|
2017
|
+
throw lastError;
|
|
2018
|
+
}
|
|
2019
|
+
if (attempt < opts.maxRetries - 1) {
|
|
2020
|
+
const delay = calculateDelay(attempt, opts);
|
|
2021
|
+
await sleep(delay);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
throw lastError;
|
|
2026
|
+
}
|
|
2027
|
+
function calculateDelay(attempt, opts) {
|
|
2028
|
+
let delay = opts.initialDelayMs * Math.pow(opts.backoffFactor, attempt);
|
|
2029
|
+
delay = Math.min(delay, opts.maxDelayMs);
|
|
2030
|
+
if (opts.jitter) {
|
|
2031
|
+
const jitterRange = delay * 0.2;
|
|
2032
|
+
delay = delay - jitterRange + Math.random() * jitterRange * 2;
|
|
2033
|
+
}
|
|
2034
|
+
return Math.round(delay);
|
|
2035
|
+
}
|
|
2036
|
+
function sleep(ms) {
|
|
2037
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// src/providers/playwright/index.ts
|
|
2041
|
+
var SCRAPE_TIMEOUT_MS = 3e4;
|
|
2042
|
+
var SEARCH_CACHE_TTL_SECONDS = 600;
|
|
2043
|
+
var PRODUCT_CACHE_TTL_SECONDS = 1800;
|
|
2044
|
+
var REVIEWS_CACHE_TTL_SECONDS = 1800;
|
|
2045
|
+
var logger5 = createLogger("provider");
|
|
2046
|
+
function log6(message) {
|
|
2047
|
+
logger5.info(message);
|
|
2048
|
+
}
|
|
2049
|
+
async function downloadImageAsBase64(page, imageUrl) {
|
|
2050
|
+
try {
|
|
2051
|
+
const response = await page.request.get(imageUrl, { timeout: 1e4 });
|
|
2052
|
+
if (!response.ok()) {
|
|
2053
|
+
log6(`Image download failed: ${response.status()} for ${imageUrl}`);
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
const buffer = await response.body();
|
|
2057
|
+
return buffer.toString("base64");
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2060
|
+
log6(`Image download error: ${message}`);
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
var PlaywrightProvider = class {
|
|
2065
|
+
serpapi;
|
|
2066
|
+
constructor(serpApiKey) {
|
|
2067
|
+
this.serpapi = serpApiKey ? createSerpApiClient(serpApiKey) : null;
|
|
2068
|
+
log6(`SerpAPI fallback: ${this.serpapi ? "configured" : "not configured"}`);
|
|
2069
|
+
}
|
|
2070
|
+
async searchProducts(query, category, maxResults) {
|
|
2071
|
+
const cacheKey = `search:${query}:${category ?? "all"}`;
|
|
2072
|
+
const cached = getCached(cacheKey, { namespace: "search" });
|
|
2073
|
+
if (cached) {
|
|
2074
|
+
log6(`Cache hit for search: ${query}`);
|
|
2075
|
+
const limit2 = maxResults ?? 20;
|
|
2076
|
+
return cached.slice(0, limit2);
|
|
2077
|
+
}
|
|
2078
|
+
const page = await getPage();
|
|
2079
|
+
await assertSessionValid(page);
|
|
2080
|
+
const searchUrl = new URL("/s", AMAZON_BASE_URL);
|
|
2081
|
+
searchUrl.searchParams.set("k", query);
|
|
2082
|
+
if (category) {
|
|
2083
|
+
searchUrl.searchParams.set("i", category);
|
|
2084
|
+
}
|
|
2085
|
+
log6(`Searching: ${searchUrl.toString()}`);
|
|
2086
|
+
await throttleNavigation();
|
|
2087
|
+
try {
|
|
2088
|
+
await retryWithBackoff(async () => {
|
|
2089
|
+
await page.goto(searchUrl.toString(), {
|
|
2090
|
+
waitUntil: "domcontentloaded",
|
|
2091
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2092
|
+
});
|
|
2093
|
+
});
|
|
2094
|
+
await simulateMouseMovement(page);
|
|
2095
|
+
await simulateScroll(page);
|
|
2096
|
+
await randomDelay(500, 1500);
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2099
|
+
throw new ScrapingError(`Failed to load search results: ${message}`, {
|
|
2100
|
+
cause: error
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
await assertSessionValid(page);
|
|
2104
|
+
const html = await page.content();
|
|
2105
|
+
if (detectCaptcha(html)) {
|
|
2106
|
+
logger5.warn("CAPTCHA detected on search page - switching to headed browser");
|
|
2107
|
+
const resolvedPage = await handleCaptcha(page.url());
|
|
2108
|
+
const freshHtml = await resolvedPage.content();
|
|
2109
|
+
if (detectCaptcha(freshHtml)) {
|
|
2110
|
+
throw new CaptchaRequiredError("CAPTCHA could not be resolved");
|
|
2111
|
+
}
|
|
2112
|
+
const products2 = parseSearchResults(freshHtml);
|
|
2113
|
+
const limit2 = maxResults ?? 20;
|
|
2114
|
+
const result2 = products2.slice(0, limit2);
|
|
2115
|
+
if (products2.length > 0) {
|
|
2116
|
+
setCache(cacheKey, products2, { ttlSeconds: SEARCH_CACHE_TTL_SECONDS, namespace: "search" });
|
|
2117
|
+
}
|
|
2118
|
+
return result2;
|
|
2119
|
+
}
|
|
2120
|
+
const products = parseSearchResults(html);
|
|
2121
|
+
const limit = maxResults ?? 20;
|
|
2122
|
+
const result = products.slice(0, limit);
|
|
2123
|
+
if (result.length === 0 && this.serpapi) {
|
|
2124
|
+
log6(`Empty search results from scraper, trying SerpAPI fallback`);
|
|
2125
|
+
const serpResult = await this.serpapi.searchProducts(query, category, maxResults);
|
|
2126
|
+
if (serpResult.length > 0) {
|
|
2127
|
+
log6(`SerpAPI fallback returned ${serpResult.length} products`);
|
|
2128
|
+
setCache(cacheKey, serpResult, {
|
|
2129
|
+
ttlSeconds: SEARCH_CACHE_TTL_SECONDS,
|
|
2130
|
+
namespace: "search"
|
|
2131
|
+
});
|
|
2132
|
+
return serpResult;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
if (products.length > 0) {
|
|
2136
|
+
setCache(cacheKey, products, { ttlSeconds: SEARCH_CACHE_TTL_SECONDS, namespace: "search" });
|
|
2137
|
+
}
|
|
2138
|
+
return result;
|
|
2139
|
+
}
|
|
2140
|
+
async getProductDetails(asin) {
|
|
2141
|
+
const cacheKey = `product:${asin}`;
|
|
2142
|
+
const cached = getCached(
|
|
2143
|
+
cacheKey,
|
|
2144
|
+
{ namespace: "product" }
|
|
2145
|
+
);
|
|
2146
|
+
if (cached) {
|
|
2147
|
+
log6(`Cache hit for product: ${asin}`);
|
|
2148
|
+
return cached;
|
|
2149
|
+
}
|
|
2150
|
+
const page = await getPage();
|
|
2151
|
+
await assertSessionValid(page);
|
|
2152
|
+
const productUrl = new URL(`/dp/${asin}`, AMAZON_BASE_URL);
|
|
2153
|
+
log6(`Fetching product details: ${productUrl.toString()}`);
|
|
2154
|
+
await throttleNavigation();
|
|
2155
|
+
try {
|
|
2156
|
+
await retryWithBackoff(async () => {
|
|
2157
|
+
await page.goto(productUrl.toString(), {
|
|
2158
|
+
waitUntil: "domcontentloaded",
|
|
2159
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2160
|
+
});
|
|
2161
|
+
});
|
|
2162
|
+
await simulateMouseMovement(page);
|
|
2163
|
+
await simulateScroll(page);
|
|
2164
|
+
await randomDelay(500, 1500);
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2167
|
+
throw new ScrapingError(`Failed to load product page: ${message}`, { cause: error });
|
|
2168
|
+
}
|
|
2169
|
+
await assertSessionValid(page);
|
|
2170
|
+
const html = await page.content();
|
|
2171
|
+
let contentHtml = html;
|
|
2172
|
+
if (detectCaptcha(html)) {
|
|
2173
|
+
logger5.warn("CAPTCHA detected on product page - switching to headed browser");
|
|
2174
|
+
const resolvedPage = await handleCaptcha(page.url());
|
|
2175
|
+
contentHtml = await resolvedPage.content();
|
|
2176
|
+
if (detectCaptcha(contentHtml)) {
|
|
2177
|
+
throw new CaptchaRequiredError("CAPTCHA could not be resolved");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
const details = parseProductDetails(contentHtml);
|
|
2181
|
+
if (!details) {
|
|
2182
|
+
if (this.serpapi) {
|
|
2183
|
+
log6(`Could not parse product details, trying SerpAPI fallback for ${asin}`);
|
|
2184
|
+
const serpDetails = await this.serpapi.getProductDetails(asin);
|
|
2185
|
+
if (serpDetails) {
|
|
2186
|
+
log6(`SerpAPI fallback returned details for "${serpDetails.title}"`);
|
|
2187
|
+
const result2 = { details: serpDetails, primaryImageBase64: null };
|
|
2188
|
+
setCache(cacheKey, result2, {
|
|
2189
|
+
ttlSeconds: PRODUCT_CACHE_TTL_SECONDS,
|
|
2190
|
+
namespace: "product"
|
|
2191
|
+
});
|
|
2192
|
+
return result2;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
throw new ScrapingError(`Could not parse product details for ASIN ${asin}`);
|
|
2196
|
+
}
|
|
2197
|
+
let primaryImageBase64 = null;
|
|
2198
|
+
if (details.imageUrls.length > 0) {
|
|
2199
|
+
primaryImageBase64 = await downloadImageAsBase64(page, details.imageUrls[0]);
|
|
2200
|
+
}
|
|
2201
|
+
const result = { details, primaryImageBase64 };
|
|
2202
|
+
setCache(cacheKey, result, { ttlSeconds: PRODUCT_CACHE_TTL_SECONDS, namespace: "product" });
|
|
2203
|
+
return result;
|
|
2204
|
+
}
|
|
2205
|
+
async getReviews(asin, maxReviews, sortBy, filterByStars) {
|
|
2206
|
+
const cacheKey = `reviews:${asin}:${sortBy ?? "default"}:${filterByStars ?? "all"}`;
|
|
2207
|
+
const cached = getCached(cacheKey, {
|
|
2208
|
+
namespace: "reviews"
|
|
2209
|
+
});
|
|
2210
|
+
if (cached) {
|
|
2211
|
+
log6(`Cache hit for reviews: ${asin}`);
|
|
2212
|
+
const limit2 = maxReviews ?? cached.reviews.length;
|
|
2213
|
+
return { reviews: cached.reviews.slice(0, limit2), summary: cached.summary };
|
|
2214
|
+
}
|
|
2215
|
+
const page = await getPage();
|
|
2216
|
+
await assertSessionValid(page);
|
|
2217
|
+
const reviewsUrl = new URL(`/product-reviews/${asin}`, AMAZON_BASE_URL);
|
|
2218
|
+
if (sortBy) {
|
|
2219
|
+
reviewsUrl.searchParams.set("sortBy", sortBy);
|
|
2220
|
+
}
|
|
2221
|
+
if (filterByStars !== void 0) {
|
|
2222
|
+
const starMap = {
|
|
2223
|
+
1: "one_star",
|
|
2224
|
+
2: "two_star",
|
|
2225
|
+
3: "three_star",
|
|
2226
|
+
4: "four_star",
|
|
2227
|
+
5: "five_star"
|
|
2228
|
+
};
|
|
2229
|
+
const filterValue = starMap[filterByStars];
|
|
2230
|
+
if (filterValue) {
|
|
2231
|
+
reviewsUrl.searchParams.set("filterByStar", filterValue);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
log6(`Fetching reviews: ${reviewsUrl.toString()}`);
|
|
2235
|
+
await throttleNavigation();
|
|
2236
|
+
try {
|
|
2237
|
+
await retryWithBackoff(async () => {
|
|
2238
|
+
await page.goto(reviewsUrl.toString(), {
|
|
2239
|
+
waitUntil: "domcontentloaded",
|
|
2240
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2241
|
+
});
|
|
2242
|
+
});
|
|
2243
|
+
await simulateMouseMovement(page);
|
|
2244
|
+
await simulateScroll(page);
|
|
2245
|
+
await randomDelay(500, 1500);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2248
|
+
throw new ScrapingError(`Failed to load reviews page: ${message}`, { cause: error });
|
|
2249
|
+
}
|
|
2250
|
+
await assertSessionValid(page);
|
|
2251
|
+
try {
|
|
2252
|
+
await page.waitForSelector('[data-hook="review"]', { timeout: 1e4 });
|
|
2253
|
+
} catch {
|
|
2254
|
+
log6("Warning: review elements did not appear within timeout");
|
|
2255
|
+
}
|
|
2256
|
+
const html = await page.content();
|
|
2257
|
+
let contentHtml = html;
|
|
2258
|
+
if (detectCaptcha(html)) {
|
|
2259
|
+
logger5.warn("CAPTCHA detected on reviews page - switching to headed browser");
|
|
2260
|
+
const resolvedPage = await handleCaptcha(page.url());
|
|
2261
|
+
contentHtml = await resolvedPage.content();
|
|
2262
|
+
if (detectCaptcha(contentHtml)) {
|
|
2263
|
+
throw new CaptchaRequiredError("CAPTCHA could not be resolved");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const { reviews, summary } = parseReviews(contentHtml);
|
|
2267
|
+
const limit = maxReviews ?? reviews.length;
|
|
2268
|
+
const result = { reviews: reviews.slice(0, limit), summary };
|
|
2269
|
+
if (result.reviews.length === 0 && this.serpapi) {
|
|
2270
|
+
log6(`Empty reviews from scraper, trying SerpAPI fallback for ${asin}`);
|
|
2271
|
+
const serpResult = await this.serpapi.getReviews(asin, maxReviews, sortBy, filterByStars);
|
|
2272
|
+
if (serpResult && serpResult.reviews.length > 0) {
|
|
2273
|
+
log6(`SerpAPI fallback returned ${serpResult.reviews.length} reviews`);
|
|
2274
|
+
setCache(cacheKey, serpResult, {
|
|
2275
|
+
ttlSeconds: REVIEWS_CACHE_TTL_SECONDS,
|
|
2276
|
+
namespace: "reviews"
|
|
2277
|
+
});
|
|
2278
|
+
return serpResult;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (reviews.length > 0) {
|
|
2282
|
+
setCache(
|
|
2283
|
+
cacheKey,
|
|
2284
|
+
{ reviews, summary },
|
|
2285
|
+
{ ttlSeconds: REVIEWS_CACHE_TTL_SECONDS, namespace: "reviews" }
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
return result;
|
|
2289
|
+
}
|
|
2290
|
+
async dismissSubscribeAndSave(page) {
|
|
2291
|
+
try {
|
|
2292
|
+
const noThanks = page.locator('text="No thanks"');
|
|
2293
|
+
if (await noThanks.isVisible({ timeout: 2e3 })) {
|
|
2294
|
+
await noThanks.click();
|
|
2295
|
+
log6("Dismissed subscribe-and-save dialog");
|
|
2296
|
+
}
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
try {
|
|
2300
|
+
const buyOnce = page.locator('input[data-action="sos-one-time-purchase"]');
|
|
2301
|
+
if (await buyOnce.isVisible({ timeout: 500 })) {
|
|
2302
|
+
await buyOnce.click();
|
|
2303
|
+
log6("Selected one-time purchase option");
|
|
2304
|
+
}
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
async dismissInsurancePrompt(page) {
|
|
2309
|
+
try {
|
|
2310
|
+
const decline = page.locator(
|
|
2311
|
+
'[data-action="decline-warranty"], button:has-text("No Thanks")'
|
|
2312
|
+
);
|
|
2313
|
+
if (await decline.first().isVisible({ timeout: 2e3 })) {
|
|
2314
|
+
await decline.first().click();
|
|
2315
|
+
log6("Dismissed insurance/warranty prompt");
|
|
2316
|
+
}
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async addToCart(asin, _quantity) {
|
|
2321
|
+
const page = await getPage();
|
|
2322
|
+
await assertSessionValid(page);
|
|
2323
|
+
const productUrl = new URL(`/dp/${asin}`, AMAZON_BASE_URL);
|
|
2324
|
+
log6(`Adding to cart: ${productUrl.toString()}`);
|
|
2325
|
+
await throttleNavigation();
|
|
2326
|
+
try {
|
|
2327
|
+
await page.goto(productUrl.toString(), {
|
|
2328
|
+
waitUntil: "domcontentloaded",
|
|
2329
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2330
|
+
});
|
|
2331
|
+
await simulateMouseMovement(page);
|
|
2332
|
+
await randomDelay(300, 800);
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2335
|
+
throw new ScrapingError(`Failed to load product page: ${message}`, { cause: error });
|
|
2336
|
+
}
|
|
2337
|
+
await assertSessionValid(page);
|
|
2338
|
+
try {
|
|
2339
|
+
const addToCartBtn = page.locator("#add-to-cart-button, #add-to-cart-button-ubb");
|
|
2340
|
+
await addToCartBtn.first().click({ timeout: 5e3 });
|
|
2341
|
+
log6("Clicked Add to Cart button");
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2344
|
+
throw new ScrapingError(`Could not find or click Add to Cart button: ${message}`, {
|
|
2345
|
+
cause: error
|
|
2346
|
+
});
|
|
2347
|
+
}
|
|
2348
|
+
await randomDelay(1e3, 2e3);
|
|
2349
|
+
await this.dismissSubscribeAndSave(page);
|
|
2350
|
+
await this.dismissInsurancePrompt(page);
|
|
2351
|
+
const cart = await this.getCart();
|
|
2352
|
+
return {
|
|
2353
|
+
success: true,
|
|
2354
|
+
message: `Added ${asin} to cart`,
|
|
2355
|
+
cart
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
async removeFromCart(asin) {
|
|
2359
|
+
const page = await getPage();
|
|
2360
|
+
await assertSessionValid(page);
|
|
2361
|
+
const cartUrl = new URL("/gp/cart/view.html", AMAZON_BASE_URL);
|
|
2362
|
+
log6(`Removing from cart: ${asin}`);
|
|
2363
|
+
await throttleNavigation();
|
|
2364
|
+
try {
|
|
2365
|
+
await page.goto(cartUrl.toString(), {
|
|
2366
|
+
waitUntil: "domcontentloaded",
|
|
2367
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2368
|
+
});
|
|
2369
|
+
await simulateMouseMovement(page);
|
|
2370
|
+
await randomDelay(300, 800);
|
|
2371
|
+
} catch (error) {
|
|
2372
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2373
|
+
throw new ScrapingError(`Failed to load cart page: ${message}`, { cause: error });
|
|
2374
|
+
}
|
|
2375
|
+
await assertSessionValid(page);
|
|
2376
|
+
try {
|
|
2377
|
+
const cartItem = page.locator(`.sc-list-item[data-asin="${asin}"]`);
|
|
2378
|
+
if (!await cartItem.isVisible({ timeout: 2e3 })) {
|
|
2379
|
+
const cart2 = await this.getCart();
|
|
2380
|
+
return {
|
|
2381
|
+
success: false,
|
|
2382
|
+
message: `Item ${asin} not in cart`,
|
|
2383
|
+
cart: cart2
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
const deleteBtn = cartItem.locator(
|
|
2387
|
+
'input[value="Delete"], [data-feature-id="item-delete-button"]'
|
|
2388
|
+
);
|
|
2389
|
+
await deleteBtn.first().click({ timeout: 5e3 });
|
|
2390
|
+
log6(`Deleted item ${asin} from cart`);
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2393
|
+
throw new ScrapingError(`Could not remove item from cart: ${message}`, { cause: error });
|
|
2394
|
+
}
|
|
2395
|
+
await randomDelay(1e3, 2e3);
|
|
2396
|
+
const cart = await this.getCart();
|
|
2397
|
+
return {
|
|
2398
|
+
success: true,
|
|
2399
|
+
message: `Removed ${asin} from cart`,
|
|
2400
|
+
cart
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
async getCart() {
|
|
2404
|
+
const page = await getPage();
|
|
2405
|
+
await assertSessionValid(page);
|
|
2406
|
+
const cartUrl = new URL("/gp/cart/view.html", AMAZON_BASE_URL);
|
|
2407
|
+
log6(`Fetching cart: ${cartUrl.toString()}`);
|
|
2408
|
+
await throttleNavigation();
|
|
2409
|
+
try {
|
|
2410
|
+
await page.goto(cartUrl.toString(), {
|
|
2411
|
+
waitUntil: "domcontentloaded",
|
|
2412
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2413
|
+
});
|
|
2414
|
+
await simulateMouseMovement(page);
|
|
2415
|
+
await simulateScroll(page);
|
|
2416
|
+
await randomDelay(500, 1500);
|
|
2417
|
+
} catch (error) {
|
|
2418
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2419
|
+
throw new ScrapingError(`Failed to load cart page: ${message}`, { cause: error });
|
|
2420
|
+
}
|
|
2421
|
+
await assertSessionValid(page);
|
|
2422
|
+
const html = await page.content();
|
|
2423
|
+
return parseCart(html);
|
|
2424
|
+
}
|
|
2425
|
+
async clearCart() {
|
|
2426
|
+
const page = await getPage();
|
|
2427
|
+
await assertSessionValid(page);
|
|
2428
|
+
log6("Clearing cart");
|
|
2429
|
+
let cart = await this.getCart();
|
|
2430
|
+
while (cart.items.length > 0) {
|
|
2431
|
+
const asin = cart.items[0].asin;
|
|
2432
|
+
log6(`Removing item ${asin} (${cart.items.length} items remaining)`);
|
|
2433
|
+
await this.removeFromCart(asin);
|
|
2434
|
+
cart = await this.getCart();
|
|
2435
|
+
}
|
|
2436
|
+
log6("Cart cleared");
|
|
2437
|
+
}
|
|
2438
|
+
async getOrderHistory(params) {
|
|
2439
|
+
const page = await getPage();
|
|
2440
|
+
log6("Loading order history page");
|
|
2441
|
+
const limit = params?.limit ?? 10;
|
|
2442
|
+
const currentPage = params?.page ?? 1;
|
|
2443
|
+
const startIndex = (currentPage - 1) * limit;
|
|
2444
|
+
let url = `${AMAZON_BASE_URL}/gp/your-account/order-history`;
|
|
2445
|
+
const urlParams = [];
|
|
2446
|
+
if (startIndex > 0) {
|
|
2447
|
+
urlParams.push(`startIndex=${startIndex}`);
|
|
2448
|
+
}
|
|
2449
|
+
if (params?.startDate || params?.endDate) {
|
|
2450
|
+
if (params.startDate && params.endDate) {
|
|
2451
|
+
urlParams.push(`startDate=${params.startDate}`);
|
|
2452
|
+
urlParams.push(`endDate=${params.endDate}`);
|
|
2453
|
+
} else if (params.startDate) {
|
|
2454
|
+
urlParams.push(`startDate=${params.startDate}`);
|
|
2455
|
+
} else if (params.endDate) {
|
|
2456
|
+
urlParams.push(`endDate=${params.endDate}`);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (urlParams.length > 0) {
|
|
2460
|
+
url += `?${urlParams.join("&")}`;
|
|
2461
|
+
}
|
|
2462
|
+
await throttleNavigation();
|
|
2463
|
+
try {
|
|
2464
|
+
await page.goto(url, {
|
|
2465
|
+
waitUntil: "domcontentloaded",
|
|
2466
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2467
|
+
});
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2470
|
+
throw new ScrapingError(`Failed to load order history page: ${message}`, { cause: error });
|
|
2471
|
+
}
|
|
2472
|
+
await assertSessionValid(page);
|
|
2473
|
+
await simulateMouseMovement(page);
|
|
2474
|
+
await randomDelay(500, 1500);
|
|
2475
|
+
await simulateScroll(page);
|
|
2476
|
+
const html = await page.content();
|
|
2477
|
+
const allOrders = parseOrderHistory(html);
|
|
2478
|
+
const orders = allOrders.slice(0, limit);
|
|
2479
|
+
const hasMore = allOrders.length > limit;
|
|
2480
|
+
const nextPage = hasMore ? currentPage + 1 : null;
|
|
2481
|
+
log6(`Retrieved ${orders.length} orders (page ${currentPage})`);
|
|
2482
|
+
return {
|
|
2483
|
+
orders,
|
|
2484
|
+
hasMore,
|
|
2485
|
+
nextPage
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
async getOrderDetails(orderId) {
|
|
2489
|
+
const page = await getPage();
|
|
2490
|
+
log6(`Loading order details for ${orderId}`);
|
|
2491
|
+
const url = `${AMAZON_BASE_URL}/gp/your-account/order-details?orderID=${orderId}`;
|
|
2492
|
+
await throttleNavigation();
|
|
2493
|
+
try {
|
|
2494
|
+
await page.goto(url, {
|
|
2495
|
+
waitUntil: "domcontentloaded",
|
|
2496
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2497
|
+
});
|
|
2498
|
+
} catch (error) {
|
|
2499
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2500
|
+
throw new ScrapingError(`Failed to load order details page: ${message}`, { cause: error });
|
|
2501
|
+
}
|
|
2502
|
+
await assertSessionValid(page);
|
|
2503
|
+
await simulateMouseMovement(page);
|
|
2504
|
+
await randomDelay(500, 1500);
|
|
2505
|
+
const html = await page.content();
|
|
2506
|
+
const order = parseOrderDetails(html);
|
|
2507
|
+
if (!order) {
|
|
2508
|
+
throw new ScrapingError(`Order ${orderId} not found or could not be parsed`);
|
|
2509
|
+
}
|
|
2510
|
+
log6(`Retrieved order details for ${orderId}`);
|
|
2511
|
+
return order;
|
|
2512
|
+
}
|
|
2513
|
+
async purchase(confirmationToken) {
|
|
2514
|
+
if (!confirmationToken || confirmationToken.trim() === "") {
|
|
2515
|
+
return {
|
|
2516
|
+
success: false,
|
|
2517
|
+
orderId: null,
|
|
2518
|
+
message: "Confirmation token is required. The AI assistant must confirm with the user before calling this tool.",
|
|
2519
|
+
orderTotal: null
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
const page = await getPage();
|
|
2523
|
+
await assertSessionValid(page);
|
|
2524
|
+
const cartUrl = new URL("/gp/cart/view.html", AMAZON_BASE_URL);
|
|
2525
|
+
log6("Starting purchase flow - checking cart");
|
|
2526
|
+
await throttleNavigation();
|
|
2527
|
+
try {
|
|
2528
|
+
await page.goto(cartUrl.toString(), {
|
|
2529
|
+
waitUntil: "domcontentloaded",
|
|
2530
|
+
timeout: SCRAPE_TIMEOUT_MS
|
|
2531
|
+
});
|
|
2532
|
+
await simulateMouseMovement(page);
|
|
2533
|
+
await randomDelay(500, 1e3);
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2536
|
+
throw new ScrapingError(`Failed to load cart page: ${message}`, { cause: error });
|
|
2537
|
+
}
|
|
2538
|
+
await assertSessionValid(page);
|
|
2539
|
+
const cartHtml = await page.content();
|
|
2540
|
+
const { isEmpty, itemCount } = parseCartForCheckout(cartHtml);
|
|
2541
|
+
if (isEmpty) {
|
|
2542
|
+
return {
|
|
2543
|
+
success: false,
|
|
2544
|
+
orderId: null,
|
|
2545
|
+
message: "Your cart is empty. Add items before attempting to purchase.",
|
|
2546
|
+
orderTotal: null
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
const cart = parseCart(cartHtml);
|
|
2550
|
+
log6(`=== PRE-PURCHASE SUMMARY ===`);
|
|
2551
|
+
log6(`Items in cart: ${itemCount}`);
|
|
2552
|
+
for (const item of cart.items) {
|
|
2553
|
+
log6(` - ${item.title} (ASIN: ${item.asin}) x${item.quantity} @ $${item.price ?? "N/A"}`);
|
|
2554
|
+
}
|
|
2555
|
+
log6(`Subtotal: $${cart.subtotal ?? "N/A"}`);
|
|
2556
|
+
log6(`=============================`);
|
|
2557
|
+
log6("Proceeding to checkout");
|
|
2558
|
+
await throttleNavigation();
|
|
2559
|
+
try {
|
|
2560
|
+
const checkoutBtn = page.locator(
|
|
2561
|
+
'#sc-buy-box-ptc-button, input[name="proceedToRetailCheckout"], [data-feature-id="proceed-to-checkout-action"]'
|
|
2562
|
+
);
|
|
2563
|
+
await checkoutBtn.first().click({ timeout: 5e3 });
|
|
2564
|
+
await randomDelay(1500, 3e3);
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
throw new ScrapingError(`Could not find or click checkout button: ${message}`, {
|
|
2568
|
+
cause: error
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
await page.waitForLoadState("domcontentloaded", { timeout: SCRAPE_TIMEOUT_MS });
|
|
2572
|
+
await assertSessionValid(page);
|
|
2573
|
+
await this.dismissSubscribeAndSave(page);
|
|
2574
|
+
await this.dismissInsurancePrompt(page);
|
|
2575
|
+
const checkoutHtml = await page.content();
|
|
2576
|
+
const checkoutResult = parseCheckoutPage(checkoutHtml);
|
|
2577
|
+
if (checkoutResult.error) {
|
|
2578
|
+
const err = checkoutResult.error;
|
|
2579
|
+
let errorMessage = err.message;
|
|
2580
|
+
if (err.type === "out_of_stock" && err.affectedItems && err.affectedItems.length > 0) {
|
|
2581
|
+
errorMessage += ` Affected items: ${err.affectedItems.join(", ")}`;
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
success: false,
|
|
2585
|
+
orderId: null,
|
|
2586
|
+
message: errorMessage,
|
|
2587
|
+
orderTotal: null
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
if (!checkoutResult.hasPlaceOrderButton) {
|
|
2591
|
+
log6("No place order button found - checkout may require additional action");
|
|
2592
|
+
return {
|
|
2593
|
+
success: false,
|
|
2594
|
+
orderId: null,
|
|
2595
|
+
message: "Checkout page is not ready. You may need to select an address or payment method on Amazon directly.",
|
|
2596
|
+
orderTotal: null
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
if (checkoutResult.summary) {
|
|
2600
|
+
log6(
|
|
2601
|
+
`Order summary: ${checkoutResult.summary.items.length} items, total ${checkoutResult.summary.total}`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
log6("Placing order");
|
|
2605
|
+
await throttleNavigation();
|
|
2606
|
+
try {
|
|
2607
|
+
const placeOrderBtn = page.locator(
|
|
2608
|
+
'input[name="placeYourOrder1"], #submitOrderButtonId input, [name*="placeOrder"]'
|
|
2609
|
+
);
|
|
2610
|
+
await placeOrderBtn.first().click({ timeout: 5e3 });
|
|
2611
|
+
await randomDelay(2e3, 4e3);
|
|
2612
|
+
} catch (error) {
|
|
2613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2614
|
+
throw new ScrapingError(`Could not find or click place order button: ${message}`, {
|
|
2615
|
+
cause: error
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
await page.waitForLoadState("domcontentloaded", { timeout: SCRAPE_TIMEOUT_MS });
|
|
2619
|
+
const confirmationHtml = await page.content();
|
|
2620
|
+
const result = parseOrderConfirmation(confirmationHtml);
|
|
2621
|
+
if (result.success) {
|
|
2622
|
+
log6(`Order placed successfully! Order ID: ${result.orderId}`);
|
|
2623
|
+
} else {
|
|
2624
|
+
log6(`Order placement failed: ${result.message}`);
|
|
2625
|
+
}
|
|
2626
|
+
return result;
|
|
2627
|
+
}
|
|
2628
|
+
};
|
|
2629
|
+
|
|
2630
|
+
// src/providers/factory.ts
|
|
2631
|
+
var SUPPORTED_PROVIDERS = ["playwright"];
|
|
2632
|
+
function log7(message) {
|
|
2633
|
+
console.error(`[provider] ${message}`);
|
|
2634
|
+
}
|
|
2635
|
+
function createProvider() {
|
|
2636
|
+
const providerName = process.env.AMAZON_MCP_PROVIDER ?? "playwright";
|
|
2637
|
+
if (!SUPPORTED_PROVIDERS.includes(providerName)) {
|
|
2638
|
+
throw new Error(
|
|
2639
|
+
`Unsupported provider: "${providerName}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
const serpApiKey = process.env.SERPAPI_API_KEY;
|
|
2643
|
+
log7(`Creating provider: ${providerName}`);
|
|
2644
|
+
log7(`SerpAPI fallback: ${serpApiKey ? "configured" : "not configured"}`);
|
|
2645
|
+
switch (providerName) {
|
|
2646
|
+
case "playwright":
|
|
2647
|
+
return new PlaywrightProvider(serpApiKey);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/index.ts
|
|
2652
|
+
var provider = createProvider();
|
|
2653
|
+
var server = createServer(provider);
|
|
2654
|
+
var transport = new StdioServerTransport();
|
|
2655
|
+
try {
|
|
2656
|
+
await ensureBrowser();
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2659
|
+
console.error(`[startup] Browser setup warning: ${message}`);
|
|
2660
|
+
}
|
|
2661
|
+
await server.connect(transport);
|
|
2662
|
+
console.error("amazon-shopping-mcp server started on stdio");
|
|
2663
|
+
async function shutdown() {
|
|
2664
|
+
console.error("[shutdown] Shutting down...");
|
|
2665
|
+
await closeBrowser();
|
|
2666
|
+
await server.close();
|
|
2667
|
+
process.exit(0);
|
|
2668
|
+
}
|
|
2669
|
+
process.on("SIGINT", shutdown);
|
|
2670
|
+
process.on("SIGTERM", shutdown);
|
|
2671
|
+
//# sourceMappingURL=index.js.map
|