the-focus-ai/chrome-driver

browser-automation

Automate Chrome browser via DevTools Protocol.

Ver código fuente
Documento original del Skill

Contenido del repositorio de origen con títulos, ejemplos, código, tablas, enlaces e imágenes preservados.

Browser Automation with Chrome DevTools Protocol

Control Chrome browser programmatically using simple command-line scripts. All scripts auto-start Chrome in headless mode if not running.

Global Options (All Commands)

All commands support these options for session persistence and interactive use:

  • --no-headless - Run Chrome with a visible window (required for interactive sites, login, etc.)
  • --user-data=PATH - Use a specific Chrome profile directory to preserve cookies, logins, and session state between runs

Example: Persistent session for logged-in sites

bash
# First time: login interactively with visible browser
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://instacart.com --no-headless --user-data=~/.chrome-instacart

# After logging in manually, future commands use same profile (cookies preserved)
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://instacart.com/store/market-32 --user-data=~/.chrome-instacart
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://instacart.com/account --user-data=~/.chrome-instacart /tmp/account.png

Available Commands

All scripts are in ${CLAUDE_PLUGIN_ROOT}/bin/:

screenshot - Capture web pages as images

bash
${CLAUDE_PLUGIN_ROOT}/bin/screenshot URL [OUTPUT] [OPTIONS]

Options:

  • --full-page - Capture entire scrollable page
  • --selector=CSS - Capture specific element
  • --format=png|jpeg|webp - Output format (default: png)
  • --quality=N - JPEG/WebP quality 0-100
  • --width=N --height=N - Set viewport size
  • --max-dimension=N - Max output dimension (default: 8000, auto-scales large pages)

Examples:

bash
# Basic screenshot
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/page.png

# Full page as JPEG
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/full.jpg --full-page --format=jpeg

# Capture specific element
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/header.png --selector="header"

pdf - Generate PDFs from web pages

bash
${CLAUDE_PLUGIN_ROOT}/bin/pdf URL [OUTPUT] [OPTIONS]

Options:

  • --paper=letter|a4|legal|a3|a5|tabloid - Paper size (default: letter)
  • --landscape - Landscape orientation
  • --margin=INCHES - All margins (default: 0.4)
  • --scale=FACTOR - Scale 0.1-2.0 (default: 1.0)
  • --no-background - Skip background colors/images

Examples:

bash
# Basic PDF
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/doc.pdf

# A4 landscape
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/report.pdf --paper=a4 --landscape

# Tight margins
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/compact.pdf --margin=0.25

extract - Extract content from web pages

bash
${CLAUDE_PLUGIN_ROOT}/bin/extract URL [OPTIONS]

Options:

  • --format=markdown|text|html - Output format (default: markdown)
  • --selector=CSS - Extract specific element only
  • --links - Also list all links
  • --images - Also list all images
  • --metadata - Also show page metadata

Examples:

bash
# Get page as markdown
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com

# Get plain text from article
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --format=text --selector="article"

# Get all links and metadata
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --links --metadata

navigate - Navigate and interact with pages

bash
${CLAUDE_PLUGIN_ROOT}/bin/navigate URL [OPTIONS]

Options:

  • --wait-for=SELECTOR - Wait for element to appear
  • --click=SELECTOR - Click an element
  • --type=SELECTOR=TEXT - Type text into input field
  • --eval=JAVASCRIPT - Execute JavaScript and print result
  • --timeout=SECONDS - Timeout (default: 30)

Examples:

bash
# Navigate and wait for content
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://example.com --wait-for="#content"

# Fill form and submit
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://google.com --type="input[name=q]=hello" --click="input[type=submit]"

# Get page title via JavaScript
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://example.com --eval="document.title"

form - Fill out and submit web forms

bash
${CLAUDE_PLUGIN_ROOT}/bin/form URL [OPTIONS]

Options:

  • --fill=SELECTOR=VALUE - Fill input field (can repeat)
  • --select=SELECTOR=VALUE - Select dropdown option (can repeat)
  • --fill-json='{"sel":"val"}' - Fill multiple fields from JSON
  • --submit=SELECTOR - Click submit button after filling
  • --wait-for=SELECTOR - Wait for element before filling
  • --wait-after=SELECTOR - Wait for element after submit
  • --screenshot=PATH - Take screenshot after completion

Examples:

bash
# Login form
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/login \
  --fill='#username=john' \
  --fill='#password=secret' \
  --submit='button[type=submit]'

# Form with dropdowns
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/register \
  --fill='#name=John Doe' \
  --fill='#email=john@example.com' \
  --select='#country=US' \
  --submit='#register-btn'

# Using JSON
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/contact \
  --fill-json='{"#name":"John","#email":"john@test.com"}' \
  --submit='button.send'

record - Record screencast frames

bash
${CLAUDE_PLUGIN_ROOT}/bin/record URL OUTPUT_DIR [OPTIONS]

Options:

  • --duration=SECONDS - Recording duration (default: 5)
  • --count=N - Exact number of frames to capture
  • --format=jpeg|png - Frame format (default: jpeg)
  • --quality=N - JPEG quality 0-100 (default: 80)
  • --max-width=N - Maximum frame width
  • --max-height=N - Maximum frame height
  • --fps=N - Approximate frames per second (default: 10)

Examples:

bash
# Record 5 seconds
${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames

# Record 30 PNG frames
${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames --count=30 --format=png

# Convert to video with ffmpeg
ffmpeg -framerate 10 -i /tmp/frames/frame-%04d.jpg -c:v libx264 output.mp4

interact - Interact with current page (no navigation)

bash
${CLAUDE_PLUGIN_ROOT}/bin/interact [OPTIONS]

This is the key command for multi-step workflows. Unlike navigate, this command connects to the already-open browser tab and performs actions WITHOUT reloading the page. Essential for:

  • Multi-step processes (search → click result → add to cart)
  • Single-page applications (SPAs)
  • Sites where navigation resets state

Options:

  • --click=SELECTOR - Click element matching CSS selector
  • --type=SELECTOR=TEXT - Type text into input field
  • --eval=JAVASCRIPT - Execute JavaScript and print result
  • --wait-for=SELECTOR - Wait for element to appear
  • --select=SELECTOR=VALUE - Select dropdown option
  • --focus=SELECTOR - Focus an element
  • --text=SELECTOR - Get text content of element
  • --timeout=SECONDS - Timeout (default: 30)

Examples:

bash
# Execute JavaScript on current page
${CLAUDE_PLUGIN_ROOT}/bin/interact --eval="document.title" --user-data=~/.chrome-mysite

# Click a button on current page
${CLAUDE_PLUGIN_ROOT}/bin/interact --click="#submit-btn" --user-data=~/.chrome-mysite

# Type into an input without navigating away
${CLAUDE_PLUGIN_ROOT}/bin/interact --type="#search=query" --user-data=~/.chrome-mysite

# Chain actions: focus, type, then click
${CLAUDE_PLUGIN_ROOT}/bin/interact --focus="#search" --type="#search=pork" --click="#search-btn" --user-data=~/.chrome-mysite

# Get text from an element
${CLAUDE_PLUGIN_ROOT}/bin/interact --text="h1.title" --user-data=~/.chrome-mysite

chrome-status - Check browser status

bash
${CLAUDE_PLUGIN_ROOT}/bin/chrome-status

Shows whether Chrome is running, version info, and open tabs.

Common Workflows

Screenshot a page

bash
${CLAUDE_PLUGIN_ROOT}/bin/screenshot https://example.com /tmp/screenshot.png

Convert page to PDF

bash
${CLAUDE_PLUGIN_ROOT}/bin/pdf https://example.com /tmp/document.pdf --paper=a4

Scrape page content

bash
${CLAUDE_PLUGIN_ROOT}/bin/extract https://example.com --format=markdown

Fill and submit a form

bash
${CLAUDE_PLUGIN_ROOT}/bin/form https://example.com/login \
  --fill='#username=user' \
  --fill='#password=pass' \
  --submit='button[type=submit]' \
  --wait-after='.dashboard'

Record a screencast

bash
${CLAUDE_PLUGIN_ROOT}/bin/record https://example.com /tmp/frames --duration=10
ffmpeg -framerate 10 -i /tmp/frames/frame-%04d.jpg -c:v libx264 video.mp4

Notes

  • Chrome auto-starts in headless mode when needed (use --no-headless for visible browser)
  • Chrome continues running between commands for speed
  • Use --user-data=PATH to preserve login/cookies between sessions
  • Use pkill -f 'chrome.*--remote-debugging-port' to stop Chrome manually
  • Default port is 9222; set CHROME_DRIVER_PORT to change
  • All scripts support --help for full usage info
  • Large screenshots are auto-scaled to fit within 8000px (API limit)

Session Management Tips

For sites requiring login (Instacart, Amazon, banking, etc.):

  1. Create a persistent profile: Use --user-data=~/.chrome-sitename consistently
  2. First-time login: Use --no-headless to see the browser and log in manually
  3. Subsequent automation: Same --user-data path preserves your session
  4. Multiple accounts: Use different --user-data paths for different accounts/sites

Multi-Step Workflow Guide: Shopping Sites (Instacart, Amazon, etc.)

This section walks through automating complex multi-step processes on sites that require login and multiple interactions. The key insight is using two commands together:

  • navigate - Go to a URL (use for initial navigation)
  • interact - Work with the current page without reloading (use for everything after)

Understanding the Two-Command Pattern

Why `navigate` alone isn't enough:

  • navigate ALWAYS loads/reloads the specified URL
  • Each navigate call is independent - it doesn't "continue" from the previous state
  • Multi-step workflows (search → select → add to cart) break because each step resets

Why `interact` is essential:

  • interact connects to the EXISTING browser tab
  • It performs actions on whatever page is currently displayed
  • The page state is preserved between interact calls
  • Perfect for SPAs and multi-step processes

Complete Example: Adding Items to Instacart Cart

Here's a real-world example that works:

Step 1: Initial Setup - Create Profile and Login

First time only - open browser visibly so user can log in:

bash
# Open Instacart with visible browser and persistent profile
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com \
  --no-headless \
  --user-data=~/.chrome-instacart

# USER ACTION: Log in manually in the browser window
# Your cookies/session will be saved to ~/.chrome-instacart

Step 2: Navigate to Store

After login, go to the desired store:

bash
# First, find the correct store URL by checking the homepage
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com/store \
  --no-headless \
  --user-data=~/.chrome-instacart

# Use JavaScript to find links containing your store name
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Market 32')).map(a => a.href).join('\n')" \
  --no-headless \
  --user-data=~/.chrome-instacart

# Navigate to the store (use the URL found above)
${CLAUDE_PLUGIN_ROOT}/bin/navigate https://www.instacart.com/store/price-chopper-ny/storefront \
  --no-headless \
  --user-data=~/.chrome-instacart

Step 3: Search for Product (Multi-Step with interact)

Now use interact to search WITHOUT navigating away:

bash
# Click the search input
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --click="#search-bar-input" \
  --no-headless \
  --user-data=~/.chrome-instacart

# Type the search query
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --type="#search-bar-input=pork shoulder" \
  --no-headless \
  --user-data=~/.chrome-instacart

# Submit the search using JavaScript (trigger form submit event)
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --eval="var input = document.querySelector('#search-bar-input'); var form = input.closest('form'); form.dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" \
  --no-headless \
  --user-data=~/.chrome-instacart

Step 4: View Search Results

Check what products are available:

bash
# Wait a moment for results to load, then check the page content
sleep 2

${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --eval="document.body.innerText.substring(0, 2000)" \
  --no-headless \
  --user-data=~/.chrome-instacart

Step 5: Add Product to Cart

Click the Add button for the desired product:

bash
# Find and click the first "Add" button
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --eval="var addBtns = Array.from(document.querySelectorAll('button')).filter(b => b.innerText.trim() === 'Add'); if (addBtns.length > 0) { addBtns[0].click(); 'Added to cart'; } else { 'No Add button found'; }" \
  --no-headless \
  --user-data=~/.chrome-instacart

Step 6: Verify Cart Updated

Confirm the item was added:

bash
# Check cart count or page state
${CLAUDE_PLUGIN_ROOT}/bin/interact \
  --eval="document.body.innerText.includes('2') ? 'Cart has 2 items' : document.body.innerText.substring(0, 500)" \
  --no-headless \
  --user-data=~/.chrome-instacart

Key Techniques for Multi-Step Automation

1. Finding Elements with JavaScript

When CSS selectors are complex or dynamic, use --eval to find elements:

bash
# Find all buttons with specific text
--eval="Array.from(document.querySelectorAll('button')).filter(b => b.innerText.includes('Add')).length"

# Find links by text content
--eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Checkout'))[0].href"

# Get input field IDs/names
--eval="JSON.stringify(Array.from(document.querySelectorAll('input')).map(i => ({id: i.id, name: i.name, placeholder: i.placeholder})))"

2. Clicking Elements via JavaScript

When --click doesn't work (dynamic elements, overlays, etc.):

bash
# Click by finding element with JS
--eval="document.querySelector('.add-to-cart-btn').click(); 'clicked'"

# Click first matching element
--eval="Array.from(document.querySelectorAll('button')).filter(b => b.innerText === 'Add')[0].click(); 'clicked'"

3. Submitting Forms

Different approaches for form submission:

bash
# Method 1: Dispatch submit event (most reliable for SPAs)
--eval="document.querySelector('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'"

# Method 2: Click submit button
--click="button[type=submit]"

# Method 3: Call submit() directly
--eval="document.querySelector('form').submit(); 'submitted'"

4. Waiting for Dynamic Content

Use --wait-for or JavaScript polling:

bash
# Wait for element to appear
--wait-for=".search-results"

# Or use JavaScript with timeout
--eval="new Promise(r => { let i = setInterval(() => { if (document.querySelector('.results')) { clearInterval(i); r('found'); }}, 100); setTimeout(() => { clearInterval(i); r('timeout'); }, 5000); })"

5. Reading Page State

Extract information to make decisions:

bash
# Get current URL
--eval="window.location.href"

# Check if logged in
--eval="document.body.innerText.includes('Sign Out') ? 'logged in' : 'not logged in'"

# Get cart count
--eval="document.querySelector('.cart-count')?.innerText || '0'"

# Get product prices
--eval="JSON.stringify(Array.from(document.querySelectorAll('.price')).map(p => p.innerText))"

Troubleshooting Multi-Step Workflows

Problem: Page resets between commands

Solution: Use interact instead of navigate for all steps after initial navigation.

Problem: Elements not found

Solution:

  1. Use --eval to inspect the page structure
  2. Wait for dynamic content with --wait-for or sleep
  3. Check if element is in an iframe

Problem: Click doesn't work

Solution:

  1. Try JavaScript click: --eval="document.querySelector('...').click()"
  2. Scroll element into view first
  3. Check for overlays blocking the element

Problem: Form doesn't submit

Solution:

  1. Use form submit event: form.dispatchEvent(new Event('submit', ...))
  2. Check if it's a SPA with custom form handling
  3. Look for and click the actual submit button

Problem: Login session lost

Solution:

  1. Always use same --user-data path
  2. Check if cookies expired
  3. Some sites require re-login periodically

Best Practices

  1. Always use `--user-data` for sites requiring login
  2. Use `--no-headless` during development to see what's happening
  3. Start with `navigate` to load the initial page
  4. Switch to `interact` for all subsequent actions on that page
  5. Use `--eval` liberally to inspect page state and find elements
  6. Add `sleep` between actions if the site needs time to update
  7. Check results after each step to verify the action worked

Instacart Automation Guide (Tested & Proven Patterns)

This section contains battle-tested patterns for automating Instacart shopping. These patterns were developed through extensive real-world testing.

Quick Reference: Essential Commands

bash
# Base command pattern (always use these flags)
INTERACT="${CLAUDE_PLUGIN_ROOT}/bin/interact --no-headless --user-data=~/.chrome-instacart"
NAVIGATE="${CLAUDE_PLUGIN_ROOT}/bin/navigate --no-headless --user-data=~/.chrome-instacart"

# Suppress stderr noise
$INTERACT --eval="..." 2>/dev/null

Store Selection

CRITICAL: Never construct Instacart URLs manually. Always navigate via the UI:

bash
# Go to homepage first
$NAVIGATE "https://www.instacart.com"

# Find store links on page
$INTERACT --eval="Array.from(document.querySelectorAll('a')).filter(a => a.innerText.includes('Stop & Shop')).map(a => ({text: a.innerText, href: a.href}))"

# Click the store link found above (or navigate to storefront)
$NAVIGATE "https://www.instacart.com/store/stop-shop/storefront"

Search Pattern (Reliable 3-Step Process)

bash
# Step 1: Clear existing search (IMPORTANT - prevents concatenation)
$INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null

# Step 2: Type search term
$INTERACT --type="#search-bar-input=pork shoulder" 2>/dev/null

# Step 3: Submit form (dispatchEvent is most reliable for SPAs)
$INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null && sleep 3

Combined one-liner for efficiency:

bash
$INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null && \
$INTERACT --type="#search-bar-input=SEARCH_TERM" 2>/dev/null && \
$INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null && sleep 3

Product Discovery: Finding Available Items

List available "Add" buttons (shows product names):

bash
$INTERACT --eval="JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*=\"Add\"]')).slice(0,8).map(b => b.getAttribute('aria-label')))" 2>/dev/null

Get page content to see prices and sizes:

bash
$INTERACT --eval="document.body.innerText.substring(0, 4000)" 2>/dev/null

Adding Products to Cart

Instacart uses aria-labels for product buttons. Key patterns:

ActionAria-label Pattern
Add itemAdd 1 ct [Product Name] or Add 1 lb [Product Name]
IncrementIncrement quantity of [Product Name]
DecrementDecrement quantity of [Product Name]
RemoveRemove [Product Name]

Add a product (exact match):

bash
$INTERACT --eval="var btn = document.querySelector('button[aria-label=\"Add 1 ct Store Brand Baby Spinach\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null

Add a product (partial match - more flexible):

bash
$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"Spinach\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null

Quantity Management

Increment quantity (add more of same item):

bash
$INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Avocado\"]'); if(btn) { btn.click(); 'Incremented'; } else { 'Not found'; }" 2>/dev/null

Increment multiple times (e.g., get 6 avocados):

bash
# Add first one, then increment 5 times
$INTERACT --eval="document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"Avocado\"]').click(); 'Added 1'" 2>/dev/null && sleep 1
for i in 1 2 3 4 5; do
  sleep 0.5
  $INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Avocado\"]'); if(btn) btn.click(); 'Inc'" 2>/dev/null
done

JavaScript loop for multiple increments (faster):

bash
$INTERACT --eval="for(let i=0;i<5;i++){var btn=document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"Lemon\"]');if(btn)btn.click();} 'Incremented to 6'" 2>/dev/null

Remove item from cart:

bash
$INTERACT --eval="var btn = document.querySelector('button[aria-label=\"Remove Bertolli Original Extra Virgin Olive Oil\"]'); if(btn) { btn.click(); 'Removed'; } else { 'Not found'; }" 2>/dev/null

Cart Management

View cart (click cart badge):

bash
$INTERACT --eval="var badge = Array.from(document.querySelectorAll('*')).find(el => /^\\d+$/.test(el.innerText.trim()) && parseInt(el.innerText) > 10 && el.offsetWidth < 50); if(badge) { var parent = badge.closest('a') || badge.closest('button') || badge.parentElement.parentElement; if(parent) parent.click(); 'clicked cart'; } else { 'cart not found'; }" 2>/dev/null && sleep 2

Get cart total:

bash
$INTERACT --eval="document.body.innerText.match(/\\$\\d{2,3}\\.\\d{2}/) ? document.body.innerText.match(/\\$\\d{2,3}\\.\\d{2}/)[0] : 'no total found'" 2>/dev/null

List all items in cart with prices (when cart panel is open):

bash
$INTERACT --eval="document.body.innerText.substring(5500, 12000)" 2>/dev/null

Find all remove/decrement buttons in cart:

bash
$INTERACT --eval="Array.from(document.querySelectorAll('button')).filter(b => b.getAttribute('aria-label') && (b.getAttribute('aria-label').includes('Remove') || b.getAttribute('aria-label').includes('Decrement'))).slice(0,20).map(b => b.getAttribute('aria-label')).join(' | ')" 2>/dev/null

Complete Shopping Workflow

Efficient pattern for adding multiple items:

bash
# Function to search and add item
add_item() {
  local search="$1"
  local product="$2"
  local qty="${3:-1}"

  # Search
  $INTERACT --eval="var input = document.querySelector('#search-bar-input'); input.select(); document.execCommand('delete'); 'cleared'" 2>/dev/null
  $INTERACT --type="#search-bar-input=$search" 2>/dev/null
  $INTERACT --eval="document.querySelector('#search-bar-input').closest('form').dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); 'submitted'" 2>/dev/null
  sleep 3

  # Add
  $INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Add\"][aria-label*=\"$product\"]'); if(btn) { btn.click(); 'Added'; } else { 'Not found'; }" 2>/dev/null
  sleep 1

  # Increment if qty > 1
  if [ "$qty" -gt 1 ]; then
    for i in $(seq 2 $qty); do
      sleep 0.5
      $INTERACT --eval="var btn = document.querySelector('button[aria-label*=\"Increment\"][aria-label*=\"$product\"]'); if(btn) btn.click();" 2>/dev/null
    done
  fi
}

# Usage examples:
add_item "avocados" "Hass Avocado" 6
add_item "baby spinach" "Store Brand Baby Spinach" 2
add_item "bell peppers" "Green Bell Pepper" 2

Troubleshooting Instacart-Specific Issues

Problem: Search terms concatenate (e.g., "lemonsfresh thyme")

  • Cause: Previous search text not cleared
  • Solution: Always clear with input.select(); document.execCommand('delete') before typing

Problem: "Add" button not found

  • Cause: Product name in aria-label doesn't match search
  • Solution:
  1. List available buttons: JSON.stringify(Array.from(document.querySelectorAll('button[aria-label*="Add"]')).slice(0,8).map(b => b.getAttribute('aria-label')))
  2. Use partial match: aria-label*="Add"][aria-label*="partial name"

Problem: Store not available at address

  • Cause: Instacart shows different stores based on delivery address
  • Solution: Check which stores are available at current address, or change address

Problem: Cart shows wrong item count

  • Cause: Some items are sold by weight (e.g., meat shows as 1 item but weighs 5 lbs)
  • Solution: This is normal - check cart details for actual quantities

Problem: Prices differ between stores

  • Cause: Each store has independent pricing
  • Solution: Compare carts between stores, swap expensive items for alternatives

Product Selection Tips

  1. Prefer Store Brand - Usually cheapest option, search results often show store brand
  2. Check sizes - "16 oz" vs "50.7 oz" significantly affects price
  3. Value packs - Look for "Value Pack" in product names for better per-unit pricing
  4. Per-pound items - Meat/produce sold by weight; qty is weight, not count
  5. Watch for sales - "With loyalty card" prices shown in results

Key Lessons Learned

  1. Always use `interact` after initial `navigate` - Instacart is an SPA; navigation resets state
  2. Clear search input before new searches - Prevents concatenation bugs
  3. Use aria-label selectors - Most reliable way to find Instacart buttons
  4. Add `2>/dev/null` - Suppresses noisy stderr output from Chrome driver
  5. Add `sleep 3` after search submit - Allow time for results to load
  6. Partial aria-label matches (`=`)** - More robust than exact matches
  7. Check cart periodically - Verify items added correctly
  8. Store availability varies by address - Not all stores deliver everywhere