#!/usr/bin/env bash set -euo pipefail # Read-only eBay search through the user's local ego browser. # The browser session must already be able to pass eBay's Akamai checks. if [[ "$#" -eq 0 && -z "${EBAY_QUERY:-}" ]]; then printf 'Usage: %s \n' "$0" >&2 exit 64 fi query="${EBAY_QUERY:-$*}" printf '%s' "$query" > /tmp/ego-ebay-query ego-cli nodejs <<'EOF' const fs = await import('node:fs/promises'); const query = await fs.readFile('/tmp/ego-ebay-query', 'utf8'); await useOrCreateTaskSpace('ebay find-a-product'); await openOrReuseTab('https://www.ebay.com/sch/i.html?_nkw=' + encodeURIComponent(query), { wait: true, timeout: 40 }); await wait(2); const result = await js(`(() => { const body = document.body?.innerText || ''; const title = document.title || ''; const blocked = /Pardon Our Interruption|Access Denied|Are you 18 or older/i.test( title + ' ' + body.slice(0, 5000), ); if (blocked) { return { success: false, method: 'browser', reason: /18 or older/i.test(title + body) ? 'age_gated' : 'akamai_block', detail: title || body.slice(0, 240), }; } const text = (selector, root) => root.querySelector(selector)?.textContent?.trim() || null; const findText = (root, pattern) => [...root.querySelectorAll('span,div')] .filter((node) => node.children.length === 0) .map((node) => node.textContent?.trim()) .filter((value) => value && pattern.test(value)) .sort((a, b) => a.length - b.length)[0] || null; const rows = [...document.querySelectorAll('ul.srp-results > li.s-item, ul.srp-results > li.s-card')]; const listings = rows .map((row) => { const href = row.querySelector('a.s-item__link, a.s-card__link')?.href || null; const canonical = href ? href.split('?')[0] : null; const itemId = canonical?.match(/\\/itm\\/(\\d+)/)?.[1] || null; const titleNode = row.querySelector('.s-item__title > span:not(.LIGHT_HIGHLIGHT), .s-card__title .su-styled-text, .s-card__title'); const titleText = titleNode?.textContent?.trim().replace(/Opens in a new window or tab.*$/i, '') || null; const price = text('.s-item__price, .s-card__price', row); const range = price?.match(/(.+?)\\s+to\\s+(.+)/i); return { item_id: itemId, item_url: canonical, title: titleText, price, price_currency: price?.startsWith('£') ? 'GBP' : price?.startsWith('€') || price?.startsWith('EUR') ? 'EUR' : price?.startsWith('\\u0024') ? 'USD' : null, price_range: range ? { low: range[1].trim(), high: range[2].trim() } : null, buy_format: text('.s-item__purchase-options-with-icon', row) || findText(row, /Buy It Now|Best Offer|auction/i), condition: text('.SECONDARY_INFO, .s-card__subtitle', row), shipping: text('.s-item__shipping, .s-item__logisticsCost', row) || findText(row, /shipping|delivery/i), free_shipping: /free shipping|free delivery/i.test(findText(row, /free shipping|free delivery/i) || ''), location: text('.s-item__location', row) || findText(row, /Located in|from United/i), seller: text('.s-item__seller-info-text, .s-card__program-badge', row) || findText(row, /positive \\(.*\\)/i), bids: text('.s-item__bids', row), time_left: text('.s-item__time-left', row), sold_count: text('.s-item__hotness', row) || text('.s-item__quantitySold', row), thumbnail: row.querySelector('img.s-item__image-img, img.s-card__image')?.src || row.querySelector('img.s-item__image-img, img.s-card__image')?.dataset?.src || null, }; }) .filter((item) => item.item_url && item.item_id && item.title && item.title !== 'Shop on eBay'); const totalText = document.querySelector('.srp-controls__count-heading')?.textContent?.trim() || ''; const total = Number((totalText.match(/[\\d,]+/) || ['0'])[0].replace(/,/g, '')) || listings.length; return { success: true, method: 'browser', locale: location.hostname.replace(/^www\\./, ''), result_count: listings.length, total_available: total, listings, }; })()`); cliLog(JSON.stringify({ query, ...result }, null, 2)); EOF