From bec623a5e226e4d3da3d19cab3539dac2942d281 Mon Sep 17 00:00:00 2001 From: David Vasandani Date: Wed, 23 Jul 2025 07:15:56 -0700 Subject: [PATCH 1/3] Refactor: Modularize code and add dynamic domain extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Break out monolithic amazon.ts into separate modules: - products.ts: Product search and details functions - cart.ts: Cart management functions (add, clear, get content) - orders.ts: Order history functions - Add dynamic Amazon domain extraction from cookies: - Automatically detects regional Amazon sites (amazon.co.uk, amazon.de, etc.) - Falls back to amazon.com if no cookies found - All URLs now use the detected domain instead of hardcoded values - Update all imports and test files to use new module structure - Add CLAUDE.md documentation for future Claude Code instances - Fix TypeScript diagnostic issues This refactoring improves code maintainability and enables the MCP server to work with any regional Amazon site based on the provided cookies. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 87 +++ src/amazon.addToCart.test.ts | 2 +- src/amazon.clearCart.test.ts | 2 +- src/amazon.getCartContent.test.ts | 2 +- src/amazon.getOrdersHistory.test.ts | 2 +- src/amazon.getProductDetails.test.ts | 2 +- src/amazon.searchProducts.test.ts | 2 +- src/amazon.ts | 802 --------------------------- src/cart.ts | 304 ++++++++++ src/config.ts | 49 +- src/index.ts | 4 +- src/orders.ts | 136 +++++ src/products.ts | 355 ++++++++++++ src/utils.ts | 7 + 14 files changed, 946 insertions(+), 810 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 src/amazon.ts create mode 100644 src/cart.ts create mode 100644 src/orders.ts create mode 100644 src/products.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..14b5d02 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is an Amazon MCP (Model Context Protocol) Server that enables AI assistants to interact with Amazon services through web scraping. The server uses Puppeteer for browser automation and exposes Amazon functionality through MCP tools. + +## Development Commands + +```bash +# Install dependencies (use -D flag for Puppeteer) +npm install -D + +# Build TypeScript to JavaScript +npm run build + +# Clean mock HTML files +npm run clean +``` + +## Architecture + +### Core Components + +- **MCP Server** (`src/index.ts`): Defines and exposes tools via the MCP protocol +- **Amazon Scraper** (`src/amazon.ts`): Contains all Amazon interaction logic using Puppeteer and Cheerio +- **Configuration** (`src/config.ts`): Manages server settings and paths +- **Browser Utils** (`src/utils.ts`): Helper functions for Puppeteer browser automation + +### Key Dependencies + +- `@modelcontextprotocol/sdk`: MCP framework +- `puppeteer`: Browser automation +- `cheerio`: HTML parsing +- `zod`: Schema validation + +### Authentication + +The server requires Amazon cookies for authentication: +1. Export cookies from browser using a cookie export extension +2. Save to `amazonCookies.json` in project root +3. Format: Array of cookie objects with standard properties + +## Important Implementation Details + +### Browser Automation +- Uses headless Chrome with specific flags to avoid detection +- Implements user agent spoofing +- Handles Amazon's anti-bot measures + +### Error Handling +- Detects login page redirects and throws authentication errors +- Implements retry logic for network failures +- Provides detailed error messages for debugging + +### Mock Mode +- Set `USE_MOCK_RESPONSES=true` in environment to use mock HTML files +- Mock files stored in `mock/` directory +- Useful for development and testing without hitting Amazon + +### Logging +- Server logs to `~/Library/Logs/Claude/mcp-server-amazon.log` +- Check logs for debugging authentication or scraping issues + +## MCP Tools Exposed + +1. `search-products`: Search Amazon catalog +2. `get-product-details`: Get detailed product information +3. `get-orders-history`: View past orders +4. `get-cart-content`: View current cart +5. `add-to-cart`: Add items to cart +6. `clear-cart`: Remove all items from cart +7. `perform-purchase`: Complete purchase (mock mode only) + +## Testing Approach + +No formal test suite exists. Testing is done through: +- Manual testing with Claude Desktop +- Mock mode for development +- Log analysis for debugging + +## Common Issues + +1. **Authentication failures**: Update cookies from browser +2. **Scraping failures**: Amazon HTML structure may have changed +3. **Rate limiting**: Add delays between requests if needed \ No newline at end of file diff --git a/src/amazon.addToCart.test.ts b/src/amazon.addToCart.test.ts index e0c0c33..aacba04 100644 --- a/src/amazon.addToCart.test.ts +++ b/src/amazon.addToCart.test.ts @@ -1,4 +1,4 @@ -import { addToCart } from './amazon.js' +import { addToCart } from './cart.js' import { USE_MOCKS } from './config.js' async function testAddToCart() { diff --git a/src/amazon.clearCart.test.ts b/src/amazon.clearCart.test.ts index 133afb3..51530ee 100644 --- a/src/amazon.clearCart.test.ts +++ b/src/amazon.clearCart.test.ts @@ -1,4 +1,4 @@ -import { clearCart } from './amazon.js' +import { clearCart } from './cart.js' import { USE_MOCKS } from './config.js' async function testClearCart() { diff --git a/src/amazon.getCartContent.test.ts b/src/amazon.getCartContent.test.ts index 93b9a54..0b68971 100644 --- a/src/amazon.getCartContent.test.ts +++ b/src/amazon.getCartContent.test.ts @@ -1,4 +1,4 @@ -import { getCartContent } from './amazon.js' +import { getCartContent } from './cart.js' async function main() { try { diff --git a/src/amazon.getOrdersHistory.test.ts b/src/amazon.getOrdersHistory.test.ts index dd8c4bf..5ae091b 100644 --- a/src/amazon.getOrdersHistory.test.ts +++ b/src/amazon.getOrdersHistory.test.ts @@ -1,4 +1,4 @@ -import { getOrdersHistory } from './amazon.js' +import { getOrdersHistory } from './orders.js' async function main() { try { diff --git a/src/amazon.getProductDetails.test.ts b/src/amazon.getProductDetails.test.ts index d104ad2..9372fcd 100644 --- a/src/amazon.getProductDetails.test.ts +++ b/src/amazon.getProductDetails.test.ts @@ -1,4 +1,4 @@ -import { getProductDetails } from './amazon.js' +import { getProductDetails } from './products.js' async function testGetProductDetails_regular() { console.log('\n\n--------------------------------------') diff --git a/src/amazon.searchProducts.test.ts b/src/amazon.searchProducts.test.ts index e623404..d9b5391 100644 --- a/src/amazon.searchProducts.test.ts +++ b/src/amazon.searchProducts.test.ts @@ -1,4 +1,4 @@ -import { searchProducts } from './amazon.js' +import { searchProducts } from './products.js' async function testSearchProducts() { console.log('๐Ÿงช Testing Amazon Search Products functionality...') diff --git a/src/amazon.ts b/src/amazon.ts deleted file mode 100644 index bf132b4..0000000 --- a/src/amazon.ts +++ /dev/null @@ -1,802 +0,0 @@ -import * as cheerio from 'cheerio' -import fs from 'fs' -import puppeteer from 'puppeteer' -import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS } from './config.js' -import { createBrowserAndPage, getTimestamp } from './utils.js' - -const __dirname = new URL('.', import.meta.url).pathname - -async function throwIfNotLoggedIn(page: puppeteer.Page): Promise { - const isLoginPage = (await page.$('#ap_email')) !== null || (await page.$('#signInSubmit')) !== null - if (isLoginPage) { - throw new Error('You need to be logged in to access this feature. Please log in to Amazon first and then try again.') - } -} - -// ################################## -// Start getOrdersHistory -// ################################## - -export async function getOrdersHistory() { - let html: string - if (USE_MOCKS) { - console.error('[INFO][get-orders-history] Fetching orders history from mocks') - const mockPath = `${__dirname}/../mocks/getOrdersHistory.html` - html = fs.readFileSync(mockPath, 'utf-8') - } else { - const url = 'https://www.amazon.es/-/en/gp/css/order-history' - console.error(`[INFO][get-orders-history] Fetching orders history from ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for the order cards to load (adjust selector as needed) - try { - await page.waitForSelector('.order-card, .your-orders-content-container', { timeout: 10000 }) - } catch (e) { - throw new Error( - '[INFO][get-orders-history] Could not find orders card selector. Ensure you are logged in and the orders history is accessible.' - ) - } - - if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { - // Export only the .order-card and .your-orders-content-container content to a mock file - const timestamp = getTimestamp() - const mockPath = `${__dirname}/../mocks/getOrdersHistory_${timestamp}.html` - const orderCardsHtml = await page.$$eval('.order-card, .your-orders-content-container', elements => - elements.map(el => el.outerHTML).join('\n') - ) - fs.writeFileSync(mockPath, orderCardsHtml) - console.error(`[INFO][get-orders-history] Exported order cards HTML to ${mockPath}`) - } - - // Get the HTML content after JavaScript execution - html = await page.content() - } finally { - await browser.close() - } - } - - const $ = cheerio.load(html) - const orderCards = $('.order-card') - .map((index, element) => extractOrdersHistoryPageData($, $(element))) - .get() - return orderCards -} - -function extractOrdersHistoryPageData($: cheerio.CheerioAPI, $card: cheerio.Cheerio) { - // Extract order information - const orderNumber = $card.find('.yohtmlc-order-id span').last().text().trim() - const orderDate = $card.find('.order-header__header-list-item').first().find('.a-size-base').text().trim() - const total = $card.find('.order-header__header-list-item').eq(1).find('.a-size-base').text().trim() - const status = $card.find('.delivery-box__primary-text').text().trim() - const collectionMatch = status.match(/Collected on (.+)/) - const collectionDate = collectionMatch ? collectionMatch[1] : null - - // Extract delivery address - const deliveryName = $card.find('.a-popover-preload h5').text().trim() - const deliveryAddress = $card.find('.a-popover-preload .a-row').eq(1).text().trim().replace(/\s+/g, ' ') - const deliveryCountry = $card.find('.a-popover-preload .a-row').last().text().trim() - - // Extract items - const items: { - title: string - image: string | undefined - productUrl: string | undefined - asin: string | null - returnEligible: boolean - returnDate: string | null - }[] = [] - $card.find('.item-box').each((index, element) => { - const $element = $(element) - const title = $element.find('.yohtmlc-product-title a').text().trim() - const image = $element.find('.product-image img').attr('src') - const productUrl = $element.find('.yohtmlc-product-title a').attr('href') - const returnText = $element.find('.a-size-small').text().trim() - - let asin = null - if (productUrl) { - const asinMatch = productUrl.match(/\/dp\/([A-Z0-9]{10})/) - asin = asinMatch ? asinMatch[1] : null - } - - let returnEligible = false - let returnDate = null - if (returnText.includes('Return or Replace Items')) { - returnEligible = true - const returnDateMatch = returnText.match(/until (.+)/) - returnDate = returnDateMatch ? returnDateMatch[1] : null - } - - items.push({ - title, - image, - productUrl, - asin, - returnEligible, - returnDate, - }) - }) - - return { - orderInfo: { - orderNumber, - orderDate, - total, - deliveryAddress: { - name: deliveryName, - address: deliveryAddress, - country: deliveryCountry, - }, - status, - collectionDate, - }, - items, - } -} - -// ################################## -// End getOrdersHistory -// ################################## - -// ################################## -// Start getCartContent -// ################################## - -interface CartItem { - title: string - price: string - quantity: number - image?: string - productUrl?: string - asin?: string - availability: string - isSelected: boolean -} - -interface CartContent { - isEmpty: boolean - items: CartItem[] - subtotal?: string - totalItems?: number -} - -export async function getCartContent(): Promise { - let html: string - if (USE_MOCKS) { - console.error('[INFO][get-cart-content] Fetching cart content from mocks') - const mockPath = `${__dirname}/../mocks/getCartContent.html` - html = fs.readFileSync(mockPath, 'utf-8') - } else { - const url = 'https://www.amazon.es/-/en/gp/cart/view.html?ref_=nav_cart' - console.error(`[INFO][get-cart-content] Fetching cart content from ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the cart page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for the cart content to load - try { - await page.waitForSelector('#sc-active-cart', { timeout: 10000 }) - } catch (e) { - throw new Error('[INFO][get-cart-content] Could not find cart container. Ensure you are logged in and the cart is accessible.') - } - - if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { - // Export only the `#sc-active-cart` content to a mock file - const timestamp = getTimestamp() - const mockPath = `${__dirname}/../mocks/getCartContent_${timestamp}.html` - const cartHtml = await page.$eval('#sc-active-cart', el => el.outerHTML) - fs.writeFileSync(mockPath, cartHtml) - console.error(`[INFO][get-cart-content] Exported cart container HTML to ${mockPath}`) - } - - // Get the HTML content after JavaScript execution - html = await page.content() - } finally { - await browser.close() - } - } - - const $ = cheerio.load(html) - return extractCartPageData($) -} - -function extractCartPageData($: cheerio.CheerioAPI): CartContent { - const $cartContainer = $('#sc-active-cart') - - // Check if cart is empty - const emptyCartText = $cartContainer.text() - if (emptyCartText.includes('Your Amazon Cart is empty')) { - return { - isEmpty: true, - items: [], - } - } - - // Extract cart items - const items: CartItem[] = [] - $cartContainer.find('[data-asin]').each((index, element) => { - const $item = $(element) - - // Extract basic item information - const titleElement = $item.find('a.sc-product-title').first() - const title = titleElement.find('.a-truncate-full').text().trim() - const price = $item.find('.apex-price-to-pay-value .a-offscreen').text().trim() - const quantityElement = $item.find('[data-a-selector="value"]').text().trim() - const quantity = parseInt(quantityElement) || 1 - - // Extract optional information - const image = $item.find('.sc-product-image').attr('src') - const productUrl = $item.find('.sc-product-link').attr('href') - const asin = $item.attr('data-asin') - const availability = $item.find('.sc-product-availability').text().trim() || 'Unknown' - const isSelected = $item.find('input[type="checkbox"]').is(':checked') - - console.error(`[INFO][get-cart-content] Extracted ASIN: ${asin}, Price: ${price}, Quantity: ${quantity}, item: ${title}`) - // Only add items with valid titles and prices - if (title && price) { - items.push({ - title, - price, - quantity, - image, - productUrl, - asin, - availability, - isSelected, - }) - } - }) - - // Extract subtotal information - const subtotal = - $cartContainer.find('#sc-subtotal-amount-activecart .sc-price').text().trim() || - $cartContainer.find('.sc-subtotal .sc-price').text().trim() - - const totalItemsText = $cartContainer.find('#sc-subtotal-label-activecart').text().trim() - const totalItemsMatch = totalItemsText.match(/\((\d+)\s+item/) - const totalItems = totalItemsMatch ? parseInt(totalItemsMatch[1]) : items.length - - return { - isEmpty: false, - items, - subtotal, - totalItems, - } -} - -// ################################## -// End getCartContent -// ################################## - -// ################################## -// Start addToCart -// ################################## - -export async function addToCart(asin: string): Promise<{ success: boolean; message: string }> { - if (!asin || asin.length !== 10) { - throw new Error('Invalid ASIN provided. ASIN should be a 10-character string.') - } - - const url = `https://www.amazon.es/-/en/gp/product/${asin}` - console.error(`[INFO][add-to-cart] Adding product ${asin} to cart from ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the product page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for the page to load completely - await page.waitForSelector('body', { timeout: 10000 }) - - try { - // Check for subscribe and save option using XPath - const xpath = "//div[contains(@class, 'accordion-caption')]//span[contains(text(), 'One-time purchase')]" - const element = await page.waitForSelector(`::-p-xpath(${xpath})`, { timeout: 2000 }) - if (element) { - console.error(`[INFO][add-to-cart] The item is a subscribe and save product, clicking the one-time purchase option`) - element.click() - // Wait for the page to update - await new Promise(resolve => setTimeout(resolve, 2000)) - } else { - console.error('[INFO][add-to-cart] No subscribe and save option found, proceeding to add to cart') - } - } catch (error) { - console.error(`[INFO][add-to-cart] Error checking for subscribe and save option: ${error}`) - } - - // Find and click the add to cart button - try { - await page.waitForSelector('#add-to-cart-button', { timeout: 10000 }) - await page.click('#add-to-cart-button') - console.error('[INFO][add-to-cart] Clicked add to cart button') - } catch (error) { - throw new Error(`Could not find or click the add to cart button: ${error}`) - } - - // If there is an insurance option, refuse it - try { - await page.waitForSelector('#productTitle', { timeout: 1000 }) - await page.click('#productTitle', { delay: 100 }) - await page.click('#attachSiNoCoverage', { delay: 300 }) - } catch (error) { - console.error(`[WARNING][add-to-cart] Failed to click insurance option (it may not have been presented):`, error) - } - - // Wait for the confirmation page/modal - try { - await page.waitForSelector('#sw-atc-confirmation', { timeout: 15000 }) - - // Check for success message - const confirmationText = await page.$eval('#sw-atc-confirmation', el => el.textContent || '') - - if (!confirmationText.includes('Added to basket')) { - throw new Error(`Unexpected confirmation message: ${confirmationText}`) - } - - console.error('[INFO][add-to-cart] Successfully added product to cart') - return { - success: true, - message: `Product ${asin} successfully added to cart`, - } - } catch (error) { - throw new Error(`Could not verify that the product was added to cart: ${error}`) - } - } finally { - await browser.close() - } -} - -// ################################## -// End addToCart -// ################################## - -// ################################## -// Start clearCart -// ################################## - -export async function clearCart() { - const url = 'https://www.amazon.es/-/en/gp/cart/view.html' - console.error(`[INFO][clear-cart] Clearing cart at ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the cart page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for the cart to load - await page.waitForSelector('#sc-active-cart, .sc-cart-item, .sc-empty-cart-banner', { timeout: 10000 }) - - // Find all delete buttons - const deleteButtons = await page.$$('span[data-action="delete-active"]') - - if (deleteButtons.length === 0) { - console.error('[INFO][clear-cart] No items found in cart to remove') - return { - success: true, - message: 'No items found in cart to remove', - itemsRemoved: 0, - } - } - - console.error(`[INFO][clear-cart] Found ${deleteButtons.length} items to remove`) - - let itemsRemoved = 0 - - // Click each delete button with delay - for (let i = 0; i < deleteButtons.length; i++) { - try { - // Re-query the delete buttons as DOM changes after each deletion - const currentDeleteButtons = await page.$$('span[data-action="delete-active"]') - - if (currentDeleteButtons.length === 0) { - console.error('[INFO][clear-cart] No more items to delete') - break - } - - // Click the first available delete button - await currentDeleteButtons[0].click() - itemsRemoved++ - - console.error(`[INFO][clear-cart] Removed item ${itemsRemoved}`) - - // Wait for the page to update after deletion - if (i < deleteButtons.length - 1) { - await new Promise(resolve => setTimeout(resolve, 800)) - } - } catch (error) { - console.error(`[WARNING][clear-cart] Failed to remove item ${i + 1}:`, error) - } - } - - console.error(`[INFO][clear-cart] Successfully removed ${itemsRemoved} items from cart`) - - return { - success: true, - message: `Successfully cleared cart. Removed ${itemsRemoved} items.`, - itemsRemoved, - } - } catch (error: any) { - console.error('[ERROR][clear-cart] Error clearing cart:', error) - throw new Error(`Failed to clear cart: ${error.message}`) - } finally { - await browser.close() - } -} - -// ################################## -// End clearCart -// ################################## - -// ################################## -// Start getProductDetails -// ################################## - -interface ProductDetails { - data: { - asin: string - title: string - price: string - canUseSubscribeAndSave: boolean - description: { - overview?: string - features?: string - facts?: string - brandSnapshot?: string - } - reviews: { - averageRating?: string - reviewsCount?: string - } - mainImageUrl?: string - } - mainImageBase64?: string -} - -export async function getProductDetails(asin: string): Promise { - if (!asin || asin.length !== 10) { - throw new Error('Invalid ASIN provided. ASIN should be a 10-character string.') - } - - let html: string - if (USE_MOCKS) { - console.error('[INFO][get-product-details] Fetching product details from mocks') - const mockPath = `${__dirname}/../mocks/getProductDetails.html` - html = fs.readFileSync(mockPath, 'utf-8') - } else { - const url = `https://www.amazon.es/-/en/gp/product/${asin}` - console.error(`[INFO][get-product-details] Fetching product details from ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the product page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for the product page to load - try { - await page.waitForSelector('#productTitle', { timeout: 10000 }) - } catch (e) { - throw new Error('[INFO][get-product-details] Could not find product title. The product may not exist or be accessible.') - } - - if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { - // Export the main product content to a mock file - const timestamp = getTimestamp() - const mockPath = `${__dirname}/../mocks/getProductDetails_${timestamp}.html` - const productHtml = await page.content() - fs.writeFileSync(mockPath, productHtml) - console.error(`[INFO][get-product-details] Exported product page HTML to ${mockPath}`) - } - - // Get the HTML content after JavaScript execution - html = await page.content() - } finally { - await browser.close() - } - } - - const $ = cheerio.load(html) - return extractProductDetailsPageData($, asin) -} - -async function extractProductDetailsPageData($: cheerio.CheerioAPI, asin: string): Promise { - // Extract product title - const title = $('span#productTitle').text().trim() - - // Extract price information - let price = '' - let canUseSubscribeAndSave: ProductDetails['data']['canUseSubscribeAndSave'] = false - - // Check if it's a subscribe and save product - const subscriptionPrice = $('#subscriptionPrice .a-price .a-offscreen').prop('innerText')?.trim() - if (subscriptionPrice) { - price = subscriptionPrice - canUseSubscribeAndSave = true - } else { - // Use regular price - price = $('.priceToPay').text().trim() - } - - // Extract description sections - const description: ProductDetails['data']['description'] = {} - - const overview = $('#productOverview_feature_div').prop('innerText')?.trim() - if (overview) description.overview = overview - - const features = $('#featurebullets_feature_div').prop('innerText')?.trim() - if (features) description.features = features - - const facts = $('#productFactsDesktop_feature_div').prop('innerText')?.trim() - if (facts) description.facts = facts - - const brandSnapshot = $('#brandSnapshot_feature_div').prop('innerText')?.trim() - if (brandSnapshot) description.brandSnapshot = brandSnapshot - - // Extract reviews information - const reviews: ProductDetails['data']['reviews'] = {} - - const averageRating = $('#averageCustomerReviews span.a-size-small.a-color-base').text().trim() - if (averageRating) reviews.averageRating = averageRating - - const reviewsCountElement = $('#acrCustomerReviewLink span') - const reviewsCount = reviewsCountElement.attr('aria-label') - if (reviewsCount) - reviews.reviewsCount = reviewsCount - .replace(/\s+.*$/g, '') - .replace(/,/g, '') - .trim() - - // Extract main product image - const mainImageUrl = $('#main-image-container img.a-dynamic-image').attr('src') - // Download the image and convert to base64 - let mainImageBase64: ProductDetails['mainImageBase64'] = undefined - if (mainImageUrl) { - if (USE_MOCKS) { - console.error('[INFO][get-product-details] Downloading product main image from mocks') - const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64.txt` - mainImageBase64 = fs.readFileSync(mockPath, 'utf-8') - } else { - // FIXME: This is not supported yet by Claude Desktop client!! Uncomment when they implement it - // console.error(`[INFO][get-product-details] Downloading main image from ${mainImageUrl}`) - // mainImageBase64 = await downloadImageAsBase64(mainImageUrl) - // if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { - // const timestamp = getTimestamp() - // const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64_${timestamp}.txt` - // fs.writeFileSync(mockPath, mainImageBase64) - // console.error(`[INFO][get-product-details] Exported main image base64 to ${mockPath}`) - // } - } - } - - console.error( - `[INFO][get-product-details] Extracted product: ASIN: ${asin}, ${title}, Price: ${price}, Can use subscribe and save: ${canUseSubscribeAndSave}, Reviews: ${reviews.averageRating} (${reviews.reviewsCount} reviews), Main image URL: ${mainImageUrl}` - ) - - return { - data: { - asin, - title, - price, - canUseSubscribeAndSave, - description, - reviews, - mainImageUrl, - }, - mainImageBase64, - } -} - -// ################################## -// End getProductDetails -// ################################## - -// ################################## -// Start searchProducts -// ################################## - -interface ProductSearchResult { - asin: string - title: string - isSponsored: boolean - brand?: string - price?: string - pricePerUnit?: string - description?: { - overview?: string - features?: string - facts?: string - brandSnapshot?: string - } - reviews?: { - averageRating?: string - reviewCount?: string - } - imageUrl?: string - isPrimeEligible: boolean - deliveryInfo?: string - productUrl?: string -} - -export async function searchProducts(searchTerm: string): Promise { - if (!searchTerm || searchTerm.trim().length === 0) { - throw new Error('Search term is required and cannot be empty.') - } - - let html: string - if (USE_MOCKS) { - console.error('[INFO][search-products] Fetching search results from mocks') - const mockPath = `${__dirname}/../mocks/searchProducts.html` - html = fs.readFileSync(mockPath, 'utf-8') - } else { - const url = `https://www.amazon.es/s?k=${encodeURIComponent(searchTerm)}` - console.error(`[INFO][search-products] Searching for products with term "${searchTerm}" from ${url}`) - - const { browser, page } = await createBrowserAndPage() - - try { - // Navigate to the search page - await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) - - // Handle login if needed - await throwIfNotLoggedIn(page) - - // Wait for search results to load - try { - await page.waitForSelector('.s-search-results', { timeout: 10000 }) - } catch (e) { - throw new Error( - '[INFO][search-products] Could not find search results container. The search may have failed or returned no results.' - ) - } - - if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { - // Export the search results content to a mock file - const timestamp = getTimestamp() - const searchResultsHtml = await page.$eval('.s-search-results', el => el.outerHTML) - const mockFileName = `searchProducts_${timestamp}.html` - const mockPath = `${__dirname}/../mocks/${mockFileName}` - fs.writeFileSync(mockPath, searchResultsHtml) - console.error(`[INFO][search-products] Exported search results HTML to ${mockPath}`) - } - - // Get the HTML content after JavaScript execution - html = await page.content() - } finally { - await browser.close() - } - } - - const $ = cheerio.load(html) - return extractSearchResultsPageData($, searchTerm) -} - -function extractSearchResultsPageData($: cheerio.CheerioAPI, searchTerm: string): ProductSearchResult[] { - const searchResults: ProductSearchResult[] = [] - - // Find the search results using the actual Amazon structure - const $productItems = $('[role="listitem"]') - - if ($productItems.length === 0) { - console.error('[INFO][search-products] No search results found') - return [] - } - - // Limit to first 20 items - const limitedItems = $productItems.slice(0, 20) - - console.error(`[INFO][search-products] Found ${$productItems.length} products, processing first ${limitedItems.length}`) - - limitedItems.each((index, element) => { - const $item = $(element) - - try { - const productData = extractSearchResultSingleProductData($, $item) - if (productData && productData.asin) { - searchResults.push(productData) - console.error(`[INFO][search-products] Extracted product ${index + 1}: ${productData.asin} - ${productData.title}`) - } - } catch (error) { - console.error(`[INFO][search-products] Error extracting product ${index + 1}:`, error) - } - }) - - console.error(`[INFO][search-products] Successfully extracted ${searchResults.length} products for search term "${searchTerm}"`) - return searchResults -} - -function extractSearchResultSingleProductData($: cheerio.CheerioAPI, $item: cheerio.Cheerio): ProductSearchResult | null { - // Extract ASIN - const asin = $item.attr('data-asin') - if (!asin) { - return null - } - - // Extract title and check if sponsored - const titleElement = $item.find('h2[aria-label]') - const fullTitle = titleElement.attr('aria-label') || '' - const isSponsored = fullTitle.startsWith('Sponsored Ad โ€“ ') - const title = isSponsored ? fullTitle.replace('Sponsored Ad โ€“ ', '') : fullTitle - - // Extract brand - const brand = $item.find('h2.a-size-mini span.a-size-base-plus.a-color-base').text().trim() || undefined - - // Extract price information - const price = $item.find('span.a-price[data-a-size="xl"] > span.a-offscreen').text().trim() || undefined - - // Extract price per unit (more complex selector) - let pricePerUnit: string | undefined - const pricePerUnitElement = $item.find('span.a-price[data-a-size="b"][data-a-color="secondary"] > span.a-offscreen') - if (pricePerUnitElement.length > 0) { - const parentText = pricePerUnitElement.parent().parent().text().trim() - pricePerUnit = parentText || undefined - } - - // Extract reviews - const reviews: ProductSearchResult['reviews'] = {} - - const ratingElement = $item.find('i.a-icon-star-mini span.a-icon-alt') - const ratingText = ratingElement.text().trim() - if (ratingText) { - reviews.averageRating = ratingText - } - - const reviewCountElement = $item.find('a[aria-label*="ratings"] span.a-size-small') - const reviewCount = reviewCountElement.text().trim() - if (reviewCount) { - reviews.reviewCount = reviewCount - } - - // Extract image URL - const imageUrl = $item.find('img.s-image').attr('src') || undefined - - // Check Prime eligibility - const isPrimeEligible = $item.find('i.a-icon-prime').length > 0 - - // Extract delivery information - const deliveryInfo = $item.find('div.udm-primary-delivery-message').text().trim() || undefined - - // Extract product URL - const productUrl = `https://www.amazon.es/-/en/gp/product/${asin}` - - return { - asin, - title, - isSponsored, - brand, - price, - pricePerUnit, - reviews: Object.keys(reviews).length > 0 ? reviews : undefined, - imageUrl, - isPrimeEligible, - deliveryInfo, - productUrl, - } -} - -// ################################## -// End searchProducts -// ################################## diff --git a/src/cart.ts b/src/cart.ts new file mode 100644 index 0000000..6c3b502 --- /dev/null +++ b/src/cart.ts @@ -0,0 +1,304 @@ +import * as cheerio from 'cheerio' +import fs from 'fs' +import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js' +import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js' + +const __dirname = new URL('.', import.meta.url).pathname + +// ################################## +// Cart Content Types +// ################################## + +interface CartItem { + title: string + price: string + quantity: number + image?: string + productUrl?: string + asin?: string + availability: string + isSelected: boolean +} + +interface CartContent { + isEmpty: boolean + items: CartItem[] + subtotal?: string + totalItems?: number +} + +// ################################## +// Get Cart Content +// ################################## + +export async function getCartContent(): Promise { + let html: string + if (USE_MOCKS) { + console.error('[INFO][get-cart-content] Fetching cart content from mocks') + const mockPath = `${__dirname}/../mocks/getCartContent.html` + html = fs.readFileSync(mockPath, 'utf-8') + } else { + const domain = getAmazonDomain() + const url = `https://www.${domain}/-/en/gp/cart/view.html?ref_=nav_cart` + console.error(`[INFO][get-cart-content] Fetching cart content from ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the cart page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the cart content to load + try { + await page.waitForSelector('#sc-active-cart', { timeout: 10000 }) + } catch (e) { + throw new Error('[INFO][get-cart-content] Could not find cart container. Ensure you are logged in and the cart is accessible.') + } + + if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { + // Export only the `#sc-active-cart` content to a mock file + const timestamp = getTimestamp() + const mockPath = `${__dirname}/../mocks/getCartContent_${timestamp}.html` + const cartHtml = await page.$eval('#sc-active-cart', el => el.outerHTML) + fs.writeFileSync(mockPath, cartHtml) + console.error(`[INFO][get-cart-content] Exported cart container HTML to ${mockPath}`) + } + + // Get the HTML content after JavaScript execution + html = await page.content() + } finally { + await browser.close() + } + } + + const $ = cheerio.load(html) + return extractCartPageData($) +} + +function extractCartPageData($: cheerio.CheerioAPI): CartContent { + const $cartContainer = $('#sc-active-cart') + + // Check if cart is empty + const emptyCartText = $cartContainer.text() + if (emptyCartText.includes('Your Amazon Cart is empty')) { + return { + isEmpty: true, + items: [], + } + } + + // Extract cart items + const items: CartItem[] = [] + $cartContainer.find('[data-asin]').each((_index, element) => { + const $item = $(element) + + // Extract basic item information + const titleElement = $item.find('a.sc-product-title').first() + const title = titleElement.find('.a-truncate-full').text().trim() + const price = $item.find('.apex-price-to-pay-value .a-offscreen').text().trim() + const quantityElement = $item.find('[data-a-selector="value"]').text().trim() + const quantity = parseInt(quantityElement) || 1 + + // Extract optional information + const image = $item.find('.sc-product-image').attr('src') + const productUrl = $item.find('.sc-product-link').attr('href') + const asin = $item.attr('data-asin') + const availability = $item.find('.sc-product-availability').text().trim() || 'Unknown' + const isSelected = $item.find('input[type="checkbox"]').is(':checked') + + console.error(`[INFO][get-cart-content] Extracted ASIN: ${asin}, Price: ${price}, Quantity: ${quantity}, item: ${title}`) + // Only add items with valid titles and prices + if (title && price) { + items.push({ + title, + price, + quantity, + image, + productUrl, + asin, + availability, + isSelected, + }) + } + }) + + // Extract subtotal information + const subtotal = + $cartContainer.find('#sc-subtotal-amount-activecart .sc-price').text().trim() || + $cartContainer.find('.sc-subtotal .sc-price').text().trim() + + const totalItemsText = $cartContainer.find('#sc-subtotal-label-activecart').text().trim() + const totalItemsMatch = totalItemsText.match(/\((\d+)\s+item/) + const totalItems = totalItemsMatch ? parseInt(totalItemsMatch[1]) : items.length + + return { + isEmpty: false, + items, + subtotal, + totalItems, + } +} + +// ################################## +// Add to Cart +// ################################## + +export async function addToCart(asin: string): Promise<{ success: boolean; message: string }> { + if (!asin || asin.length !== 10) { + throw new Error('Invalid ASIN provided. ASIN should be a 10-character string.') + } + + const domain = getAmazonDomain() + const url = `https://www.${domain}/-/en/gp/product/${asin}` + console.error(`[INFO][add-to-cart] Adding product ${asin} to cart from ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the product page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the page to load completely + await page.waitForSelector('body', { timeout: 10000 }) + + try { + // Check for subscribe and save option using XPath + const xpath = "//div[contains(@class, 'accordion-caption')]//span[contains(text(), 'One-time purchase')]" + const element = await page.waitForSelector(`::-p-xpath(${xpath})`, { timeout: 2000 }) + if (element) { + console.error(`[INFO][add-to-cart] The item is a subscribe and save product, clicking the one-time purchase option`) + element.click() + // Wait for the page to update + await new Promise(resolve => setTimeout(resolve, 2000)) + } else { + console.error('[INFO][add-to-cart] No subscribe and save option found, proceeding to add to cart') + } + } catch (error) { + console.error(`[INFO][add-to-cart] Error checking for subscribe and save option: ${error}`) + } + + // Find and click the add to cart button + try { + await page.waitForSelector('#add-to-cart-button', { timeout: 10000 }) + await page.click('#add-to-cart-button') + console.error('[INFO][add-to-cart] Clicked add to cart button') + } catch (error) { + throw new Error(`Could not find or click the add to cart button: ${error}`) + } + + // If there is an insurance option, refuse it + try { + await page.waitForSelector('#productTitle', { timeout: 1000 }) + await page.click('#productTitle', { delay: 100 }) + await page.click('#attachSiNoCoverage', { delay: 300 }) + } catch (error) { + console.error(`[WARNING][add-to-cart] Failed to click insurance option (it may not have been presented):`, error) + } + + // Wait for the confirmation page/modal + try { + await page.waitForSelector('#sw-atc-confirmation', { timeout: 15000 }) + + // Check for success message + const confirmationText = await page.$eval('#sw-atc-confirmation', el => el.textContent || '') + + if (!confirmationText.includes('Added to basket')) { + throw new Error(`Unexpected confirmation message: ${confirmationText}`) + } + + console.error('[INFO][add-to-cart] Successfully added product to cart') + return { + success: true, + message: `Product ${asin} successfully added to cart`, + } + } catch (error) { + throw new Error(`Could not verify that the product was added to cart: ${error}`) + } + } finally { + await browser.close() + } +} + +// ################################## +// Clear Cart +// ################################## + +export async function clearCart() { + const domain = getAmazonDomain() + const url = `https://www.${domain}/-/en/gp/cart/view.html` + console.error(`[INFO][clear-cart] Clearing cart at ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the cart page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the cart to load + await page.waitForSelector('#sc-active-cart, .sc-cart-item, .sc-empty-cart-banner', { timeout: 10000 }) + + // Find all delete buttons + const deleteButtons = await page.$$('span[data-action="delete-active"]') + + if (deleteButtons.length === 0) { + console.error('[INFO][clear-cart] No items found in cart to remove') + return { + success: true, + message: 'No items found in cart to remove', + itemsRemoved: 0, + } + } + + console.error(`[INFO][clear-cart] Found ${deleteButtons.length} items to remove`) + + let itemsRemoved = 0 + + // Click each delete button with delay + for (let i = 0; i < deleteButtons.length; i++) { + try { + // Re-query the delete buttons as DOM changes after each deletion + const currentDeleteButtons = await page.$$('span[data-action="delete-active"]') + + if (currentDeleteButtons.length === 0) { + console.error('[INFO][clear-cart] No more items to delete') + break + } + + // Click the first available delete button + await currentDeleteButtons[0].click() + itemsRemoved++ + + console.error(`[INFO][clear-cart] Removed item ${itemsRemoved}`) + + // Wait for the page to update after deletion + if (i < deleteButtons.length - 1) { + await new Promise(resolve => setTimeout(resolve, 800)) + } + } catch (error) { + console.error(`[WARNING][clear-cart] Failed to remove item ${i + 1}:`, error) + } + } + + console.error(`[INFO][clear-cart] Successfully removed ${itemsRemoved} items from cart`) + + return { + success: true, + message: `Successfully cleared cart. Removed ${itemsRemoved} items.`, + itemsRemoved, + } + } catch (error: any) { + console.error('[ERROR][clear-cart] Error clearing cart:', error) + throw new Error(`Failed to clear cart: ${error.message}`) + } finally { + await browser.close() + } +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c6ec283..3bea0cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,7 +2,7 @@ import { loadAmazonCookiesFile } from './utils.js' const __dirname = new URL('.', import.meta.url).pathname -export const IS_BROWSER_VISIBLE = true +export const IS_BROWSER_VISIBLE = false /** Use local mock files instead of live scraping */ export const USE_MOCKS = false @@ -31,3 +31,50 @@ export const AMAZON_COOKIES: { storeId: string | null value: string }[] = loadAmazonCookiesFile() + +/** + * Extract the Amazon domain from cookies + * Returns the domain without the leading dot (e.g., "amazon.com", "amazon.co.uk", "amazon.de") + */ +export function getAmazonDomain(): string { + if (!AMAZON_COOKIES || AMAZON_COOKIES.length === 0) { + console.error('[WARN] No cookies found, using default amazon.com domain') + return 'amazon.com' + } + + // Find a cookie with domain starting with ".amazon." + const amazonCookie = AMAZON_COOKIES.find(cookie => + cookie.domain && cookie.domain.startsWith('.amazon.') + ) + + if (amazonCookie) { + // Remove the leading dot from domain + const domain = amazonCookie.domain.startsWith('.') + ? amazonCookie.domain.substring(1) + : amazonCookie.domain + console.error(`[INFO] Detected Amazon domain from cookies: ${domain}`) + return domain + } + + // Fallback: try to find any cookie with "amazon" in the domain + const fallbackCookie = AMAZON_COOKIES.find(cookie => + cookie.domain && cookie.domain.includes('amazon') + ) + + if (fallbackCookie) { + let domain = fallbackCookie.domain + // Remove leading dot if present + if (domain.startsWith('.')) { + domain = domain.substring(1) + } + // If it's a subdomain like "www.amazon.com", extract the main domain + if (domain.startsWith('www.')) { + domain = domain.substring(4) + } + console.error(`[INFO] Detected Amazon domain from cookies (fallback): ${domain}`) + return domain + } + + console.error('[WARN] Could not detect Amazon domain from cookies, using default amazon.com') + return 'amazon.com' +} diff --git a/src/index.ts b/src/index.ts index a17e9b0..2f8bad7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod' -import { getOrdersHistory, getCartContent, addToCart, getProductDetails, searchProducts, clearCart } from './amazon.js' +import { getOrdersHistory } from './orders.js' +import { getCartContent, addToCart, clearCart } from './cart.js' +import { getProductDetails, searchProducts } from './products.js' // Create server instance const server = new McpServer({ diff --git a/src/orders.ts b/src/orders.ts new file mode 100644 index 0000000..826a440 --- /dev/null +++ b/src/orders.ts @@ -0,0 +1,136 @@ +import * as cheerio from 'cheerio' +import fs from 'fs' +import puppeteer from 'puppeteer' +import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js' +import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js' + +const __dirname = new URL('.', import.meta.url).pathname + +// ################################## +// Get Orders History +// ################################## + +export async function getOrdersHistory() { + let html: string + if (USE_MOCKS) { + console.error('[INFO][get-orders-history] Fetching orders history from mocks') + const mockPath = `${__dirname}/../mocks/getOrdersHistory.html` + html = fs.readFileSync(mockPath, 'utf-8') + } else { + const domain = getAmazonDomain() + const url = `https://www.${domain}/-/en/gp/css/order-history` + console.error(`[INFO][get-orders-history] Fetching orders history from ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the order cards to load (adjust selector as needed) + try { + await page.waitForSelector('.order-card, .your-orders-content-container', { timeout: 10000 }) + } catch (e) { + throw new Error( + '[INFO][get-orders-history] Could not find orders card selector. Ensure you are logged in and the orders history is accessible.' + ) + } + + if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { + // Export only the .order-card and .your-orders-content-container content to a mock file + const timestamp = getTimestamp() + const mockPath = `${__dirname}/../mocks/getOrdersHistory_${timestamp}.html` + const orderCardsHtml = await page.$$eval('.order-card, .your-orders-content-container', elements => + elements.map(el => el.outerHTML).join('\n') + ) + fs.writeFileSync(mockPath, orderCardsHtml) + console.error(`[INFO][get-orders-history] Exported order cards HTML to ${mockPath}`) + } + + // Get the HTML content after JavaScript execution + html = await page.content() + } finally { + await browser.close() + } + } + + const $ = cheerio.load(html) + const orderCards = $('.order-card') + .map((index, element) => extractOrdersHistoryPageData($, $(element))) + .get() + return orderCards +} + +function extractOrdersHistoryPageData($: cheerio.CheerioAPI, $card: cheerio.Cheerio) { + // Extract order information + const orderNumber = $card.find('.yohtmlc-order-id span').last().text().trim() + const orderDate = $card.find('.order-header__header-list-item').first().find('.a-size-base').text().trim() + const total = $card.find('.order-header__header-list-item').eq(1).find('.a-size-base').text().trim() + const status = $card.find('.delivery-box__primary-text').text().trim() + const collectionMatch = status.match(/Collected on (.+)/) + const collectionDate = collectionMatch ? collectionMatch[1] : null + + // Extract delivery address + const deliveryName = $card.find('.a-popover-preload h5').text().trim() + const deliveryAddress = $card.find('.a-popover-preload .a-row').eq(1).text().trim().replace(/\s+/g, ' ') + const deliveryCountry = $card.find('.a-popover-preload .a-row').last().text().trim() + + // Extract items + const items: { + title: string + image: string | undefined + productUrl: string | undefined + asin: string | null + returnEligible: boolean + returnDate: string | null + }[] = [] + $card.find('.item-box').each((index, element) => { + const $element = $(element) + const title = $element.find('.yohtmlc-product-title a').text().trim() + const image = $element.find('.product-image img').attr('src') + const productUrl = $element.find('.yohtmlc-product-title a').attr('href') + const returnText = $element.find('.a-size-small').text().trim() + + let asin = null + if (productUrl) { + const asinMatch = productUrl.match(/\/dp\/([A-Z0-9]{10})/) + asin = asinMatch ? asinMatch[1] : null + } + + let returnEligible = false + let returnDate = null + if (returnText.includes('Return or Replace Items')) { + returnEligible = true + const returnDateMatch = returnText.match(/until (.+)/) + returnDate = returnDateMatch ? returnDateMatch[1] : null + } + + items.push({ + title, + image, + productUrl, + asin, + returnEligible, + returnDate, + }) + }) + + return { + orderInfo: { + orderNumber, + orderDate, + total, + deliveryAddress: { + name: deliveryName, + address: deliveryAddress, + country: deliveryCountry, + }, + status, + collectionDate, + }, + items, + } +} \ No newline at end of file diff --git a/src/products.ts b/src/products.ts new file mode 100644 index 0000000..e5672f1 --- /dev/null +++ b/src/products.ts @@ -0,0 +1,355 @@ +import * as cheerio from 'cheerio' +import fs from 'fs' +import puppeteer from 'puppeteer' +import { USE_MOCKS, EXPORT_LIVE_SCRAPING_FOR_MOCKS, getAmazonDomain } from './config.js' +import { createBrowserAndPage, getTimestamp, throwIfNotLoggedIn } from './utils.js' + +const __dirname = new URL('.', import.meta.url).pathname + +// ################################## +// Product Details +// ################################## + +interface ProductDetails { + data: { + asin: string + title: string + price: string + canUseSubscribeAndSave: boolean + description: { + overview?: string + features?: string + facts?: string + brandSnapshot?: string + } + reviews: { + averageRating?: string + reviewsCount?: string + } + mainImageUrl?: string + } + mainImageBase64?: string +} + +export async function getProductDetails(asin: string): Promise { + if (!asin || asin.length !== 10) { + throw new Error('Invalid ASIN provided. ASIN should be a 10-character string.') + } + + let html: string + if (USE_MOCKS) { + console.error('[INFO][get-product-details] Fetching product details from mocks') + const mockPath = `${__dirname}/../mocks/getProductDetails.html` + html = fs.readFileSync(mockPath, 'utf-8') + } else { + const domain = getAmazonDomain() + const url = `https://www.${domain}/-/en/gp/product/${asin}` + console.error(`[INFO][get-product-details] Fetching product details from ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the product page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the product page to load + try { + await page.waitForSelector('#productTitle', { timeout: 10000 }) + } catch (e) { + throw new Error('[INFO][get-product-details] Could not find product title. The product may not exist or be accessible.') + } + + if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { + // Export the main product content to a mock file + const timestamp = getTimestamp() + const mockPath = `${__dirname}/../mocks/getProductDetails_${timestamp}.html` + const productHtml = await page.content() + fs.writeFileSync(mockPath, productHtml) + console.error(`[INFO][get-product-details] Exported product page HTML to ${mockPath}`) + } + + // Get the HTML content after JavaScript execution + html = await page.content() + } finally { + await browser.close() + } + } + + const $ = cheerio.load(html) + return extractProductDetailsPageData($, asin) +} + +async function extractProductDetailsPageData($: cheerio.CheerioAPI, asin: string): Promise { + // Extract product title + const title = $('span#productTitle').text().trim() + + // Extract price information + let price = '' + let canUseSubscribeAndSave: ProductDetails['data']['canUseSubscribeAndSave'] = false + + // Check if it's a subscribe and save product + const subscriptionPrice = $('#subscriptionPrice .a-price .a-offscreen').prop('innerText')?.trim() + if (subscriptionPrice) { + price = subscriptionPrice + canUseSubscribeAndSave = true + } else { + // Use regular price + price = $('.priceToPay').text().trim() + } + + // Extract description sections + const description: ProductDetails['data']['description'] = {} + + const overview = $('#productOverview_feature_div').prop('innerText')?.trim() + if (overview) description.overview = overview + + const features = $('#featurebullets_feature_div').prop('innerText')?.trim() + if (features) description.features = features + + const facts = $('#productFactsDesktop_feature_div').prop('innerText')?.trim() + if (facts) description.facts = facts + + const brandSnapshot = $('#brandSnapshot_feature_div').prop('innerText')?.trim() + if (brandSnapshot) description.brandSnapshot = brandSnapshot + + // Extract reviews information + const reviews: ProductDetails['data']['reviews'] = {} + + const averageRating = $('#averageCustomerReviews span.a-size-small.a-color-base').text().trim() + if (averageRating) reviews.averageRating = averageRating + + const reviewsCountElement = $('#acrCustomerReviewLink span') + const reviewsCount = reviewsCountElement.attr('aria-label') + if (reviewsCount) + reviews.reviewsCount = reviewsCount + .replace(/\s+.*$/g, '') + .replace(/,/g, '') + .trim() + + // Extract main product image + const mainImageUrl = $('#main-image-container img.a-dynamic-image').attr('src') + // Download the image and convert to base64 + let mainImageBase64: ProductDetails['mainImageBase64'] = undefined + if (mainImageUrl) { + if (USE_MOCKS) { + console.error('[INFO][get-product-details] Downloading product main image from mocks') + const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64.txt` + mainImageBase64 = fs.readFileSync(mockPath, 'utf-8') + } else { + // FIXME: This is not supported yet by Claude Desktop client!! Uncomment when they implement it + // console.error(`[INFO][get-product-details] Downloading main image from ${mainImageUrl}`) + // mainImageBase64 = await downloadImageAsBase64(mainImageUrl) + // if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { + // const timestamp = getTimestamp() + // const mockPath = `${__dirname}/../mocks/getProductDetails_image_base64_${timestamp}.txt` + // fs.writeFileSync(mockPath, mainImageBase64) + // console.error(`[INFO][get-product-details] Exported main image base64 to ${mockPath}`) + // } + } + } + + console.error( + `[INFO][get-product-details] Extracted product: ASIN: ${asin}, ${title}, Price: ${price}, Can use subscribe and save: ${canUseSubscribeAndSave}, Reviews: ${reviews.averageRating} (${reviews.reviewsCount} reviews), Main image URL: ${mainImageUrl}` + ) + + return { + data: { + asin, + title, + price, + canUseSubscribeAndSave, + description, + reviews, + mainImageUrl, + }, + mainImageBase64, + } +} + +// ################################## +// Product Search +// ################################## + +interface ProductSearchResult { + asin: string + title: string + isSponsored: boolean + brand?: string + price?: string + pricePerUnit?: string + description?: { + overview?: string + features?: string + facts?: string + brandSnapshot?: string + } + reviews?: { + averageRating?: string + reviewCount?: string + } + imageUrl?: string + isPrimeEligible: boolean + deliveryInfo?: string + productUrl?: string +} + +export async function searchProducts(searchTerm: string): Promise { + if (!searchTerm || searchTerm.trim().length === 0) { + throw new Error('Search term is required and cannot be empty.') + } + + let html: string + if (USE_MOCKS) { + console.error('[INFO][search-products] Fetching search results from mocks') + const mockPath = `${__dirname}/../mocks/searchProducts.html` + html = fs.readFileSync(mockPath, 'utf-8') + } else { + const domain = getAmazonDomain() + const url = `https://www.${domain}/s?k=${encodeURIComponent(searchTerm)}` + console.error(`[INFO][search-products] Searching for products with term "${searchTerm}" from ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the search page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for search results to load + try { + await page.waitForSelector('.s-search-results', { timeout: 10000 }) + } catch (e) { + throw new Error( + '[INFO][search-products] Could not find search results container. The search may have failed or returned no results.' + ) + } + + if (EXPORT_LIVE_SCRAPING_FOR_MOCKS) { + // Export the search results content to a mock file + const timestamp = getTimestamp() + const searchResultsHtml = await page.$eval('.s-search-results', el => el.outerHTML) + const mockFileName = `searchProducts_${timestamp}.html` + const mockPath = `${__dirname}/../mocks/${mockFileName}` + fs.writeFileSync(mockPath, searchResultsHtml) + console.error(`[INFO][search-products] Exported search results HTML to ${mockPath}`) + } + + // Get the HTML content after JavaScript execution + html = await page.content() + } finally { + await browser.close() + } + } + + const $ = cheerio.load(html) + return extractSearchResultsPageData($, searchTerm) +} + +function extractSearchResultsPageData($: cheerio.CheerioAPI, searchTerm: string): ProductSearchResult[] { + const searchResults: ProductSearchResult[] = [] + + // Find the search results using the actual Amazon structure + const $productItems = $('[role="listitem"]') + + if ($productItems.length === 0) { + console.error('[INFO][search-products] No search results found') + return [] + } + + // Limit to first 20 items + const limitedItems = $productItems.slice(0, 20) + + console.error(`[INFO][search-products] Found ${$productItems.length} products, processing first ${limitedItems.length}`) + + limitedItems.each((index, element) => { + const $item = $(element) + + try { + const productData = extractSearchResultSingleProductData($, $item) + if (productData && productData.asin) { + searchResults.push(productData) + console.error(`[INFO][search-products] Extracted product ${index + 1}: ${productData.asin} - ${productData.title}`) + } + } catch (error) { + console.error(`[INFO][search-products] Error extracting product ${index + 1}:`, error) + } + }) + + console.error(`[INFO][search-products] Successfully extracted ${searchResults.length} products for search term "${searchTerm}"`) + return searchResults +} + +function extractSearchResultSingleProductData($: cheerio.CheerioAPI, $item: cheerio.Cheerio): ProductSearchResult | null { + // Extract ASIN + const asin = $item.attr('data-asin') + if (!asin) { + return null + } + + // Extract title and check if sponsored + const titleElement = $item.find('h2[aria-label]') + const fullTitle = titleElement.attr('aria-label') || '' + const isSponsored = fullTitle.startsWith('Sponsored Ad โ€“ ') + const title = isSponsored ? fullTitle.replace('Sponsored Ad โ€“ ', '') : fullTitle + + // Extract brand + const brand = $item.find('h2.a-size-mini span.a-size-base-plus.a-color-base').text().trim() || undefined + + // Extract price information + const price = $item.find('span.a-price[data-a-size="xl"] > span.a-offscreen').text().trim() || undefined + + // Extract price per unit (more complex selector) + let pricePerUnit: string | undefined + const pricePerUnitElement = $item.find('span.a-price[data-a-size="b"][data-a-color="secondary"] > span.a-offscreen') + if (pricePerUnitElement.length > 0) { + const parentText = pricePerUnitElement.parent().parent().text().trim() + pricePerUnit = parentText || undefined + } + + // Extract reviews + const reviews: ProductSearchResult['reviews'] = {} + + const ratingElement = $item.find('i.a-icon-star-mini span.a-icon-alt') + const ratingText = ratingElement.text().trim() + if (ratingText) { + reviews.averageRating = ratingText + } + + const reviewCountElement = $item.find('a[aria-label*="ratings"] span.a-size-small') + const reviewCount = reviewCountElement.text().trim() + if (reviewCount) { + reviews.reviewCount = reviewCount + } + + // Extract image URL + const imageUrl = $item.find('img.s-image').attr('src') || undefined + + // Check Prime eligibility + const isPrimeEligible = $item.find('i.a-icon-prime').length > 0 + + // Extract delivery information + const deliveryInfo = $item.find('div.udm-primary-delivery-message').text().trim() || undefined + + // Extract product URL + const domain = getAmazonDomain() + const productUrl = `https://www.${domain}/-/en/gp/product/${asin}` + + return { + asin, + title, + isSponsored, + brand, + price, + pricePerUnit, + reviews: Object.keys(reviews).length > 0 ? reviews : undefined, + imageUrl, + isPrimeEligible, + deliveryInfo, + productUrl, + } +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 381b8a0..f7a6080 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -76,3 +76,10 @@ export async function downloadImageAsBase64(url: string): Promise { const b64 = buffer.toString('base64') return `${b64}` } + +export async function throwIfNotLoggedIn(page: puppeteer.Page): Promise { + const isLoginPage = (await page.$('#ap_email')) !== null || (await page.$('#signInSubmit')) !== null + if (isLoginPage) { + throw new Error('You need to be logged in to access this feature. Please log in to Amazon first and then try again.') + } +} From d9a3913fbe18e15c59e3822c949b41b1a1c01780 Mon Sep 17 00:00:00 2001 From: David Vasandani Date: Wed, 23 Jul 2025 07:26:57 -0700 Subject: [PATCH 2/3] Update tests to use dynamic ASINs for better reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update getProductDetails test to dynamically search for products before testing - Update addToCart test to find available products through search - Fix cart confirmation message check to support both "Added to cart" and "Added to basket" - Add helper functions to find suitable test products - Tests now adapt to product availability instead of using hardcoded ASINs This ensures tests remain functional even when specific products become unavailable. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 20 + .vscode/settings.json | 8 + amazonCookies.example.json | 18 +- package-lock.json | 2387 ++++++++++++++++++++++++++ src/amazon.addToCart.test.ts | 100 +- src/amazon.getProductDetails.test.ts | 73 +- src/cart.ts | 2 +- 7 files changed, 2585 insertions(+), 23 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .vscode/settings.json create mode 100644 package-lock.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e01d678 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build:*)", + "Bash(rm:*)", + "Bash(ls:*)", + "mcp__ide__getDiagnostics", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git rm:*)", + "Bash(git commit:*)", + "Bash(gh repo view:*)", + "Bash(gh repo fork:*)", + "Bash(git remote add:*)", + "Bash(git push:*)", + "Bash(node:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1c37642 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "activecart", + "featurebullets", + "networkidle", + "yohtmlc" + ] +} \ No newline at end of file diff --git a/amazonCookies.example.json b/amazonCookies.example.json index bb2b59a..e97b294 100644 --- a/amazonCookies.example.json +++ b/amazonCookies.example.json @@ -1,6 +1,6 @@ [ { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.4719, "hostOnly": false, "httpOnly": false, @@ -13,7 +13,7 @@ "value": "262-3992533-1234567" }, { - "domain": "www.amazon.es", + "domain": "www.amazon.com", "expirationDate": 1751038456.64728, "hostOnly": true, "httpOnly": false, @@ -26,7 +26,7 @@ "value": "eyJ3YXNtVGVzxxxxxxxxxxxxxxx" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1766055357.407565, "hostOnly": false, "httpOnly": false, @@ -39,7 +39,7 @@ "value": "xxxxxxxxxx" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.472211, "hostOnly": false, "httpOnly": false, @@ -52,7 +52,7 @@ "value": "2082787201l" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.313376, "hostOnly": false, "httpOnly": true, @@ -65,7 +65,7 @@ "value": "Atza|IwEBIOy-xxxxxxxxx" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.313526, "hostOnly": false, "httpOnly": true, @@ -78,7 +78,7 @@ "value": "\"5qfQRHBU2eZuCZkFhj+xxxxxxxxx\"" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.471672, "hostOnly": false, "httpOnly": false, @@ -91,7 +91,7 @@ "value": "262-2607238-3505323" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1766055357.407166, "hostOnly": false, "httpOnly": false, @@ -104,7 +104,7 @@ "value": "HUoAsHXDi+Hxxxxxxxxxxxxxxxxx" }, { - "domain": ".amazon.es", + "domain": ".amazon.com", "expirationDate": 1765985623.313788, "hostOnly": false, "httpOnly": true, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9d5760a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2387 @@ +{ + "name": "mcp-server-amazon", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mcp-server-amazon", + "version": "1.0.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.4.0", + "cheerio": "^1.1.0", + "puppeteer": "^24.10.2", + "zod": "^3.25.67" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "typescript": "^5.7.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.16.0.tgz", + "integrity": "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.6.tgz", + "integrity": "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.1", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.2", + "tar-fs": "^3.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "license": "Apache-2.0" + }, + "node_modules/bare-events": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.6.tgz", + "integrity": "sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chromium-bidi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-7.2.0.tgz", + "integrity": "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1464554", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1464554.tgz", + "integrity": "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==", + "license": "BSD-3-Clause" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.15.0.tgz", + "integrity": "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1464554", + "puppeteer-core": "24.15.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.15.0.tgz", + "integrity": "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.6", + "chromium-bidi": "7.2.0", + "debug": "^4.4.1", + "devtools-protocol": "0.0.1464554", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", + "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/src/amazon.addToCart.test.ts b/src/amazon.addToCart.test.ts index aacba04..b93f856 100644 --- a/src/amazon.addToCart.test.ts +++ b/src/amazon.addToCart.test.ts @@ -1,6 +1,47 @@ import { addToCart } from './cart.js' +import { searchProducts } from './products.js' import { USE_MOCKS } from './config.js' +async function getTestProductASIN() { + try { + // Search for products that are commonly available and likely to have add to cart functionality + const searchTerms = ['usb cable', 'hdmi cable', 'phone charger', 'mouse pad']; + + for (const term of searchTerms) { + console.log(`Searching for "${term}" to find a product to add to cart...`); + const results = await searchProducts(term); + + if (results && results.length > 0) { + // Find a product that's not sponsored and is Prime eligible (more likely to be in stock) + const idealProduct = results.find(p => + p.asin && + !p.title.toLowerCase().includes('sponsored') && + p.isPrimeEligible && + p.price // Has a price, indicating it's available + ); + + if (idealProduct) { + console.log(`Found product: ${idealProduct.title} (${idealProduct.asin})`); + return idealProduct.asin; + } + + // Fallback to any product with a price + const anyProduct = results.find(p => p.asin && p.price); + if (anyProduct) { + console.log(`Found product: ${anyProduct.title} (${anyProduct.asin})`); + return anyProduct.asin; + } + } + } + + throw new Error('Could not find any suitable products to test'); + } catch (error) { + console.error('Error finding test product:', error); + // Return a commonly available ASIN as fallback + return 'B07THHPGCV'; // Common USB cable + } +} + async function testAddToCart() { if (USE_MOCKS) { console.log('Skipping addToCart test because USE_MOCKS is enabled') @@ -8,11 +49,10 @@ async function testAddToCart() { } try { - // Test with a valid ASIN (example product) - const testAsin = 'B0CM8P9PV9' // ASIN with insurance option - // const testAsin = 'B0B15FKR8T' // ASIN with subscription option - - console.log(`Testing addToCart with ASIN: ${testAsin}`) + // Get a dynamic ASIN from search results + const testAsin = await getTestProductASIN(); + + console.log(`\nTesting addToCart with ASIN: ${testAsin}`) const result = await addToCart(testAsin) console.log('Result:', result) @@ -27,7 +67,53 @@ async function testAddToCart() { } } +async function testAddToCartWithOptions() { + if (USE_MOCKS) { + console.log('Skipping addToCart test with options because USE_MOCKS is enabled') + return + } + + console.log('\n\n--------------------------------------') + console.log('Testing add to cart with products that may have options...') + + try { + // Search for products that often have subscribe & save options + console.log('Searching for products with subscribe & save options...'); + const results = await searchProducts('paper towels'); + + const subscribeProduct = results.find(p => + p.asin && + (p.title.toLowerCase().includes('pack') || + p.title.toLowerCase().includes('count') || + p.title.toLowerCase().includes('rolls')) + ); + + if (subscribeProduct) { + console.log(`\nTesting with product that may have subscribe & save: ${subscribeProduct.title}`); + console.log(`ASIN: ${subscribeProduct.asin}`); + + try { + const result = await addToCart(subscribeProduct.asin); + console.log('Result:', result); + + if (result.success) { + console.log('โœ… Test passed: Product with options successfully added to cart'); + } + } catch (error: any) { + console.log('Note: Product may require additional interaction:', error.message); + } + } + } catch (error) { + console.error('Could not test products with options:', error); + } +} + // Run test if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { - testAddToCart() -} + (async () => { + console.log('=== Amazon Add to Cart Test ===\n'); + await testAddToCart(); + await testAddToCartWithOptions(); + console.log('\n=== Tests completed ==='); + })(); +} \ No newline at end of file diff --git a/src/amazon.getProductDetails.test.ts b/src/amazon.getProductDetails.test.ts index 9372fcd..8a6746b 100644 --- a/src/amazon.getProductDetails.test.ts +++ b/src/amazon.getProductDetails.test.ts @@ -1,19 +1,70 @@ -import { getProductDetails } from './products.js' +import { getProductDetails, searchProducts } from './products.js' +import { USE_MOCKS } from './config.js' + +async function getTestASINs() { + try { + // Search for common products that are likely to be available + const searchTerms = ['usb cable', 'notebook', 'pen']; + + for (const term of searchTerms) { + console.log(`Searching for "${term}" to get test ASINs...`); + const results = await searchProducts(term); + + if (results && results.length > 0) { + // Find a regular product and one that might have subscribe & save + const regularProduct = results.find(p => p.asin && !p.title.toLowerCase().includes('sponsored')); + const subscribeProduct = results.find(p => p.asin && (p.title.toLowerCase().includes('pack') || p.title.toLowerCase().includes('count'))); + + return { + regular: regularProduct?.asin || results[0].asin, + subscribe: subscribeProduct?.asin || results[1]?.asin || results[0].asin + }; + } + } + + throw new Error('Could not find any products to test with'); + } catch (error) { + console.error('Error getting test ASINs:', error); + // Fallback to some commonly available ASINs + return { + regular: 'B07THHPGCV', // Common USB cable + subscribe: 'B07232M876' // Common office supplies + }; + } +} async function testGetProductDetails_regular() { console.log('\n\n--------------------------------------') console.log('Run testGetProductDetails_regular...') + + if (USE_MOCKS) { + console.log('Using mock data - skipping dynamic ASIN lookup'); + const testAsin = 'B0F2255HFW'; + try { + const result = await getProductDetails(testAsin); + console.log('โœ… Mock test passed'); + return; + } catch (error) { + console.error('โŒ Mock test failed:', error); + return; + } + } + try { - const testAsin = 'B0F2255HFW' + const asins = await getTestASINs(); + const testAsin = asins.regular; console.log(`Testing getProductDetails with ASIN: ${testAsin}`) const result = await getProductDetails(testAsin) console.log('Result data:') - console.log(result.data) + console.log(JSON.stringify(result.data, null, 2)) // Basic validation if (result.data.asin === testAsin && result.data.title && result.data.price) { console.log('โœ… Test passed: Product details successfully retrieved') + console.log(` Title: ${result.data.title}`) + console.log(` Price: ${result.data.price}`) + console.log(` Reviews: ${result.data.reviews.averageRating || 'N/A'} (${result.data.reviews.reviewsCount || '0'} reviews)`) } else { console.log('โŒ Test failed: Missing required product data') } @@ -25,17 +76,27 @@ async function testGetProductDetails_regular() { async function testGetProductDetails_subscribeAndSave() { console.log('\n\n--------------------------------------') console.log('Run testGetProductDetails_subscribeAndSave...') + + if (USE_MOCKS) { + console.log('Using mock data - skipping dynamic ASIN lookup'); + return; + } + try { - const testAsin = 'B0B15FKR8T' + const asins = await getTestASINs(); + const testAsin = asins.subscribe; console.log(`Testing getProductDetails with ASIN: ${testAsin}`) const result = await getProductDetails(testAsin) console.log('Result data:') - console.log(result.data) + console.log(JSON.stringify(result.data, null, 2)) // Basic validation if (result.data.asin === testAsin && result.data.title && result.data.price) { console.log('โœ… Test passed: Product details successfully retrieved') + console.log(` Title: ${result.data.title}`) + console.log(` Price: ${result.data.price}`) + console.log(` Subscribe & Save available: ${result.data.canUseSubscribeAndSave}`) } else { console.log('โŒ Test failed: Missing required product data') } @@ -65,4 +126,4 @@ async function main() { console.log('\nTests completed.') } -main().catch(console.error) +main().catch(console.error) \ No newline at end of file diff --git a/src/cart.ts b/src/cart.ts index 6c3b502..d580093 100644 --- a/src/cart.ts +++ b/src/cart.ts @@ -208,7 +208,7 @@ export async function addToCart(asin: string): Promise<{ success: boolean; messa // Check for success message const confirmationText = await page.$eval('#sw-atc-confirmation', el => el.textContent || '') - if (!confirmationText.includes('Added to basket')) { + if (!confirmationText.includes('Added to cart') && !confirmationText.includes('Added to basket')) { throw new Error(`Unexpected confirmation message: ${confirmationText}`) } From 71c40d2584fc69b37fdfe55fa0b82ee3603e5735 Mon Sep 17 00:00:00 2001 From: David Vasandani Date: Wed, 23 Jul 2025 09:59:31 -0700 Subject: [PATCH 3/3] feat: add return/replace items functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add returnUrl field to order history data - Create new MCP tool 'initiate-return' for starting returns - Implement return page navigation and item extraction - Extract returnable items with eligibility status - Support both live and mock modes ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 1 + src/index.ts | 40 +++++++++++++++- src/orders.ts | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0479145..b7a761d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This server allows you to interact with Amazon's services using the MCP (Model C - **Cart management**: Add items or clear your Amazon cart - **Ordering**: Place orders (fake for demonstration purposes) - **Orders history**: Retrieve your recent Amazon orders details +- **Returns**: Initiate returns or replacements for eligible orders ## Demo diff --git a/src/index.ts b/src/index.ts index 2f8bad7..ec0982e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { z } from 'zod' -import { getOrdersHistory } from './orders.js' +import { getOrdersHistory, initiateReturn } from './orders.js' import { getCartContent, addToCart, clearCart } from './cart.js' import { getProductDetails, searchProducts } from './products.js' @@ -268,6 +268,44 @@ server.tool( } ) +server.tool( + 'initiate-return', + 'Navigate to the return/replace page for a specific order - ' + + 'Use this to start the return process for items in an order - ' + + 'The order ID can be obtained from get-orders-history', + { + orderId: z + .string() + .min(1, { message: 'Order ID cannot be empty.' }) + .describe('The order ID to initiate return for. Format: XXX-XXXXXXX-XXXXXXX (e.g., 114-3824678-7026645)'), + }, + async ({ orderId }) => { + let result: Awaited> + try { + result = await initiateReturn(orderId) + } catch (error: any) { + console.error('[ERROR][initiate-return] Error in initiate-return tool:', error) + return { + content: [ + { + type: 'text', + text: `An error occurred while initiating return. Error: ${error.message}`, + }, + ], + } + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + } + } +) + // Start the server async function main() { const transport = new StdioServerTransport() diff --git a/src/orders.ts b/src/orders.ts index 826a440..3548b21 100644 --- a/src/orders.ts +++ b/src/orders.ts @@ -118,6 +118,9 @@ function extractOrdersHistoryPageData($: cheerio.CheerioAPI, $card: cheerio.Chee }) }) + const domain = getAmazonDomain() + const returnUrl = orderNumber ? `https://www.${domain}/spr/returns/cart?orderId=${orderNumber}&ref=ppx_yo2ov_dt_b_return_replace` : null + return { orderInfo: { orderNumber, @@ -130,7 +133,129 @@ function extractOrdersHistoryPageData($: cheerio.CheerioAPI, $card: cheerio.Chee }, status, collectionDate, + returnUrl, }, items, } +} + +// ################################## +// Initiate Return or Replace +// ################################## + +export async function initiateReturn(orderId: string) { + if (USE_MOCKS) { + console.error('[INFO][initiate-return] Mock mode: Would navigate to return page for order', orderId) + return { + success: true, + message: `Mock mode: Would open return page for order ${orderId}`, + returnUrl: `https://www.${getAmazonDomain()}/spr/returns/cart?orderId=${orderId}&ref=ppx_yo2ov_dt_b_return_replace` + } + } + + const domain = getAmazonDomain() + const url = `https://www.${domain}/spr/returns/cart?orderId=${orderId}&ref=ppx_yo2ov_dt_b_return_replace` + console.error(`[INFO][initiate-return] Navigating to return page: ${url}`) + + const { browser, page } = await createBrowserAndPage() + + try { + // Navigate to the return page + await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }) + + // Handle login if needed + await throwIfNotLoggedIn(page) + + // Wait for the return page to load + try { + await page.waitForSelector('h1', { timeout: 10000 }) + } catch (e) { + throw new Error( + '[ERROR][initiate-return] Could not find page title. The page may not have loaded correctly.' + ) + } + + // Get the page content to extract available items for return + const html = await page.content() + const $ = cheerio.load(html) + + // Check if we're on the return page + const pageTitle = $('h1').text().trim() + const isReturnPage = pageTitle.toLowerCase().includes('choose items to return') || + pageTitle.toLowerCase().includes('return') || + pageTitle.toLowerCase().includes('replace') + + if (!isReturnPage) { + // Check for error messages + const errorMessage = $('.a-alert-error').text().trim() || $('.a-box-error').text().trim() + if (errorMessage) { + throw new Error(`[ERROR][initiate-return] ${errorMessage}`) + } + throw new Error(`[ERROR][initiate-return] Not on the return page. Page title: "${pageTitle}". Order may not be eligible for returns.`) + } + + // Extract returnable items + const returnableItems: Array<{ + title: string + asin: string | null + returnEligible: boolean + price?: string + orderNumber?: string + quantity?: number + itemKey?: string + }> = [] + + // Find all checkbox containers which represent returnable items + $('.orc-item-selection-checkbox').each((index, element) => { + const $checkbox = $(element) + const itemKey = $checkbox.attr('data-item-key') || '' + const checkbox = $checkbox.find('input[type="checkbox"]') + const isDisabled = checkbox.attr('disabled') !== undefined + const isChecked = checkbox.attr('checked') !== undefined + + // Find the corresponding item details + const $itemDetails = $(`.orc-returnable-item-details[data-item-key="${itemKey}"]`) + const title = $itemDetails.find('.a-text-bold').first().text().trim() + + // Extract order number from popover content + const popoverContent = $itemDetails.find('.a-popover-preload').html() || '' + const orderMatch = popoverContent.match(/Order #:\s*<\/span>\s*]*>([^<]+)/) + const orderNumber = orderMatch ? orderMatch[1].trim() : undefined + + // Extract price + const priceText = $itemDetails.find('.a-size-small').filter((i, el) => $(el).text().includes('$')).first().text().trim() + + // Try to find ASIN from product link or image + let asin = null + const productLink = $itemDetails.find('a[href*="/dp/"]').attr('href') + if (productLink) { + const asinMatch = productLink.match(/\/dp\/([A-Z0-9]{10})/) + asin = asinMatch ? asinMatch[1] : null + } + + returnableItems.push({ + title, + asin, + returnEligible: !isDisabled, + price: priceText, + orderNumber, + itemKey + }) + }) + + return { + success: true, + orderId, + returnUrl: url, + pageTitle, + returnableItems, + message: 'Successfully loaded return page. User can now select items to return.' + } + + } catch (error: any) { + console.error('[ERROR][initiate-return] Error:', error) + throw error + } finally { + await browser.close() + } } \ No newline at end of file