good-eggs-mcp-server 0.1.2 → 0.1.4
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/package.json +1 -1
- package/shared/server.js +118 -51
- package/shared/tools.js +3 -1
- package/shared/types.d.ts +2 -0
package/package.json
CHANGED
package/shared/server.js
CHANGED
|
@@ -389,19 +389,32 @@ export class GoodEggsClient {
|
|
|
389
389
|
// Extract past order information
|
|
390
390
|
const orders = await page.evaluate(() => {
|
|
391
391
|
const orderList = [];
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const countEl = el.querySelector('[class*="count"], [class*="items"]');
|
|
392
|
+
// Good Eggs reorder page uses 'reorder-page__grid__header' class for order date headers
|
|
393
|
+
// Each header contains a <p> element with text like "Delivered Saturday, January 10th"
|
|
394
|
+
const headerElements = document.querySelectorAll('.reorder-page__grid__header');
|
|
395
|
+
headerElements.forEach((header) => {
|
|
396
|
+
const dateEl = header.querySelector('p');
|
|
398
397
|
const dateText = dateEl?.textContent?.trim();
|
|
399
398
|
if (!dateText)
|
|
400
399
|
return;
|
|
400
|
+
// Skip non-order headers like "Based on your shopping" recommendations section
|
|
401
|
+
if (!dateText.includes('Delivered'))
|
|
402
|
+
return;
|
|
403
|
+
// Extract just the date part (e.g., "Saturday, January 10th" from "Delivered Saturday, January 10th")
|
|
404
|
+
const dateMatch = dateText.match(/Delivered\s+(.+)/);
|
|
405
|
+
const cleanDate = dateMatch ? dateMatch[1] : dateText;
|
|
406
|
+
// Count the products in this order section
|
|
407
|
+
// The products follow the header in the DOM as sibling elements with js-product-link
|
|
408
|
+
let itemCount = 0;
|
|
409
|
+
let sibling = header.nextElementSibling;
|
|
410
|
+
while (sibling && !sibling.classList.contains('reorder-page__grid__header')) {
|
|
411
|
+
const productLinks = sibling.querySelectorAll('a.js-product-link');
|
|
412
|
+
itemCount += productLinks.length;
|
|
413
|
+
sibling = sibling.nextElementSibling;
|
|
414
|
+
}
|
|
401
415
|
orderList.push({
|
|
402
|
-
date:
|
|
403
|
-
|
|
404
|
-
itemCount: countEl?.textContent ? parseInt(countEl.textContent) : undefined,
|
|
416
|
+
date: cleanDate,
|
|
417
|
+
itemCount: itemCount > 0 ? itemCount : undefined,
|
|
405
418
|
});
|
|
406
419
|
});
|
|
407
420
|
return orderList;
|
|
@@ -410,59 +423,113 @@ export class GoodEggsClient {
|
|
|
410
423
|
}
|
|
411
424
|
async getPastOrderGroceries(orderDate) {
|
|
412
425
|
const page = await this.ensureBrowser();
|
|
413
|
-
// First, go to
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
426
|
+
// First, go to account orders page to find the order ID
|
|
427
|
+
// Check if we're on the orders list page (not a specific order details page)
|
|
428
|
+
const currentUrl = page.url();
|
|
429
|
+
const isOnOrdersList = currentUrl.includes('/account/orders') && !/\/account\/orders\/[^/]+/.test(currentUrl);
|
|
430
|
+
if (!isOnOrdersList) {
|
|
431
|
+
await page.goto(`${BASE_URL}/account/orders`, { waitUntil: 'domcontentloaded' });
|
|
417
432
|
await page.waitForTimeout(3000);
|
|
418
433
|
}
|
|
419
434
|
// Check if we're redirected to signin
|
|
420
435
|
if (page.url().includes('/signin')) {
|
|
421
436
|
throw new Error('Not logged in. Cannot access past orders.');
|
|
422
437
|
}
|
|
423
|
-
//
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
438
|
+
// Find the order that matches the date and get its URL
|
|
439
|
+
const orderUrl = await page.evaluate((targetDate) => {
|
|
440
|
+
// Try to find by looking at the page structure for order links
|
|
441
|
+
const orderLinks = Array.from(document.querySelectorAll('a[href*="/account/orders/"]'));
|
|
442
|
+
for (const link of orderLinks) {
|
|
443
|
+
const linkEl = link;
|
|
444
|
+
// Get the parent container to check the date
|
|
445
|
+
const container = linkEl.closest('[class*="card"], [class*="row"], div') || linkEl;
|
|
446
|
+
const text = container.textContent || '';
|
|
447
|
+
// Check if this order matches our target date
|
|
448
|
+
// Dates can be like "Saturday 1/10", "January 10th", etc.
|
|
449
|
+
if (text.includes(targetDate)) {
|
|
450
|
+
return linkEl.href;
|
|
451
|
+
}
|
|
452
|
+
// Try more flexible date matching
|
|
453
|
+
// Extract date components from targetDate and compare
|
|
454
|
+
const datePatterns = [
|
|
455
|
+
/(\d{1,2})\/(\d{1,2})/, // 1/10
|
|
456
|
+
/([A-Za-z]+)\s+(\d{1,2})/, // January 10
|
|
457
|
+
/(\d{1,2})(?:st|nd|rd|th)/, // 10th
|
|
458
|
+
];
|
|
459
|
+
for (const pattern of datePatterns) {
|
|
460
|
+
const targetMatch = targetDate.match(pattern);
|
|
461
|
+
const textMatch = text.match(pattern);
|
|
462
|
+
if (targetMatch && textMatch && targetMatch[0] === textMatch[0]) {
|
|
463
|
+
return linkEl.href;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return null;
|
|
468
|
+
}, orderDate);
|
|
469
|
+
if (!orderUrl) {
|
|
470
|
+
// Fallback: try clicking on "Order Details" for a matching date
|
|
471
|
+
const clickedUrl = await page.evaluate((targetDate) => {
|
|
472
|
+
const cards = Array.from(document.querySelectorAll('[class*="single-order"], [class*="order-summary"], div'));
|
|
473
|
+
for (const card of cards) {
|
|
474
|
+
const text = card.textContent || '';
|
|
475
|
+
if (text.includes(targetDate)) {
|
|
476
|
+
// This card contains the target date, look for Order Details link
|
|
477
|
+
const detailsLink = card.querySelector('a[href*="/account/orders/"]');
|
|
478
|
+
if (detailsLink) {
|
|
479
|
+
return detailsLink.href;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}, orderDate);
|
|
485
|
+
if (!clickedUrl) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
await page.goto(clickedUrl, { waitUntil: 'domcontentloaded' });
|
|
489
|
+
await page.waitForTimeout(2000);
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
await page.goto(orderUrl, { waitUntil: 'domcontentloaded' });
|
|
427
493
|
await page.waitForTimeout(2000);
|
|
428
494
|
}
|
|
429
|
-
//
|
|
495
|
+
// Now we're on the order details page - extract items with quantity ordered
|
|
430
496
|
const items = await page.evaluate(() => {
|
|
431
497
|
const products = [];
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
498
|
+
// Find all line items on the order details page
|
|
499
|
+
const lineItems = document.querySelectorAll('.single-order-page__line-item');
|
|
500
|
+
lineItems.forEach((item) => {
|
|
501
|
+
// Extract quantity ordered (the number on the left)
|
|
502
|
+
const quantityOrderedEl = item.querySelector('.single-order-page__line-item-quantity-value');
|
|
503
|
+
const quantityOrdered = parseInt(quantityOrderedEl?.textContent?.trim() || '1', 10) || 1;
|
|
504
|
+
// Extract product name and URL
|
|
505
|
+
const nameLink = item.querySelector('.single-order-page__line-item-details-name a');
|
|
506
|
+
const name = nameLink?.textContent?.trim() || '';
|
|
507
|
+
const url = nameLink?.href || '';
|
|
508
|
+
// Extract brand
|
|
509
|
+
const brandEl = item.querySelector('.single-order-page__line-item-details-vendor-name a');
|
|
510
|
+
const brand = brandEl?.textContent?.trim() || '';
|
|
511
|
+
// Extract unit (e.g., "1 bunch", "1 lb")
|
|
512
|
+
const unitEl = item.querySelector('.single-order-page__line-item-details-unit-quantity');
|
|
513
|
+
const unit = unitEl?.textContent?.trim() || '';
|
|
514
|
+
// Extract price
|
|
515
|
+
const priceEl = item.querySelector('.summary-item__price');
|
|
516
|
+
const price = priceEl?.textContent?.trim() || '';
|
|
517
|
+
// Extract image URL
|
|
518
|
+
const imageDiv = item.querySelector('.single-order-page__line-item-image-image');
|
|
519
|
+
const bgImage = imageDiv?.style?.backgroundImage || '';
|
|
520
|
+
const imageMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
|
|
521
|
+
const imageUrl = imageMatch ? imageMatch[1] : undefined;
|
|
522
|
+
if (name && url) {
|
|
523
|
+
products.push({
|
|
524
|
+
url,
|
|
525
|
+
name,
|
|
526
|
+
brand,
|
|
527
|
+
price,
|
|
528
|
+
quantity: unit, // unit of sale (e.g., "1 bunch")
|
|
529
|
+
quantityOrdered, // number ordered (e.g., 2)
|
|
530
|
+
imageUrl,
|
|
531
|
+
});
|
|
455
532
|
}
|
|
456
|
-
if (!name || name.length < 3)
|
|
457
|
-
return;
|
|
458
|
-
seen.add(href);
|
|
459
|
-
products.push({
|
|
460
|
-
url: href,
|
|
461
|
-
name: name,
|
|
462
|
-
brand: brandEl?.textContent?.trim() || '',
|
|
463
|
-
price: priceEl?.textContent?.trim() || '',
|
|
464
|
-
imageUrl: imgEl?.src || undefined,
|
|
465
|
-
});
|
|
466
533
|
});
|
|
467
534
|
return products;
|
|
468
535
|
});
|
package/shared/tools.js
CHANGED
|
@@ -64,6 +64,8 @@ Provide the past_order_date (from get_list_of_past_order_dates) to see what was
|
|
|
64
64
|
Returns a list of items from that order including:
|
|
65
65
|
- Product URL
|
|
66
66
|
- Product name and brand
|
|
67
|
+
- Quantity ordered (e.g., 2 if you ordered 2 of something)
|
|
68
|
+
- Unit of sale (e.g., "1 bunch", "1 lb", "15 oz")
|
|
67
69
|
- Price at time of order
|
|
68
70
|
|
|
69
71
|
Useful for reordering frequently purchased items.
|
|
@@ -441,7 +443,7 @@ export function createRegisterTools(clientFactory, getReadyClient) {
|
|
|
441
443
|
};
|
|
442
444
|
}
|
|
443
445
|
const formattedResults = results
|
|
444
|
-
.map((item, i) => `${i + 1}. **${item.name}**\n Brand: ${item.brand || 'N/A'}\n Price: ${item.price || 'N/A'}\n URL: ${item.url}`)
|
|
446
|
+
.map((item, i) => `${i + 1}. **${item.name}**\n Brand: ${item.brand || 'N/A'}\n Quantity Ordered: ${item.quantityOrdered || 1}\n Unit: ${item.quantity || 'N/A'}\n Price: ${item.price || 'N/A'}\n URL: ${item.url}`)
|
|
445
447
|
.join('\n\n');
|
|
446
448
|
return {
|
|
447
449
|
content: [
|