Skill: bewithgaurav/zepto (version 1.0.6)

--- name: zepto description: Order groceries from Zepto in seconds. Just say what you need, get a payment link on WhatsApp, pay on your phone, done. Remembers your usual items. Works across India where Zepto delivers. metadata: {"openclaw":{"emoji":"🛒","requires":{"config":["browser.enabled"]}}} --- # zepto **Order groceries from Zepto in 30 seconds. From chat to checkout.** Tell your AI what you need. It shops, generates a payment link, sends it to WhatsApp. You pay on your phone. Groceries arrive in 10 minutes. ## 💬 Examples **Quick orders:** ``` "Order milk and bread from Zepto" "Add vegetables - tomatoes, onions, potatoes" "Get me Amul butter and cheese" ``` **Your usuals:** ``` "Add my usual milk" → AI picks the brand you always order "Order the usual groceries" → AI suggests your frequent items ``` **Full shopping list:** ``` "Add milk, bread, eggs, coriander, ginger, and tea bags" → AI adds everything, shows total: ₹X → Sends payment link to WhatsApp → You pay, groceries arrive ``` --- ## 🔒 Security & Privacy **What this skill does:** - ✅ Browser automation on zepto.com (your local browser, your session) - ✅ Stores order history locally in `~/.openclaw/skills/zepto/order-history.json` (local file, not shared) - ✅ Sends payment links via WhatsApp (requires your consent for each order) - ✅ All authentication happens through Zepto's official flow (Phone + OTP) **What this skill does NOT do:** - ❌ No automatic payments (you must click the link and pay manually) - ❌ No data sent to external servers (except Zepto.com and WhatsApp via your channels) - ❌ No persistent background jobs (optional one-time order status check only if you approve) - ❌ No storage of payment info or OTPs - ❌ No access to your banking/UPI apps **Data Storage:** - Order history: `~/.openclaw/skills/zepto/order-history.json` (local only, helps with "usuals" feature) - Browser session: Managed by OpenClaw's browser (standard Chrome/Chromium profile) **User Control:** - You control when to order - You approve each payment link - You can delete order history file anytime - All browser actions happen in your profile with your visibility --- ## 🚨 CRITICAL WORKFLOW RULES **ALWAYS follow this order when building an order:** ### Rule 1: CHECK CART FIRST ```bash # Before adding ANY items, ALWAYS check cart state node zepto-agent.js get-cart ``` **Why:** Cart may have items from previous sessions. Adding duplicates is wasteful. ### Rule 2: Use smart-shop (RECOMMENDED) ```bash # This handles everything: clears unwanted, checks duplicates, adds missing node zepto-agent.js smart-shop "milk, bread, eggs" ``` **What it does:** 1. Checks current cart state 2. Clears existing items (if any) 3. For each item: checks if already in cart → skips if present → adds only if missing 4. Returns: `{ added: [], skipped: [], failed: [] }` ### Rule 3: NEVER take screenshots unless snapshot data is insufficient - Snapshot shows all refs, buttons, text - Screenshot is ONLY for visual debugging when snapshot is truncated or unclear - **In 99% of cases, snapshot is enough** ### Rule 4: Detect "already in cart" signals When you see in snapshot: ``` "Decrease quantity 1 Increase quantity" → Item is IN CART button "Remove" [ref=eXX] → Item is IN CART ``` **DO NOT** click "ADD" when you see these signals! --- ## Complete Flow 1. **Authentication** - Phone + OTP verification 2. **Address Confirmation** - Verify delivery location 3. **Shopping** - Search & add items (with YOUR usuals prioritized!) 4. **Payment Link** - Generate & send Juspay link via WhatsApp --- ## Step 0: Order History & Usuals **Your order history is tracked in:** `{SKILL_DIR}/order-history.json` (Where `{SKILL_DIR}` is your skill directory, typically `~/.openclaw/skills/zepto/`) **Smart Selection Logic:** 1. When user requests an item (e.g., "add milk") 2. Check `order-history.json` for that category 3. **If ordered 2+ times** → Auto-add your most-ordered variant 4. **If ordered 0-1 times** → Show options and ask for selection ### Automated Order History Scraper **When to run:** User says "update my zepto history" or "refresh order history" **Process:** 1. Navigate to account page 2. Get all delivered order URLs 3. Visit each order sequentially 4. Extract items using DOM scraping 5. Build frequency map 6. Save to `order-history.json` **Implementation:** ```bash # Step 1: Navigate to account page browser navigate url=https://www.zepto.com/account profile=openclaw # Step 2: Extract order URLs browser act profile=openclaw request='{"fn":"() => { const orders = []; document.querySelectorAll(\"a[href*=\\\"/order/\\\"]\").forEach(link => { if (link.href.includes(\"isArchived=false\") && link.textContent.includes(\"delivered\")) { orders.push(link.href); } }); return [...new Set(orders)]; }", "kind":"evaluate"}' # Returns array of order URLs # Step 3: For each order URL: browser navigate url={order_url} profile=openclaw # Step 4: Extract items from order page browser act profile=openclaw request='{"fn":"() => { const items = []; document.querySelectorAll(\"*\").forEach(el => { const text = el.textContent; if (text.match(/\\d+\\s*unit/i)) { const parent = el.closest(\"div\"); if (parent) { const lines = parent.textContent.split(\"\\n\").map(l => l.trim()).filter(l => l && l.length > 5 && l.length < 100); if (lines[0]) { const qtyMatch = text.match(/(\\d+)\\s*unit/i); items.push({ name: lines[0], quantity: qtyMatch ? parseInt(qtyMatch[1]) : 1 }); } } } }); const uniqueItems = {}; items.forEach(item => { if (!uniqueItems[item.name]) uniqueItems[item.name] = item; }); return Object.values(uniqueItems); }", "kind":"evaluate"}' # Returns array of {name, quantity} # Step 5: Aggregate all items into frequency map # Build JSON structure with counts # Step 6: Write to file write path={SKILL_DIR}/order-history.json content={json_data} ``` **Automated scraper advantages:** - ✅ No manual screenshot review - ✅ Faster (visits all orders programmatically) - ✅ Always up-to-date - ✅ Can re-run anytime **Example:** ``` User: "Update my Zepto order history" Response: "🔍 Scanning your Zepto orders... 📦 Found 6 delivered orders 🔄 Extracting items... ✅ Updated! Found: - Coriander: 4 orders - Milk: 3 orders - Bread: 2 orders - Potato: 2 orders + 15 other items Your usuals are ready!" ``` --- **Smart Selection Logic (Using History):** **Example:** ``` User: "Add milk" [Check order-history.json] → "Amul Taaza Toned Fresh Milk | Pouch (500ml)" ordered 3x Response: "🥛 Adding your usual milk! Amul Taaza Toned Fresh Milk (500ml) - ₹29 📊 You've ordered this 3 times ✅ Added to cart" ``` **If only ordered once or never:** ``` User: "Add milk" [Check order-history.json] → "Amul Taaza" ordered 1x only Response: "🥛 Found some milk options: 1. Amul Taaza Toned (500ml) - ₹29 ⭐ 4.8 (100k) - You've ordered this once 2. Amul Gold (1L) - ₹68 ⭐ 4.9 (80k) - Most popular 3. Mother Dairy (500ml) - ₹30 ⭐ 4.7 (60k) Which one? (or tell me a number)" ``` **Update order history:** After each successful order, update the JSON file with new items. --- ## Step 1: Authentication (First Time Only) **Check if already logged in:** ```bash browser open url=https://www.zepto.com profile=openclaw browser snapshot --interactive profile=openclaw # Look for "login" button vs "profile" link ``` **If NOT logged in, start auth flow:** ### 1.1: Get Phone Number Ask user: "What's your phone number for Zepto? (10 digits)" ### 1.2: Enter Phone & Request OTP ```bash # Click login button browser act profile=openclaw request='{"kind":"click","ref":"{login_button_ref}"}' # Type phone number browser act profile=openclaw request='{"kind":"type","ref":"{phone_input_ref}","text":"{phone}"}' # Click Continue browser act profile=openclaw request='{"kind":"click","ref":"{continue_button_ref}"}' ``` ### 1.3: Get OTP from User Ask user: "I've sent the OTP to {phone}. What's the OTP you received?" ### 1.4: Enter OTP ```bash browser snapshot --interactive profile=openclaw # Get OTP input refs browser act profile=openclaw request='{"kind":"type","ref":"{otp_input_ref}","text":"{otp}"}' # OTP auto-submits after 6 digits ``` **Result:** User is now logged in! Session persists across browser restarts. --- ## Step 2: Address Confirmation **🚨 CRITICAL: ALWAYS CHECK ADDRESS BEFORE PROCEEDING WITH ANY SHOPPING!** ### Address Selection Rules **Default behavior:** 1. Most users have multiple saved addresses (Home, Office, etc.) 2. **ALWAYS show current address and ASK for confirmation** - never assume 3. Check what was used in the last order (if order history exists) 4. Wait for explicit user confirmation before proceeding **On homepage, address is visible in the header:** ```bash browser snapshot --interactive profile=openclaw # Look for button with heading level=3 containing the address # Example ref: e16 with text like "Home - [Address Details]..." # Delivery time shown nearby (e.g., "10 minutes") ``` **ALWAYS ask user to confirm before shopping:** ``` 📍 I see your delivery address is set to: {Address Name} - {Full Address} ⏱️ Delivery in ~{X} minutes Is this correct? Should I proceed with this address? ``` ### Programmatic Address Selection (NEW!) **Use the `zepto-agent.js select-address` command:** ```bash node zepto-agent.js select-address "Home" node zepto-agent.js select-address "sanskar" # Fuzzy matching works! node zepto-agent.js select-address "kundu blr" ``` **How it works:** 1. **Fuzzy matching** - Case-insensitive, partial match supported - "sanskar" → "Sanskar Blr" ✅ - "home" → "New Home" ✅ - "kundu" → "Kundu Blr" ✅ 2. **Already-selected detection** - Skips if you're already at that address 3. **Verification** - Confirms address change in header after click **Example:** ```bash # Current address: "Kundu Blr" node zepto-agent.js select-address "sanskar" # Output: # ℹ️ Opening Zepto... # ✅ Zepto opened # ℹ️ 📍 Selecting address: "sanskar" # ℹ️ Current: Kundu Blr # ✅ Clicked: Sanskar BlrA-301, A, BLOCK-B... # 🎉 Address changed to: Sanskar blr ``` **When user says "change address to X" or "deliver to X":** ```bash # Just call the command with their address name/query node zepto-agent.js select-address "{user_query}" ``` **No manual modal navigation needed!** The script handles: - Opening the address modal - Finding the address (fuzzy match) - Clicking it - Verifying the change - Closing the modal **Manual Selection (Fallback):** If the programmatic method fails or address isn't found: ```bash # Click the address button (ref e16 or similar) browser act profile=openclaw request='{"kind":"click","ref":"e16"}' # This opens address selection modal with all saved addresses ``` **Select address using JavaScript:** ```bash # Replace {USER_ADDRESS_NAME} with the actual address name user selected browser act profile=openclaw request='{"fn":"() => { const input = document.querySelector('input[placeholder*=\"address\"]'); if (!input) return { error: 'Modal not found' }; let modal = input; for (let i = 0; i < 15; i++) { if (!modal.parentElement) break; modal = modal.parentElement; if (window.getComputedStyle(modal).position === 'fixed') break; } const divs = Array.from(modal.querySelectorAll('div')); const match = divs.find(d => d.textContent && d.textContent.trim().startsWith('{USER_ADDRESS_NAME}')); if (!match) return { error: 'Address not found' }; let p = match; for (let i = 0; i < 10; i++) { if (!p) break; const s = window.getComputedStyle(p); if (p.onclick || p.getAttribute('onClick') || s.cursor === 'pointer') { p.scrollIntoView({ block: 'center' }); setTimeout(() => {}, 300); p.click(); return { clicked: true, text: match.textContent.substring(0, 100) }; } p = p.parentElement; } return { error: 'No clickable parent' }; }()","kind":"evaluate"}' ``` **After address confirmed by user:** ``` ✅ Delivery address confirmed: {address_name} 📍 {full_address} ⏱️ ETA: {eta} mins Ready to shop! What would you like to add to cart? ``` **⚠️ Address is CRITICAL - never skip this step!** --- ## Step 3: Shopping ### 3A: Discovery Mode (Browse & Explore) When user asks to "explore", "show me", "what's good", "find something", or "discover": **Common Discovery Patterns:** - "Show me healthy snacks under ₹50" - "What's good in dairy products?" - "Find me something for breakfast" - "Any deals on fruits?" - "Discover protein bars" **Browse Categories:** ```bash # Navigate to category pages browser navigate url=https://www.zepto.com profile=openclaw browser snapshot --interactive profile=openclaw # Categories available on homepage: # - Fruits & Vegetables # - Dairy, Bread & Eggs # - Munchies (snacks) # - Cold Drinks & Juices # - Breakfast & Sauces # - Atta, Rice, Oil & Dals # - Cleaning Essentials # - Bath & Body # - Makeup & Beauty ``` **Filter & Sort:** ```bash # Example: Browse "Munchies" category browser navigate url=https://www.zepto.com/pn/munchies profile=openclaw browser snapshot --interactive profile=openclaw # Take screenshot to show user the options browser screenshot profile=openclaw ``` **Discovery Response Format:** ``` 🔍 Found some great options in {category}: 1. **{Product Name}** - ₹{price} ({discount}% OFF) ⭐ {rating} ({review_count} reviews) 📦 {size/quantity} 2. **{Product Name}** - ₹{price} ⭐ {rating} ({review_count} reviews) 3. **{Product Name}** - ₹{price} ({discount}% OFF) ⭐ {rating} ({review_count} reviews) Want me to add any of these? Just tell me the number(s)! ``` **Smart Filtering Tips:** - Price range: Extract from query ("under ₹50", "below 100") - Discount focus: Look for items with ₹X OFF tags - High ratings: Prioritize 4.5+ star products - Popular items: Sort by review count (k = thousands) - Health focus: Keywords like "protein", "sugar-free", "organic", "millet" **Interactive Discovery:** After showing options, user can: - Add by number: "Add 1 and 3" - Ask for more: "Show me more" - Refine: "Show cheaper options" or "What about chocolate flavors?" - Browse different category: "Now show me dairy products" ### 3B: Direct Search (Specific Items) **MANDATORY PRE-FLIGHT CHECK:** Before adding ANY items: 1. Click cart button 2. Read current cart contents 3. If cart has items: Ask user "Keep existing items or clear cart first?" 4. If empty: Proceed to shopping **Multi-Item Shopping Flow:** When user gives a list (e.g., "add milk, butter, bread"): 1. **Add items ONE AT A TIME with verification:** - Search for item - Click ADD button - Wait 0.5s for page update - VERIFY item shows quantity controls (means it's in cart) - If verification fails: Retry up to 3 times 2. **Then show final cart summary** with all items and total **CRITICAL:** Never batch-add without verification! Page refs change after each add. **Item Selection Logic:** - Check order-history.json first - If item ordered 2+ times → auto-select that variant - If item ordered 0-1 times or multiple unclear variants → **show options and ASK** - Pick closest match to user's request (e.g., "Yakult Light" when they said "light") - Use highest review count as tiebreaker **When UNCLEAR about variant:** ``` 🥛 Found multiple milk options: 1. Amul Taaza (500ml) - ₹29 ⭐ 4.8 (100k) 2. Amul Gold (1L) - ₹68 ⭐ 4.9 (80k) 3. Mother Dairy (500ml) - ₹30 ⭐ 4.7 (60k) Which one? (or tell me a number) ``` **Search Process:** ```bash browser navigate url=https://www.zepto.com/search?query={item} profile=openclaw browser snapshot --interactive profile=openclaw ``` ### Select Best Product **Rule:** Pick product with highest review count (unless order history says otherwise). Format: `{rating} ({count})` where k=thousand, M=million. Example: "4.8 (694.4k)" = 694,400 reviews = most popular. ### Add to Cart ```bash browser act profile=openclaw request='{"kind":"click","ref":"{ADD_button_ref}"}' ``` ### View Cart Summary (ALWAYS show after adding all items) ```bash browser navigate url=https://www.zepto.com/?cart=open profile=openclaw browser snapshot profile=openclaw # Get cart summary ``` **Cart Summary Format:** ``` 🛒 Added to cart: 1. Item 1 - ₹XX 2. Item 2 - ₹YY 3. Item 3 - ₹ZZ 💰 Total: ₹{total} Ready to checkout? (say "yes" or "checkout" or "lessgo") ``` **CRITICAL - Quantity Mapping:** When user provides a shopping list with quantities (e.g., "3x jeera, 2x saffola oats"): 1. **ALWAYS create a mapping file FIRST** before any cart operations 2. Map each item name to its requested quantity 3. Before removing/modifying items, **verify against this mapping** 4. Never assume which item has which quantity - CHECK THE MAPPING Example mapping: ```json { "jeera": 3, "saffola_oats": 2, "milk": 1 } ``` **Before removing duplicates or adjusting quantities:** - Take a cart snapshot - Match cart items to your mapping by name similarity - Verify quantities match the original request - If unsure, ASK the user before making changes ### Error Handling - Out of Stock **If item not found or out of stock:** ``` ❌ {item} is currently unavailable. 🔍 Suggestions: - {similar_item_1} - {similar_item_2} What would you like instead? ``` **Don't auto-add alternatives** - wait for user's next item or choice. --- ## Step 4: Generate Payment Link After all items added to cart and user confirms checkout: ### 4.1: Open Cart and Proceed to Payment ```bash # Open cart modal browser act profile=openclaw request='{"kind":"click","ref":"{cart_button_ref}"}' # Example ref from homepage: e44 # Wait for cart to open, take snapshot browser snapshot --interactive profile=openclaw # Click "Click to Pay ₹{amount}" button browser act profile=openclaw request='{"kind":"click","ref":"{click_to_pay_button_ref}"}' # Example ref: e3579 ``` ### 4.2: Extract Juspay Link ```bash # Wait 2 seconds for navigation to complete browser act profile=openclaw request='{"fn":"async () => { await new Promise(r => setTimeout(r, 2000)); return window.location.href; }","kind":"evaluate"}' ``` **URL Format:** ``` https://payments.juspay.in/payment-page/signature/zeptomarketplace-{order_id} ``` Example: ``` https://payments.juspay.in/payment-page/signature/zeptomarketplace-{ORDER_ID_EXAMPLE} ``` ### 4.3: Send Link via WhatsApp ```bash message action=send channel=whatsapp target={user_phone} message="🛒 *Your Zepto order is ready!* *Cart Summary ({item_count} items):* 1. {item1} - ₹{price1} 2. {item2} - ₹{price2} 3. {item3} - ₹{price3} *💰 Total: ₹{total}* 📍 Delivering to: {address_name} - {address} ⏱️ ETA: {eta} minutes *🔗 Click here to pay:* {juspay_payment_link} ⚠️ *IMPORTANT: After payment, message me \"DONE\" to confirm your order!* (Don't rely on the payment page - just tell me when you've paid and I'll verify it) 🚀" ``` ### 4.4: Wait for User "Done" Message & Verify Order **After user says "done" or "paid":** **Step 1: Navigate to Zepto homepage to check order status** ```bash browser navigate url=https://www.zepto.com profile=openclaw browser snapshot --interactive profile=openclaw ``` **Step 2: Look for order confirmation** Check for text like: - "Your order is on the way" - "Order confirmed" - "Preparing your order" - "Arriving in X mins" - Track order button/link **Step 3: Auto-clear cart (Post-Payment Behavior)** 🚨 **CRITICAL: After payment, cart items persist because Zepto hasn't synced yet!** **Automatically clear cart without asking (user expects cart to be empty after payment):** ```bash # Open cart browser act profile=openclaw request='{"kind":"click","ref":"{cart_button_ref}"}' browser snapshot --interactive profile=openclaw # Click Remove button for each item browser act profile=openclaw request='{"kind":"click","ref":"{remove_button_ref_1}"}' browser act profile=openclaw request='{"kind":"click","ref":"{remove_button_ref_2}"}' browser act profile=openclaw request='{"kind":"click","ref":"{remove_button_ref_3}"}' # ... repeat for all items ``` **Step 4: Confirm to user** **If order confirmed:** ``` ✅ *Payment confirmed!* 🚚 Your order is on the way! Arriving in ~{X} mins. Order details: - {item_count} items, ₹{total} - Delivery to: {address} ✅ Cart cleared ({item_count} items removed from previous order) 🛒 Ready for your next order! 🐺 ``` **If order NOT showing yet:** ``` ⏳ Payment processed, but order confirmation is still loading on Zepto's end. Let me check again in 30 seconds... ``` **Then set up a background check to try again.** **Step 1: Navigate back to Zepto homepage** ```bash browser navigate url=https://www.zepto.com profile=openclaw ``` **Step 2: Check order status on homepage** ```bash browser snapshot --interactive profile=openclaw # Look for "Your order is on the way" or order tracking ``` **Step 3: Open cart and check items** ```bash browser act profile=openclaw request='{"kind":"click","ref":"{cart_button_ref}"}' browser snapshot --interactive profile=openclaw ``` **🚨 CRITICAL: Cart items may still be there because Zepto hasn't synced order confirmation yet!** **Step 4: Ask user about clearing cart** ``` ✅ Payment confirmed! Your order is on the way. ⚠️ I can see {X} items still in the cart (from the previous order that just went through). Should I: 1. Clear the cart (recommended for fresh start) 2. Keep the items (if you want to reorder them) *Default: I'll clear the cart unless you say "keep it"* ``` **Step 5: Clear cart if user approves (or by default)** ```bash # For each item in cart, click Remove button browser act profile=openclaw request='{"kind":"click","ref":"{remove_button_ref_1}"}' browser act profile=openclaw request='{"kind":"click","ref":"{remove_button_ref_2}"}' # ... repeat for all items # Or use JavaScript to clear all at once: browser act profile=openclaw request='{"fn":"() => { const removeButtons = document.querySelectorAll(\"button\"); let count = 0; for (let btn of removeButtons) { if (btn.textContent.trim() === \"Remove\") { btn.click(); count++; } } return `Removed ${count} items`; }","kind":"evaluate"}' ``` **Confirmation message:** ``` ✅ Cart cleared! ({X} items removed) 🛒 Ready for your next order! Your current order ({item_count} items, ₹{total}) will arrive in ~{eta} mins. ``` **If user says "keep it":** ``` ✅ Got it! Keeping {X} items in cart. 🛒 Ready to add more items or proceed with these? ``` --- 2. Going to cart manually and clicking "Pay" 3. Let me know if you need me to try again ``` **If delivery address becomes unserviceable:** ``` ⚠️ Your delivery address is currently unserviceable. Should I order it to a different address? (I can show you all your saved addresses) ``` --- ## 🎯 Complete Order Flow Summary ### Before Starting ANY New Order (Normal Flow - No Recent Payment): **1. Check Address (ALWAYS)** ``` 📍 Current address: {address} Is this correct? ``` **2. Check Cart (if items exist)** ```bash # Open cart browser act profile=openclaw request='{"kind":"click","ref":"{cart_button_ref}"}' browser snapshot --interactive profile=openclaw ``` **If items in cart from NORMAL browsing (not post-payment):** ``` ⚠️ I see {X} items in your cart: 1. {item1} - ₹{price1} 2. {item2} - ₹{price2} Should I: 1. Clear the cart 2. Keep these items What would you like? ``` **Wait for user response before proceeding.** --- ### Post-Payment Behavior (After User Says "Done" or "Paid"): **This is DIFFERENT from normal flow - auto-clear expected!** **1. Navigate to zepto.com and check order status** ```bash browser navigate url=https://www.zepto.com profile=openclaw browser snapshot --interactive profile=openclaw ``` **2. Look for "Your order is on the way" or "Arriving in X mins"** **3. Open cart and AUTO-CLEAR without asking** ```bash # Open cart browser act profile=openclaw request='{"kind":"click","ref":"{cart_button_ref}"}' # Remove all items (they're from the order that just went through) browser act profile=openclaw request='{"kind":"click","ref":"{remove_ref_1}"}' browser act profile=openclaw request='{"kind":"click","ref":"{remove_ref_2}"}' browser act profile=openclaw request='{"kind":"click","ref":"{remove_ref_3}"}' ``` **4. Confirm to user** ``` ✅ Payment confirmed! Your order is on the way! Arriving in ~{X} mins. ✅ Cart cleared ({item_count} items removed from previous order) 🛒 Ready for your next order! ``` **Why auto-clear in post-payment?** - User expects cart to be empty after successful order - Cart items are from the order they just paid for - Zepto hasn't synced yet, so items persist temporarily - Clearing prevents confusion and duplicate orders --- ### Start Fresh Shopping (After Cart Cleared) ``` ✅ Cart cleared! ✅ Address confirmed: {address} What would you like to order? 🛒 ``` --- **Key Difference:** - **Normal flow**: ASK before clearing cart (user might want those items) - **Post-payment flow**: AUTO-CLEAR cart (user knows those items are ordered) --- ## Safety & Best Practices ✅ **DO:** - Check auth status before every order - Confirm address with user - Extract payment link accurately - Send link via WhatsApp - Let user complete payment ❌ **DON'T:** - Never click "Pay" button - Never store OTP - Never auto-submit payment - Never change address without user confirmation --- ## Error Handling **Phone number invalid:** ``` "Phone number should be 10 digits. Please try again." ``` **OTP verification failed:** ``` "OTP verification failed. Let me resend the OTP. Check your phone for the new code." ``` **Location not serviceable:** ``` "⚠️ Your location is currently not serviceable by Zepto. Store might be temporarily closed or location outside delivery zone. Want to try a different address?" ``` **Item not found:** ``` "Couldn't find {item} on Zepto. Try a different search term?" ``` --- ## Session Persistence **After successful authentication:** - Browser cookies persist login - No need to re-authenticate for future orders - Address selection persists - Can directly proceed to shopping **To check if authenticated:** ```bash browser navigate url=https://www.zepto.com profile=openclaw browser snapshot --interactive profile=openclaw # If "profile" link exists → logged in # If "login" button exists → need to auth ```

path: PARSER-USAGE.md
content: # zepto-parser.js - Usage Guide

## What It Does
Parses verbose Zepto browser snapshots and extracts ONLY the data you need.

**Token savings: ~95%** (2000 chars → 50 chars of JSON)

## Commands

### 1. Extract UI Refs
```bash
echo "$SNAPSHOT" | node zepto-parser.js refs
```

**Output:**
```json
{
  "search": "e3",
  "cart": "e5",
  "checkout": "e76"
}
```

### 2. Find Product
```bash
echo "$SNAPSHOT" | node zepto-parser.js product "milk"
```

**Output:**
```json
{
  "name": "Amul Taaza Toned Fresh Milk | Pouch",
  "ref": "e36",
  "price": 29,
  "buyAgain": true,
  "addButtonRef": "e37",
  "matchScore": 1.3
}
```

**Features:**
- Fuzzy matching (finds "Amul Taaza" when you search "milk")
- Boosts "Buy Again" products (+0.3 score)
- Returns product link ref + standalone ADD button ref
- Requires 50%+ match confidence

### 3. Get Cart Summary
```bash
echo "$SNAPSHOT" | node zepto-parser.js cart
```

**Output:**
```json
{
  "checkout": "e76",
  "total": 235
}
```

### 4. List All Products
```bash
echo "$SNAPSHOT" | node zepto-parser.js products 5
```

**Output:**
```json
[
  {
    "name": "Amul Taaza Toned Fresh Milk | Pouch",
    "price": 29,
    "ref": "e36",
    "buyAgain": true
  },
  ...
]
```

Optional limit parameter (default: all products)

## Integration in SKILL.md

### Before (Manual Parsing)
```bash
# Get snapshot (2000+ chars)
browser snapshot --maxChars=2000 > /tmp/snapshot.txt

# Manually parse with grep/sed (error-prone)
CART_REF=$(grep 'button "Cart"' /tmp/snapshot.txt | sed 's/.*\[ref=\([^]]*\)\].*/\1/')
```

### After (Parser)
```bash
# Get snapshot
SNAPSHOT=$(browser snapshot --maxChars=4000 | jq -r '.snapshot')

# Parse with script
REFS=$(echo "$SNAPSHOT" | node zepto-parser.js refs)
SEARCH_REF=$(echo "$REFS" | jq -r '.search')
CART_REF=$(echo "$REFS" | jq -r '.cart')

# Or find product
PRODUCT=$(echo "$SNAPSHOT" | node zepto-parser.js product "milk")
PRODUCT_REF=$(echo "$PRODUCT" | jq -r '.ref')
ADD_REF=$(echo "$PRODUCT" | jq -r '.addButtonRef')
```

## Example: Add Item to Cart

```bash
# 1. Navigate to Zepto
browser open --targetUrl="https://www.zepto.com" --profile=openclaw

# 2. Get snapshot and parse refs
SNAPSHOT=$(browser snapshot --interactive=true --maxChars=1000 --profile=openclaw | jq -r '.snapshot')
SEARCH_REF=$(echo "$SNAPSHOT" | node zepto-parser.js refs | jq -r '.search')

# 3. Search for product
browser act --request='{"kind":"click","ref":"'$SEARCH_REF'"}' --profile=openclaw
browser act --request='{"kind":"type","ref":"'$SEARCH_REF'","text":"milk"}' --profile=openclaw
browser act --request='{"kind":"press","key":"Enter"}' --profile=openclaw

# 4. Find and add product
SNAPSHOT=$(browser snapshot --interactive=true --maxChars=4000 --profile=openclaw | jq -r '.snapshot')
ADD_REF=$(echo "$SNAPSHOT" | node zepto-parser.js product "milk" | jq -r '.addButtonRef')
browser act --request='{"kind":"click","ref":"'$ADD_REF'"}' --profile=openclaw

echo "✅ Milk added to cart!"
```

## Error Handling

If product not found, parser returns:
```json
{
  "error": "No matching product found"
}
```

Exit code: 1

**Check in scripts:**
```bash
PRODUCT=$(echo "$SNAPSHOT" | node zepto-parser.js product "xyz" 2>/dev/null)
if echo "$PRODUCT" | jq -e '.error' > /dev/null; then
  echo "❌ Product not found"
  exit 1
fi
```

## Performance

**Before:** 2000-4000 chars → LLM parsing → 4000+ tokens
**After:** 2000-4000 chars → Node.js parsing → 50-200 chars JSON → ~50 tokens

**Token reduction: ~95%**

## Dependencies

- Node.js (already required by OpenClaw)
- `jq` (optional, for JSON parsing in bash)

No external npm packages required.

## Future Enhancements

- [ ] Cache "Buy Again" products (if refs are stable across sessions)
- [ ] Price validation (warn if price changed >20%)
- [ ] Smart retry with alternate search terms
- [ ] Session history (remember what was added when)
- [ ] Parallel product search (pass multiple queries)

---

**Next:** Update SKILL.md to use zepto-parser.js instead of manual parsing


path: PUBLISH_CHECKLIST.md
content: # Pre-Publication Checklist

## ✅ Personal Data Removed
- [x] Address examples replaced with generic placeholders
- [x] Order IDs replaced with {ORDER_ID_EXAMPLE}
- [x] Phone numbers removed
- [x] Personal shopping history cleared (order-history.json is now template)
- [x] Store names genericized

## ✅ Security Improvements
- [x] SECURITY.md added with full disclosure
- [x] **ALL cron job sections completely removed** (no persistent background jobs)
- [x] Order status check only on explicit user "done" message
- [x] Clear explanation of local file storage
- [x] No external data transmission except Zepto.com

## ✅ Generic/Template Approach
- [x] All examples use {PLACEHOLDER} format
- [x] No hardcoded personal values
- [x] Clear documentation for users to add their own data

## ✅ Files Excluded from Publishing
Created `.clawhubignore` to exclude:
- PROGRESS.md (dev notes)
- LEARNINGS.md (dev notes)
- ARCHITECTURE.md (internal docs)
- NO-SCREENSHOTS.md (dev strategy)
- auto-scrape.sh (dev script)
- scraper.js (dev script)
- capabilities.js (dev script)
- functions.md (dev notes)
- order-history-template.json (not needed)

## ✅ Files Included in Publishing
- SKILL.md (main documentation)
- SECURITY.md (security disclosure)
- README.md (user-facing overview)
- package.json (metadata)
- ZEPTO_AUTH.md (authentication guide)
- order-history.json (empty template)

## Ready for Publication
Version: 1.0.2
All personal data scrubbed, security documented, cron removed.


path: README.md
content: # Zepto Grocery Automation

**Order groceries in 30 seconds. From chat to checkout.**

Just tell your AI what you need. It shops, generates a payment link, sends it to WhatsApp. You pay on your phone. Groceries arrive in 10 minutes.

## How It Works

**You:** "Order milk and bread from Zepto"

**AI does:**
1. ✅ Confirms your delivery address
2. 🛒 Finds items, adds to cart
3. 💳 Generates payment link
4. 📱 Sends link to your WhatsApp

**You:** Click link, pay, done. Groceries on the way.

---

## Examples

**Quick orders:**
- "Add 2 liters of milk and a loaf of bread"
- "Order vegetables - tomatoes, onions, potatoes"
- "Get me Amul butter and cheese"

**Smart recommendations:**
- Remembers what you usually order
- "Add my usual milk" → automatically picks the brand you always get

**Full shopping list:**
- "Add milk, bread, eggs, coriander, ginger, and tea bags"
- AI adds everything, shows total, sends payment link

---

## Features

- 🏠 **Address confirmation** - Always checks before ordering
- 🧠 **Remembers your usuals** - Tracks what you order frequently
- 🛒 **Smart cart** - Adds all items, then shows summary
- 💳 **Payment links** - Pay securely via WhatsApp on your phone
- ✅ **Order verification** - Confirms when your order is on the way
- 🧹 **Auto cleanup** - Clears cart after each order

## Quick Start

**Just say:**
- "Order groceries from Zepto"
- "Add milk and bread to my Zepto cart"
- "Get me vegetables - onions, tomatoes, potatoes"

**The AI will:**
1. Confirm your address
2. Add items to cart
3. Send payment link to WhatsApp
4. Verify order after you pay

**That's it!** Groceries arrive in ~10 minutes.

## Version History

### v1.0.5 (2026-02-09)
- **Improved**: Much better description and examples - clear, concise, user-friendly
- **Improved**: README now shows actual usage examples
- **Improved**: Highlights speed and simplicity (30 seconds from chat to checkout)

### v1.0.4 (2026-02-09)
- **Security**: Completely removed all cron job functionality
- **Improved**: Payment message now explicitly asks user to message "DONE" 
- **Improved**: Order verification only on explicit user confirmation
- **Fixed**: Discrepancy in documentation about cron removal

### v1.0.3 (2026-02-09)
- **Improved**: Address confirmation now mandatory before every order
- **Improved**: Smart cart clearing - auto-clear after payment, ask during normal flow
- **Improved**: Payment link message includes order confirmation instructions
- **Improved**: Post-payment flow verifies order status and clears cart automatically
- **Fixed**: Cart persistence issue after payment (items remained in cart)
- **Fixed**: Multi-item ordering now adds all items first, then shows cart summary
- **Improved**: Item selection logic - ask when unclear about variants
- **Documented**: Complete workflow mapping with button references

### v1.0.2 (2026-02-09)
- Internal version (deleted, security improvements)

### v1.0.1 (2026-02-09)
- Changed display name from "Zepto India Grocery Automation" to "zepto"

### v1.0.0 (2026-02-08)
- Initial release with full Zepto automation

## Requirements

- **OpenClaw** with browser control enabled
- **WhatsApp** channel configured (for payment links)
- **Zepto account** (skill handles login via Phone + OTP)
- **India location** (Zepto delivers in major Indian cities)

## First Time Setup

The skill walks you through:
1. Phone number + OTP login to Zepto
2. Address confirmation
3. First order

After that, you're logged in and can order anytime.

## Author

Created by Gaurav

## License

MIT


path: SECURITY.md
content: # Security & Privacy

## Overview

This skill automates grocery ordering on Zepto.com through browser automation. It operates entirely within your local OpenClaw environment.

## What This Skill Does

- ✅ **Browser automation** on zepto.com (your local browser, your authenticated session)
- ✅ **Local order history** stored in `{SKILL_DIR}/order-history.json` to remember frequently ordered items
- ✅ **Payment link generation** via Zepto's Juspay gateway (sent to your WhatsApp)
- ✅ **Address confirmation** required before every order
- ✅ **Authentication** through Zepto's official Phone + OTP flow

## What This Skill Does NOT Do

- ❌ **No automatic payments** - You must manually click payment links and complete transactions
- ❌ **No external data transmission** - All data stays local except interactions with Zepto.com and your WhatsApp
- ❌ **No credential storage** - OTPs and passwords are never stored
- ❌ **No payment info access** - Skill only generates payment links, never handles payment details
- ❌ **No background jobs by default** - Optional one-time order status check only with explicit user approval

## Data Storage

### Local Files
- **`{SKILL_DIR}/order-history.json`** - Stores item names and order frequency for "usuals" recommendations
  - Format: `{ "item_name": order_count }`
  - Example: `{ "Milk": 5, "Bread": 3 }`
  - **Contains:** Product names and frequencies only
  - **Does NOT contain:** Prices, addresses, payment info, phone numbers

### Browser Session
- Managed by OpenClaw's browser profile
- Standard Chrome/Chromium data location
- Session cookies persist login between orders

## User Control

- ✅ You initiate all orders explicitly
- ✅ You approve each payment link before paying
- ✅ Address confirmation required before shopping
- ✅ Order history file can be deleted anytime (`rm order-history.json`)
- ✅ All browser actions visible in your profile
- ✅ No actions happen without your commands

## Permissions Required

- **Browser control** - Required for zepto.com automation
- **WhatsApp messaging** (optional) - For sending payment links
- **File write** - For storing order-history.json locally

## Privacy Best Practices

Before publishing or sharing this skill:
1. Delete or sanitize `order-history.json` if it contains personal shopping patterns
2. Review SKILL.md for any example addresses or personal data
3. Clear browser session if needed

## Threat Model

**Low Risk:**
- Skill operates locally within your OpenClaw environment
- No external API calls except Zepto.com (which you already use)
- No credential or payment data handling

**User Responsibilities:**
- Keep your OpenClaw environment secure
- Review payment links before clicking
- Monitor order confirmations from Zepto

## Questions or Concerns?

File an issue on ClawHub or contact the skill author.


path: ZEPTO_AUTH.md
content: ---
name: zepto-auth
description: Zepto authentication flow - handle phone number entry, OTP verification, and address confirmation
metadata: {"openclaw":{"emoji":"🔐"}}
---

# Zepto Authentication Skill

Complete authentication and setup flow for Zepto orders.

## Flow Overview
1. **Phone Number Entry** - User provides phone number
2. **OTP Verification** - I ask user for OTP received on phone
3. **Address Confirmation** - Verify delivery address with user
4. **Ready to Order** - User can now place orders

---

## Step 1: Phone Number Entry

**Ask user for phone number:**
```
"What's your phone number for Zepto? (10 digits)"
```

**Enter phone number in browser:**
```bash
browser act profile=openclaw request='{"kind":"type","ref":"e1","text":"{phone}"}'
browser act profile=openclaw request='{"kind":"click","ref":"e2"}'  # Continue button
```

---

## Step 2: OTP Verification

**After phone number submitted, Zepto sends OTP to user's phone.**

**Ask user for OTP:**
```
"I've sent the OTP to {phone}. What's the OTP you received?"
```

**Wait for user to provide OTP (via WhatsApp message)**

**Enter OTP in browser:**
```bash
browser snapshot --interactive profile=openclaw  # Get OTP input field refs
browser act profile=openclaw request='{"kind":"type","ref":"{otp_input_ref}","text":"{otp}"}'
browser act profile=openclaw request='{"kind":"click","ref":"{verify_button_ref}"}'
```

---

## Step 3: Address Confirmation

**After OTP verification, check if location is set:**

```bash
browser navigate url=https://www.zepto.com profile=openclaw
browser snapshot --interactive profile=openclaw
```

**Look for location button (usually shows current address or "Select Location")**

**If location not set or needs confirmation:**

**Ask user:**
```
"Where should I deliver your Zepto order? 
Current saved addresses: {list_addresses}

Reply with:
1. Number to select saved address
2. New address (I'll help you add it)"
```

**To add new address:**
1. Click "Select Location" button
2. User can either:
   - Type address
   - Enable location (if on phone)
   - Select from saved addresses

**Confirm with user:**
```
"Delivery address set to: {address}
Zepto ETA: {eta_minutes} mins
Is this correct? (yes/no)"
```

---

## Step 4: Serviceability Check

**After address is set, check if location is serviceable:**

```bash
browser navigate url=https://www.zepto.com profile=openclaw
# Look for "Store closed" or "Not serviceable" messages
```

**If serviceable:**
```
✅ You're all set! I can now order groceries for you.
Delivery ETA: {eta} mins
Store: {store_name}

What would you like to add to cart?
```

**If NOT serviceable:**
```
⚠️ Your location ({address}) is currently not serviceable by Zepto.

This could be because:
- Store temporarily closed (back in {eta} mins)
- Location outside delivery zone

Want to try a different address?
```

---

## Complete Authentication Script

```python
#!/usr/bin/env python3
"""
Zepto Authentication Handler
"""

def authenticate_zepto(phone_number):
    """
    Complete Zepto authentication flow
    Returns: (success, user_data)
    """
    
    # Step 1: Enter phone number
    print(f"Entering phone number: {phone_number}")
    # browser commands...
    
    # Step 2: Ask for OTP
    otp = input("Enter OTP received on phone: ")
    # browser commands to enter OTP...
    
    # Step 3: Check/confirm address
    # browser commands to get current address...
    
    # Step 4: Verify serviceability
    # Check if location is serviceable...
    
    return {
        "authenticated": True,
        "phone": phone_number,
        "address": address,
        "serviceable": True,
        "eta_minutes": 15,
        "store_name": "Store Name"
    }
```

---

## Error Handling

**Invalid Phone Number:**
```
"Phone number should be 10 digits. Please try again."
```

**Invalid OTP:**
```
"OTP verification failed. Let me resend the OTP.
Check your phone for the new code."
```

**Location Not Serviceable:**
```
"Your location is not serviceable right now.
Want to try a different address?"
```

---

## Session Persistence

**After successful auth:**
- Browser session stays logged in (cookies saved)
- No need to re-authenticate for future orders
- Can directly proceed to adding items to cart

**To check if already authenticated:**
```bash
browser navigate url=https://www.zepto.com profile=openclaw
browser snapshot --interactive profile=openclaw
# Look for "login" button vs user profile icon
```

**If logged in:** Skip auth, proceed to ordering
**If logged out:** Run full auth flow

---

## Security

- **Never store OTP** - ask user each time
- **Never automate OTP entry without user confirmation**
- **Always confirm address before ordering**
- **User must approve each order**

---

## Integration with Main Zepto Skill

Before any order, check authentication status:
1. Is user logged in? → Proceed
2. Not logged in? → Run auth flow first
3. Address confirmed? → Proceed
4. Location serviceable? → Proceed

Only after all checks pass, proceed with adding items to cart.


path: package.json
content: {
  "name": "zepto",
  "version": "1.0.6",
  "description": "Order groceries from Zepto in seconds. Just say what you need, get a payment link on WhatsApp, pay on your phone, done. Remembers your usual items. Works across India where Zepto delivers.",
  "author": "Gaurav",
  "license": "MIT",
  "openclaw": {
    "emoji": "🛒",
    "requires": {
      "config": ["browser.enabled"]
    }
  }
}


path: test-ops.js
content: #!/usr/bin/env node

/**
 * Test zepto-ops.js operations step-by-step
 */

const zeptoOps = require('./zepto-ops.js');

// Mock browser tool (we'll use real one in Sheru context)
const TARGET_ID = '6B33A011C5D7D5F9C5F23A0DCAA22968';

async function testClearCart() {
  console.log('📋 Test 1: Clear Cart');
  console.log('=' .repeat(50));
  
  // This would be called from Sheru's context with actual browser tool
  // For now, documenting what it should do
  
  console.log('✅ zepto-ops.js ready');
  console.log('✅ Operations available:');
  console.log('  - getCartState(browser, targetId)');
  console.log('  - clearCart(browser, targetId)');
  console.log('  - searchProduct(browser, targetId, query)');
  console.log('  - addItem(browser, targetId, query)');
  console.log('  - shop(browser, targetId, items[])');
  console.log('');
  console.log('📝 To test, call from Sheru:');
  console.log('  const zeptoOps = require("/Users/gaurav/.openclaw/skills/zepto/zepto-ops.js");');
  console.log('  const result = await zeptoOps.clearCart(browser, targetId);');
}

testClearCart();


path: zepto-agent.js
content: #!/usr/bin/env node

/**
 * Zepto Agent - Autonomous Task Executor
 * 
 * Not a parser. An agent that DOES things.
 * Takes commands, executes them, verifies results.
 * 
 * Usage:
 *   node zepto-agent.js clear-cart [--verify]
 *   node zepto-agent.js add-item "milk" [--confirm]
 *   node zepto-agent.js get-cart
 *   node zepto-agent.js shop "milk, bread, honey"
 */

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');

// ==================== CONFIG ====================
const BROWSER_PROFILE = 'openclaw';
const ZEPTO_URL = 'https://www.zepto.com';
const MAX_RETRIES = 3;
const SNAPSHOT_DELAY_MS = 1500;

// ==================== UTILITIES ====================

/**
 * Run OpenClaw browser command and return parsed result
 */
async function browserCmd(action, params = {}) {
  return new Promise((resolve, reject) => {
    const args = ['browser', '--profile', BROWSER_PROFILE];
    
    // Handle positional arguments
    if (action === 'open' && params.url) {
      args.push(action, params.url);
      delete params.url;
    } else if (action === 'click' && params.ref) {
      args.push(action, params.ref);
      delete params.ref;
    } else if (action === 'evaluate' && params.fn) {
      args.push(action);
      args.push('--fn', params.fn);
      delete params.fn;
    } else {
      args.push(action);
    }
    
    // Add remaining parameters  
    Object.entries(params).forEach(([key, val]) => {
      if (val !== undefined && val !== null) {
        // Boolean flags: just add the flag if true, skip if false
        if (typeof val === 'boolean') {
          if (val === true) {
            args.push(`--${key}`);
          }
        } else {
          args.push(`--${key}`, String(val));
        }
      }
    });

    const proc = spawn('openclaw', args, { stdio: ['pipe', 'pipe', 'pipe'] });
    let stdout = '';
    let stderr = '';

    proc.stdout.on('data', (data) => { stdout += data; });
    proc.stderr.on('data', (data) => { stderr += data; });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`Browser command failed: ${stderr}`));
      } else {
        try {
          resolve(JSON.parse(stdout));
        } catch (e) {
          resolve({ raw: stdout });
        }
      }
    });
  });
}

/**
 * Get snapshot and parse with zepto-parser.js
 */
async function getSnapshot(command = 'refs', maxLimit = 500) {
  // Get browser snapshot - using AI format with interactive flag
  const snap = await browserCmd('snapshot', { 
    interactive: true,
    limit: maxLimit 
  });

  // DEBUG: Log what we got
  console.error('DEBUG snap keys:', Object.keys(snap));
  console.error('DEBUG snap:', JSON.stringify(snap).substring(0, 200));

  // New format: snap has { interactive: [...] } directly
  if (!snap.interactive && !snap.snapshot && !snap.aria) {
    throw new Error(`No snapshot data received. Got keys: ${Object.keys(snap).join(', ')}`);
  }

  // If we have interactive array, return it with raw text version
  if (snap.interactive) {
    const snapshotText = JSON.stringify(snap.interactive, null, 2);
    
    // Parse with zepto-parser.js
    return new Promise((resolve, reject) => {
      const parserPath = path.join(__dirname, 'zepto-parser.js');
      const proc = spawn('node', [parserPath, command], { stdio: ['pipe', 'pipe', 'pipe'] });
      
      let stdout = '';
      let stderr = '';

      proc.stdin.write(snapshotText);
      proc.stdin.end();

      proc.stdout.on('data', (data) => { stdout += data; });
      proc.stderr.on('data', (data) => { stderr += data; });

      proc.on('close', (code) => {
        if (code !== 0) {
          reject(new Error(`Parser failed: ${stderr}`));
        } else {
          try {
            resolve({ 
              raw: snapshotText, 
              parsed: JSON.parse(stdout),
              interactive: snap.interactive 
            });
          } catch (e) {
            resolve({ 
              raw: snapshotText, 
              parsed: stdout.trim(),
              interactive: snap.interactive 
            });
          }
        }
      });
    });
  }

  // Fallback to old format
  const snapshotText = snap.snapshot || snap.aria || '';

  // Parse with zepto-parser.js
  return new Promise((resolve, reject) => {
    const parserPath = path.join(__dirname, 'zepto-parser.js');
    const proc = spawn('node', [parserPath, command], { stdio: ['pipe', 'pipe', 'pipe'] });
    
    let stdout = '';
    let stderr = '';

    proc.stdin.write(snapshotText);
    proc.stdin.end();

    proc.stdout.on('data', (data) => { stdout += data; });
    proc.stderr.on('data', (data) => { stderr += data; });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`Parser failed: ${stderr}`));
      } else {
        try {
          resolve({ raw: snapshotText, parsed: JSON.parse(stdout) });
        } catch (e) {
          resolve({ raw: snapshotText, parsed: stdout.trim() });
        }
      }
    });
  });
}

/**
 * Click a ref and wait
 */
async function click(ref, delayMs = 500) {
  await browserCmd('act', { request: JSON.stringify({ kind: 'click', ref }) });
  await sleep(delayMs);
}

/**
 * Type text into a ref
 */
async function type(ref, text, delayMs = 500) {
  await browserCmd('act', { request: JSON.stringify({ kind: 'type', ref, text }) });
  await sleep(delayMs);
}

/**
 * Sleep utility
 */
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Log with timestamp
 */
function log(msg, level = 'info') {
  const timestamp = new Date().toISOString();
  const prefix = level === 'error' ? '❌' : level === 'success' ? '✅' : 'ℹ️';
  console.error(`${prefix} [${timestamp}] ${msg}`);
}

// ==================== CORE OPERATIONS ====================

/**
 * Open Zepto homepage
 */
async function openZepto() {
  log('Opening Zepto...');
  await browserCmd('open', { url: ZEPTO_URL });
  await sleep(SNAPSHOT_DELAY_MS);
  log('Zepto opened', 'success');
}

/**
 * Get cart state (items, total, checkout ref)
 */
async function getCart() {
  log('Getting cart state...');
  
  // First get refs to find cart button
  const homepage = await getSnapshot('refs', 1000);
  const cartRef = homepage.parsed.cart;
  
  if (!cartRef) {
    throw new Error('Cart button not found on homepage');
  }

  // Click cart
  await click(cartRef, SNAPSHOT_DELAY_MS);

  // Get cart snapshot
  const cartSnap = await getSnapshot('cart', 3000);
  
  log(`Cart state: ${JSON.stringify(cartSnap.parsed)}`, 'success');
  return cartSnap;
}

/**
 * Clear cart completely (with verification)
 */
async function clearCart(options = {}) {
  const verify = options.verify !== false; // Default true
  
  log('Clearing cart...');
  
  let retries = 0;
  while (retries < MAX_RETRIES) {
    try {
      // Get current cart state
      const cartState = await getCart();
      
      // Check if already empty
      if (cartState.parsed.total === 0 || cartState.parsed.total === null) {
        log('Cart already empty', 'success');
        return { status: 'empty', items: 0 };
      }

      // Extract remove button refs from raw snapshot
      const removeRefs = extractRemoveRefs(cartState.raw);
      
      if (removeRefs.length === 0) {
        log('No remove buttons found, but cart not empty - retrying...', 'error');
        retries++;
        await sleep(1000);
        continue;
      }

      log(`Found ${removeRefs.length} items to remove`);

      // Click all remove buttons
      for (const ref of removeRefs) {
        log(`Removing item ${ref}...`);
        await click(ref, 300);
      }

      // Wait for removals to process
      await sleep(1000);

      // Verify if requested
      if (verify) {
        const verifyState = await getCart();
        if (verifyState.parsed.total === 0 || verifyState.parsed.total === null) {
          log('Cart cleared and verified empty', 'success');
          return { status: 'cleared', items: removeRefs.length };
        } else {
          log(`Verification failed: cart still has items (total: ₹${verifyState.parsed.total})`, 'error');
          retries++;
          continue;
        }
      } else {
        log('Cart cleared (not verified)', 'success');
        return { status: 'cleared', items: removeRefs.length, verified: false };
      }

    } catch (err) {
      log(`Clear cart attempt ${retries + 1} failed: ${err.message}`, 'error');
      retries++;
      if (retries >= MAX_RETRIES) throw err;
      await sleep(1000);
    }
  }

  throw new Error('Failed to clear cart after max retries');
}

/**
 * Extract remove button refs from cart snapshot
 */
function extractRemoveRefs(snapshotText) {
  const lines = snapshotText.split('\n');
  const removeRefs = [];
  
  for (const line of lines) {
    // Match: button "Remove" [ref=eXX]
    if (line.includes('button "Remove"') && line.includes('[ref=')) {
      const match = line.match(/\[ref=(e\d+)\]/);
      if (match) {
        removeRefs.push(match[1]);
      }
    }
  }
  
  return removeRefs;
}

/**
 * Check if item is already in cart by searching for it in results
 * Returns { inCart: boolean, productRef: string, addRef: string }
 */
async function checkItemInCart(query) {
  log(`Checking if "${query}" is already in cart...`);
  
  // Get search ref
  const homepage = await getSnapshot('refs', 1000);
  const searchRef = homepage.parsed.search;
  
  if (!searchRef) {
    throw new Error('Search button not found');
  }

  // Click search
  await click(searchRef, 500);

  // Type query
  await type(searchRef, query, 1500);

  // Get search results with full snapshot
  const results = await getSnapshot(`product ${query}`, 6000);
  
  if (!results.parsed || !results.parsed.ref) {
    throw new Error(`No product found for query: ${query}`);
  }

  const product = results.parsed;
  
  // Check if raw snapshot contains quantity controls for this product
  // Pattern: "Decrease quantity 1 Increase quantity" near the product ref
  const inCart = results.raw.includes('Decrease quantity') && 
                 results.raw.includes('Increase quantity');
  
  if (inCart) {
    log(`✓ "${product.name}" is ALREADY in cart`, 'success');
  } else {
    log(`✗ "${product.name}" is NOT in cart`);
  }

  const addRef = product.ref.replace(/e(\d+)/, (_, num) => `e${parseInt(num) + 1}`);

  return {
    inCart,
    product,
    productRef: product.ref,
    addRef
  };
}

/**
 * Search and add item (ONLY if not already in cart)
 */
async function addItem(query, options = {}) {
  const confirm = options.confirm !== false; // Default true
  const force = options.force === true; // Force add even if in cart
  
  log(`Adding item: "${query}"...`);

  // CRITICAL: Check if already in cart FIRST
  const check = await checkItemInCart(query);
  
  if (check.inCart && !force) {
    log(`SKIPPED: ${check.product.name} already in cart`, 'success');
    return { ...check.product, skipped: true, reason: 'already_in_cart' };
  }

  log(`Found: ${check.product.name} - ₹${check.product.price} [${check.productRef}]`);

  if (confirm) {
    // Click ADD button
    await click(check.addRef, 1000);
    log(`Added ${check.product.name}`, 'success');
  }

  return check.product;
}

/**
 * Get cart total
 */
async function getCartTotal() {
  const cartState = await getCart();
  console.log(JSON.stringify({
    total: cartState.parsed.total,
    checkout_ref: cartState.parsed.checkout
  }, null, 2));
  return cartState.parsed;
}

/**
 * Shop for multiple items (legacy - requires empty cart)
 */
async function shop(itemsStr, options = {}) {
  const items = itemsStr.split(',').map(s => s.trim());
  
  log(`Shopping for ${items.length} items: ${items.join(', ')}`);

  // Pre-flight: verify cart is empty
  const preCart = await getCart();
  if (preCart.parsed.total > 0) {
    log(`Cart not empty (₹${preCart.parsed.total}). Clear first!`, 'error');
    throw new Error('Cart must be empty before shopping');
  }

  const added = [];
  const failed = [];

  for (const item of items) {
    try {
      const product = await addItem(item, { confirm: true });
      added.push(product);
      await sleep(500);
    } catch (err) {
      log(`Failed to add ${item}: ${err.message}`, 'error');
      failed.push({ item, error: err.message });
    }
  }

  // Get final cart
  const finalCart = await getCart();

  log(`Shopping complete: ${added.length} added, ${failed.length} failed`, added.length === items.length ? 'success' : 'error');

  console.log(JSON.stringify({
    added: added.map(p => ({ name: p.name, price: p.price })),
    failed,
    cart_total: finalCart.parsed.total
  }, null, 2));

  return { added, failed, total: finalCart.parsed.total };
}

/**
 * Smart shop: Check cart first, remove unwanted, add missing
 */
async function smartShop(itemsStr, options = {}) {
  const targetItems = itemsStr.split(',').map(s => s.trim());
  const clearUnwanted = options.clearUnwanted !== false; // Default true
  
  log(`🧠 Smart shopping for: ${targetItems.join(', ')}`);

  // STEP 1: Check current cart state
  const cartState = await getCart();
  log(`Current cart total: ₹${cartState.parsed.total || 0}`);

  // STEP 2: If cart has items and clearUnwanted is true, clear it
  if (cartState.parsed.total > 0) {
    if (clearUnwanted) {
      log('Clearing existing cart items...');
      await clearCart({ verify: true });
    } else {
      log('Warning: Cart has existing items, will add on top');
    }
  }

  // STEP 3: Add items (with duplicate checking)
  const added = [];
  const skipped = [];
  const failed = [];

  for (const item of targetItems) {
    try {
      const result = await addItem(item, { confirm: true, force: false });
      
      if (result.skipped) {
        skipped.push(result);
      } else {
        added.push(result);
      }
      
      await sleep(500);
    } catch (err) {
      log(`Failed to add ${item}: ${err.message}`, 'error');
      failed.push({ item, error: err.message });
    }
  }

  // STEP 4: Get final cart state
  const finalCart = await getCart();

  log(`✅ Smart shop complete: ${added.length} added, ${skipped.length} skipped, ${failed.length} failed`, 'success');

  const result = {
    added: added.map(p => ({ name: p.name, price: p.price })),
    skipped: skipped.map(p => ({ name: p.name, reason: p.reason })),
    failed,
    cart_total: finalCart.parsed.total
  };

  console.log(JSON.stringify(result, null, 2));
  return result;
}

/**
 * Get list of available saved addresses
 * Returns array of address names
 */
/**
 * Get list of available saved addresses  
 * Opens modal using JavaScript and extracts address names
 */
async function getAvailableAddresses() {
  log('📋 Fetching available addresses...');
  
  // Step 1: Get current page snapshot  
  const snap = await browserCmd('snapshot', {});
  const snapshotText = snap.snapshot || snap.raw || JSON.stringify(snap);
  
  // Find address button in header
  const addrMatch = snapshotText.match(/button "([^"]*(?:blr|Home|Nagar)[^"]*)"\s*\[ref=(e\d+)\]/i);
  if (!addrMatch) {
    throw new Error('Address button not found in header');
  }
  
  const buttonRef = addrMatch[2];
  log(`Found address button: ${buttonRef}`);
  
  // Step 2: Click to open modal
  await browserCmd('click', { ref: buttonRef });
  await sleep(2000);
  
  // Step 3: Extract addresses using JavaScript
  const jsCode = `(() => {
    const input = document.querySelector('input[placeholder*="address"]');
    if (!input) return { error: 'Modal not found' };
    
    let modal = input;
    for (let i = 0; i < 15; i++) {
      if (!modal.parentElement) break;
      modal = modal.parentElement;
      if (window.getComputedStyle(modal).position === 'fixed') break;
    }
    
    // Find all divs in modal that look like address entries
    // Pattern: starts with capital letter + contains full address text
    const divs = Array.from(modal.querySelectorAll('div'));
    const addresses = [];
    const seen = new Set();
    
    for (const div of divs) {
      const text = div.textContent;
      if (!text) continue;
      
      // Look for patterns like "New HomeD-1001..." or "Sanskar BlrA-301..."
      const match = text.match(/^([A-Z][a-zA-Z\\s]+(?:Blr|Home|Nagar|Nivas))[A-Z\\d]/);
      if (match) {
        const name = match[1].trim();
        if (name.length > 3 && !seen.has(name.toLowerCase())) {
          addresses.push(name);
          seen.add(name.toLowerCase());
        }
      }
    }
    
    // Close modal
    document.dispatchEvent(new KeyboardEvent("keydown", { 
      key: "Escape", code: "Escape", keyCode: 27, bubbles: true 
    }));
    
    return { addresses };
  })()`;
  
  const result = await browserCmd('evaluate', { fn: jsCode });
  
  if (result.error || result.result?.error) {
    throw new Error(result.error || result.result.error);
  }
  
  const addresses = result.result?.addresses || result.addresses || [];
  
  if (addresses.length === 0) {
    throw new Error('No addresses found in modal');
  }
  
  log(`Found ${addresses.length} saved addresses: ${addresses.join(', ')}`);
  return addresses;
}

/**
 * Fuzzy match address name
 * Returns { exactMatch: string | null, partialMatches: string[] }
 */
function fuzzyMatchAddress(input, availableAddresses) {
  const inputLower = input.toLowerCase().trim();
  
  // Exact match (case-insensitive)
  const exact = availableAddresses.find(addr => 
    addr.toLowerCase() === inputLower
  );
  
  if (exact) {
    return { exactMatch: exact, partialMatches: [] };
  }
  
  // Partial matches (contains or is contained)
  const partial = availableAddresses.filter(addr => {
    const addrLower = addr.toLowerCase();
    return addrLower.includes(inputLower) || inputLower.includes(addrLower);
  });
  
  return { exactMatch: null, partialMatches: partial };
}

/**
 * Select a saved address by name (with optional fuzzy matching)
 * Simplified version: tries to click the address directly
 * 
 * @param {string} addressQuery - Address name or partial match (e.g., "sanskar", "New Home", "kundu")
 * @returns {Promise<{success: boolean, address: string}>}
 */
async function selectAddress(addressQuery) {
  log(`📍 Selecting address: "${addressQuery}"`);
  
  // Step 1: Get current address from header
  const snap1 = await browserCmd('snapshot', {});
  const snapshotText = snap1.snapshot || snap1.raw || JSON.stringify(snap1);
  const currentMatch = snapshotText.match(/button "([^"]*(?:blr|Home|Nagar)[^"]*)"\s*\[ref=(e\d+)\]/i);
  
  if (!currentMatch) {
    throw new Error('Address button not found in header');
  }
  
  const currentAddress = currentMatch[1];
  const buttonRef = currentMatch[2];
  
  log(`Current: ${currentAddress}`);
  
  // If query matches current (case-insensitive, partial), skip
  if (currentAddress.toLowerCase().includes(addressQuery.toLowerCase())) {
    log(`✅ Already at "${currentAddress}"`, 'success');
    return { success: true, address: currentAddress, alreadySelected: true };
  }
  
  // Step 2: Click address button to open modal
  await browserCmd('click', { ref: buttonRef });
  await sleep(1500);
  
  // Step 3: JavaScript click on target address (try exact match first, then fuzzy)
  const jsCode = `(() => {
    const query = ${JSON.stringify(addressQuery)};
    const input = document.querySelector('input[placeholder*="address"]');
    if (!input) return { error: 'Modal not found' };
    
    let modal = input;
    for (let i = 0; i < 15; i++) {
      if (!modal.parentElement) break;
      modal = modal.parentElement;
      if (window.getComputedStyle(modal).position === 'fixed') break;
    }
    
    const divs = Array.from(modal.querySelectorAll('div'));
    
    // Try exact match first (case-insensitive)
    let match = divs.find(d => {
      const text = d.textContent?.trim() || '';
      return text.toLowerCase().startsWith(query.toLowerCase());
    });
    
    // If not found, try fuzzy (contains)
    if (!match) {
      match = divs.find(d => {
        const text = d.textContent?.trim() || '';
        return text.toLowerCase().includes(query.toLowerCase());
      });
    }
    
    if (!match) return { error: 'Address not found: ' + query };
    
    // Find clickable parent
    let p = match;
    for (let i = 0; i < 10; i++) {
      if (!p) break;
      const s = window.getComputedStyle(p);
      if (p.onclick || p.getAttribute('onClick') || s.cursor === 'pointer') {
        p.scrollIntoView({ block: 'center' });
        const start = Date.now();
        while (Date.now() - start < 300) {}
        p.click();
        return { clicked: true, text: match.textContent.substring(0, 100) };
      }
      p = p.parentElement;
    }
    
    return { error: 'No clickable parent' };
  })()`;
  
  const result = await browserCmd('evaluate', { fn: jsCode });
  
  if (result.error || result.result?.error) {
    throw new Error(result.error || result.result.error);
  }
  
  const clickedText = result.clicked ? result.text : result.result?.text;
  log(`✅ Clicked: ${clickedText}`);
  
  // Step 4: Verify address change
  await sleep(2000);
  const snap2 = await browserCmd('snapshot', {});
  const snapshotText2 = snap2.snapshot || snap2.raw || JSON.stringify(snap2);
  const newMatch = snapshotText2.match(/button "([^"]*(?:blr|Home|Nagar)[^"]*)"\s*\[ref=(e\d+)\]/i);
  
  if (newMatch) {
    log(`🎉 Address changed to: ${newMatch[1]}`, 'success');
    return { success: true, address: newMatch[1] };
  }
  
  throw new Error('Could not verify address change');
}

/**
 * List all saved addresses
 */
async function listAddresses() {
  const addresses = await getAvailableAddresses();
  console.log(JSON.stringify({ addresses }, null, 2));
  return addresses;
}

// ==================== CLI ====================

async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0) {
    console.error('Usage: zepto-agent.js <command> [options]');
    console.error('');
    console.error('Commands:');
    console.error('  clear-cart [--verify]           Clear cart (default: verify)');
    console.error('  add-item <query> [--force]      Add item (skips if in cart, use --force to add anyway)');
    console.error('  get-cart                        Get cart state');
    console.error('  get-cart-total                  Get cart total only');
    console.error('  shop "<item1>, <item2>"         Add items (requires empty cart)');
    console.error('  smart-shop "<item1>, <item2>"   Smart shop: clear unwanted, add missing');
    console.error('  select-address "<name>"         Select saved address (fuzzy match)');
    console.error('');
    console.error('Examples:');
    console.error('  zepto-agent.js clear-cart');
    console.error('  zepto-agent.js add-item "milk"');
    console.error('  zepto-agent.js smart-shop "milk, bread, eggs"  # RECOMMENDED');
    console.error('  zepto-agent.js shop "milk, bread, honey"       # legacy, needs empty cart');
    console.error('  zepto-agent.js select-address "sanskar"        # fuzzy match works!');
    process.exit(1);
  }

  const command = args[0];
  const options = {};

  // Parse flags
  for (let i = 1; i < args.length; i++) {
    if (args[i].startsWith('--')) {
      const flag = args[i].slice(2);
      options[flag] = true;
    }
  }

  try {
    await openZepto();

    switch (command) {
      case 'clear-cart':
        await clearCart({ verify: options.verify !== undefined ? options.verify : true });
        break;

      case 'add-item':
        if (args.length < 2) {
          throw new Error('Usage: add-item <query> [--force]');
        }
        await addItem(args[1], { 
          confirm: true,
          force: options.force === true
        });
        break;

      case 'get-cart':
        await getCart();
        break;

      case 'get-cart-total':
        await getCartTotal();
        break;

      case 'shop':
        if (args.length < 2) {
          throw new Error('Usage: shop "<item1>, <item2>, ..."');
        }
        await shop(args[1], options);
        break;

      case 'smart-shop':
        if (args.length < 2) {
          throw new Error('Usage: smart-shop "<item1>, <item2>, ..."');
        }
        await smartShop(args[1], options);
        break;

      case 'select-address':
        if (args.length < 2) {
          throw new Error('Usage: select-address "<address name or partial match>"');
        }
        await selectAddress(args[1]);
        break;

      default:
        throw new Error(`Unknown command: ${command}`);
    }

    process.exit(0);
  } catch (err) {
    log(err.message, 'error');
    log(err.stack, 'error');
    process.exit(1);
  }
}

if (require.main === module) {
  main();
}

module.exports = { 
  clearCart, 
  addItem, 
  checkItemInCart,
  getCart, 
  shop, 
  smartShop,
  selectAddress,
  listAddresses,
  getAvailableAddresses,
  fuzzyMatchAddress,
  getCartTotal 
};


path: zepto-ops.js
content: #!/usr/bin/env node

/**
 * zepto-ops.js - Zepto Operations Library
 * 
 * Atomic operations with built-in verification and retry logic.
 * Uses OpenClaw browser tool API directly (no CLI spawning).
 * 
 * All operations follow the pattern:
 * 1. Pre-flight check (verify current state)
 * 2. Execute action
 * 3. Verify result (retry if failed)
 * 4. Return structured result
 */

const { spawnSync } = require('child_process');
const path = require('path');

// ==================== CONFIG ====================
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
const PARSER_PATH = path.join(__dirname, 'zepto-parser.js');

// ==================== UTILITIES ====================

/**
 * Sleep utility
 */
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Parse snapshot with zepto-parser.js
 */
function parseWithParser(snapshot, command, ...args) {
  const proc = spawnSync('node', [PARSER_PATH, command, ...args], {
    input: snapshot,
    encoding: 'utf8',
    timeout: 5000
  });

  if (proc.status !== 0) {
    throw new Error(`Parser failed: ${proc.stderr || proc.error}`);
  }

  const output = proc.stdout.trim();
  try {
    return JSON.parse(output);
  } catch (e) {
    // Some commands return plain text
    return output;
  }
}

// ==================== BROWSER HELPERS ====================

/**
 * Get snapshot from browser
 */
async function snapshot(browserTool, targetId, limitNodes = 600) {
  const result = await browserTool({
    action: 'snapshot',
    targetId,
    interactive: true,
    limit: limitNodes
  });
  
  if (!result.snapshot && !result.aria) {
    throw new Error('No snapshot data received');
  }
  
  return result.snapshot || result.aria || '';
}

/**
 * Click a ref
 */
async function click(browserTool, targetId, ref, delayMs = 500) {
  await browserTool({
    action: 'act',
    targetId,
    request: { kind: 'click', ref }
  });
  await sleep(delayMs);
}

/**
 * Type text into a ref
 */
async function type(browserTool, targetId, ref, text, delayMs = 500) {
  await browserTool({
    action: 'act',
    targetId,
    request: { kind: 'type', ref, text }
  });
  await sleep(delayMs);
}

// ==================== PARSERS ====================

/**
 * Parse refs (search, cart, checkout buttons)
 */
function parseRefs(snapshot) {
  return parseWithParser(snapshot, 'refs');
}

/**
 * Parse cart (total, checkout ref)
 */
function parseCart(snapshot) {
  return parseWithParser(snapshot, 'cart');
}

/**
 * Parse product search result
 */
function parseProduct(snapshot, query) {
  const result = parseWithParser(snapshot, 'product', query);
  return result && result.name ? result : null;
}

/**
 * Extract remove button refs from cart snapshot
 */
function extractRemoveRefs(snapshot) {
  const lines = snapshot.split('\n');
  const removeRefs = [];
  
  for (const line of lines) {
    // Match: button "Remove" [ref=eXX]
    if (line.includes('button "Remove"') && line.includes('[ref=')) {
      const match = line.match(/\[ref=(e\d+)\]/);
      if (match && !removeRefs.includes(match[1])) {
        removeRefs.push(match[1]);
      }
    }
  }
  
  return removeRefs;
}

/**
 * Count items in cart (count "Remove" buttons)
 */
function countCartItems(snapshot) {
  return extractRemoveRefs(snapshot).length;
}

// ==================== CORE OPERATIONS ====================

/**
 * Get current cart state
 * Returns: { items: number, total: number, checkout_ref: string|null, raw_snapshot: string }
 */
async function getCartState(browserTool, targetId) {
  // Get homepage to find cart button
  const homeSnap = await snapshot(browserTool, targetId, 200);
  const refs = parseRefs(homeSnap);
  
  if (!refs.cart) {
    throw new Error('Cart button not found on homepage');
  }

  // Click cart
  await click(browserTool, targetId, refs.cart, 1500);

  // Get cart snapshot
  const cartSnap = await snapshot(browserTool, targetId, 600);
  
  // Parse cart
  const parsed = parseCart(cartSnap);
  const itemCount = countCartItems(cartSnap);

  return {
    items: itemCount,
    total: parsed.total || 0,
    checkout_ref: parsed.checkout || null,
    raw_snapshot: cartSnap
  };
}

/**
 * Clear cart completely (with verification)
 * Returns: { status: 'empty'|'cleared'|'failed', items_removed: number, attempts: number }
 */
async function clearCart(browserTool, targetId) {
  // Pre-flight: check current state
  let state = await getCartState(browserTool, targetId);
  
  if (state.items === 0) {
    return { status: 'empty', items_removed: 0, attempts: 0 };
  }

  const initialItems = state.items;
  let attempts = 0;

  while (attempts < MAX_RETRIES) {
    attempts++;

    // Extract remove refs
    const removeRefs = extractRemoveRefs(state.raw_snapshot);
    
    if (removeRefs.length === 0) {
      // No remove buttons but items exist - cart might be in weird state
      await sleep(RETRY_DELAY_MS);
      state = await getCartState(browserTool, targetId);
      continue;
    }

    // Click all remove buttons
    for (const ref of removeRefs) {
      await click(browserTool, targetId, ref, 300);
    }

    // Wait for UI to update
    await sleep(1000);

    // Verify cart is empty
    state = await getCartState(browserTool, targetId);
    
    if (state.items === 0) {
      return {
        status: 'cleared',
        items_removed: initialItems,
        attempts
      };
    }

    // Still has items, retry
    if (attempts < MAX_RETRIES) {
      await sleep(RETRY_DELAY_MS);
    }
  }

  // Failed after max retries
  return {
    status: 'failed',
    items_removed: initialItems - state.items,
    items_remaining: state.items,
    attempts
  };
}

/**
 * Search for a product
 * Returns: { name: string, price: number, ref: string, score: number } | null
 */
async function searchProduct(browserTool, targetId, query) {
  // Get search button ref
  const homeSnap = await snapshot(browserTool, targetId, 200);
  const refs = parseRefs(homeSnap);
  
  if (!refs.search) {
    throw new Error('Search button not found');
  }

  // Click search
  await click(browserTool, targetId, refs.search, 500);

  // Type query
  await type(browserTool, targetId, refs.search, query, 1500);

  // Get search results
  const resultsSnap = await snapshot(browserTool, targetId, 1200);
  
  // Parse product
  const product = parseProduct(resultsSnap, query);
  
  return product; // { name, price, ref, score } or null
}

/**
 * Add item to cart
 * Returns: { status: 'added'|'not_found'|'failed', product: object, in_cart: boolean, attempts: number }
 */
async function addItem(browserTool, targetId, query) {
  let attempts = 0;

  while (attempts < MAX_RETRIES) {
    attempts++;

    try {
      // Search for product
      const product = await searchProduct(browserTool, targetId, query);
      
      if (!product) {
        return {
          status: 'not_found',
          query,
          product: null,
          in_cart: false,
          attempts
        };
      }

      // Calculate ADD button ref (usually product ref + 1)
      const refNum = parseInt(product.ref.replace('e', ''));
      const addRef = `e${refNum + 1}`;

      // Click ADD
      await click(browserTool, targetId, addRef, 1000);

      // Verify item was added to cart
      const cartState = await getCartState(browserTool, targetId);
      
      // Check if cart increased
      if (cartState.items > 0) {
        return {
          status: 'added',
          product: { name: product.name, price: product.price },
          in_cart: true,
          attempts
        };
      }

      // Cart didn't increase, retry
      if (attempts < MAX_RETRIES) {
        await sleep(RETRY_DELAY_MS);
      }

    } catch (err) {
      if (attempts >= MAX_RETRIES) {
        return {
          status: 'failed',
          query,
          error: err.message,
          in_cart: false,
          attempts
        };
      }
      await sleep(RETRY_DELAY_MS);
    }
  }

  return {
    status: 'failed',
    query,
    error: 'Max retries exceeded',
    in_cart: false,
    attempts
  };
}

/**
 * Shop for multiple items
 * Returns: { added: array, failed: array, cart_total: number }
 */
async function shop(browserTool, targetId, items) {
  // Pre-flight: verify cart is empty
  const preState = await getCartState(browserTool, targetId);
  if (preState.items > 0) {
    throw new Error(`Cart not empty: ${preState.items} items (total: ₹${preState.total}). Clear first!`);
  }

  const added = [];
  const failed = [];

  for (const item of items) {
    const result = await addItem(browserTool, targetId, item);
    
    if (result.status === 'added') {
      added.push({ ...result.product, query: item });
    } else {
      failed.push({ query: item, reason: result.status, error: result.error });
    }

    // Small delay between items
    await sleep(500);
  }

  // Get final cart state
  const finalState = await getCartState(browserTool, targetId);

  return {
    added,
    failed,
    cart_total: finalState.total,
    cart_items: finalState.items
  };
}

// ==================== EXPORTS ====================

module.exports = {
  // Core operations
  getCartState,
  clearCart,
  searchProduct,
  addItem,
  shop,
  
  // Helpers (for advanced usage)
  snapshot,
  click,
  type,
  parseRefs,
  parseCart,
  parseProduct,
  extractRemoveRefs,
  countCartItems
};


path: zepto-parser.js
content: #!/usr/bin/env node

/**
 * zepto-parser.js - Intelligent snapshot parser for Zepto.com
 * Extracts only meaningful data from verbose browser snapshots
 * 
 * Usage:
 *   echo "$SNAPSHOT" | node zepto-parser.js refs
 *   echo "$SNAPSHOT" | node zepto-parser.js product "milk"
 *   echo "$SNAPSHOT" | node zepto-parser.js cart
 */

const readline = require('readline');

// Simple fuzzy matching (no deps)
function fuzzyMatch(str, pattern) {
  const strLower = str.toLowerCase();
  const patternLower = pattern.toLowerCase();
  
  // Exact substring match = best
  if (strLower.includes(patternLower)) {
    return 1.0;
  }
  
  // Simple character overlap ratio
  const strChars = new Set(strLower);
  const patternChars = patternLower.split('');
  const overlap = patternChars.filter(c => strChars.has(c)).length;
  return overlap / patternLower.length;
}

function extractRef(line) {
  const match = line.match(/\[ref=([^\]]+)\]/);
  return match ? match[1] : null;
}

function extractPrice(text) {
  const match = text.match(/₹(\d+)/);
  return match ? parseInt(match[1]) : null;
}

function parseProductLine(line) {
  // Format: link "ProductName ADD ... ₹X ... [details]" [ref=eXX]
  if (!line.includes('link "') || !line.includes('ADD')) {
    return null;
  }
  
  const ref = extractRef(line);
  if (!ref) return null;
  
  // Extract product name (before "ADD")
  const linkMatch = line.match(/link "([^"]+)"/);
  if (!linkMatch) return null;
  
  const fullText = linkMatch[1];
  const addIndex = fullText.indexOf(' ADD');
  if (addIndex === -1) return null;
  
  const name = fullText.substring(0, addIndex).trim();
  
  // Extract price
  const price = extractPrice(fullText);
  
  // Check for "Buy Again"
  const buyAgain = fullText.includes('Buy Again');
  
  return { name, ref, price, buyAgain };
}

function parseSnapshot(lines) {
  const data = {
    searchRef: null,
    cartRef: null,
    products: [],
    checkoutRef: null,
    cartTotal: null
  };
  
  for (const line of lines) {
    // Search bar (can be combobox or link)
    if ((line.includes('combobox') || line.includes('link')) && line.includes('Search')) {
      data.searchRef = extractRef(line);
    }
    
    // Cart button
    else if (line.includes('button') && line.includes('Cart') && !line.includes('Add to')) {
      const ref = extractRef(line);
      // Skip if it's a complex cart description (delivery address)
      if (ref && line.split('"').length < 6) {
        data.cartRef = ref;
      }
    }
    
    // Checkout button
    else if (line.includes('Click to Pay ₹')) {
      data.checkoutRef = extractRef(line);
      data.cartTotal = extractPrice(line);
    }
    
    // Product listings
    else if (line.includes('link "') && line.includes('ADD')) {
      const product = parseProductLine(line);
      if (product) {
        data.products.push(product);
      }
    }
    
    // ADD buttons (standalone) - find the one right after product
    else if (line.includes('button "ADD"')) {
      const ref = extractRef(line);
      if (ref && data.products.length > 0) {
        const lastProduct = data.products[data.products.length - 1];
        // If product doesn't have a ref yet, use this button's ref
        if (!lastProduct.addButtonRef) {
          lastProduct.addButtonRef = ref;
        }
      }
    }
  }
  
  return data;
}

function findProduct(data, query) {
  let bestMatch = null;
  let bestScore = 0;
  
  for (const product of data.products) {
    let score = fuzzyMatch(product.name, query);
    
    // Boost for "Buy Again"
    if (product.buyAgain) {
      score += 0.3;
    }
    
    // Boost for exact price match (if provided)
    // Future: Could pass expected price as param
    
    if (score > bestScore) {
      bestScore = score;
      bestMatch = product;
    }
  }
  
  // Require at least 50% match
  return bestScore > 0.5 ? { ...bestMatch, matchScore: bestScore } : null;
}

async function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  
  if (!command || command === '--help') {
    console.log('Usage:');
    console.log('  zepto-parser.js refs              - Extract UI refs (search, cart, checkout)');
    console.log('  zepto-parser.js product <query>   - Find product matching query');
    console.log('  zepto-parser.js cart              - Extract cart summary');
    console.log('  zepto-parser.js products [limit]  - List all products (optional limit)');
    console.log('');
    console.log('Input: Browser snapshot via stdin');
    console.log('Output: JSON to stdout');
    process.exit(0);
  }
  
  // Read snapshot from stdin
  const rl = readline.createInterface({
    input: process.stdin,
    crlfDelay: Infinity
  });
  
  const lines = [];
  for await (const line of rl) {
    lines.push(line);
  }
  
  const data = parseSnapshot(lines);
  
  // Execute command
  switch (command) {
    case 'refs':
      console.log(JSON.stringify({
        search: data.searchRef,
        cart: data.cartRef,
        checkout: data.checkoutRef
      }, null, 2));
      break;
      
    case 'product': {
      const query = args.slice(1).join(' ');
      if (!query) {
        console.error('Error: Missing product query');
        process.exit(1);
      }
      const match = findProduct(data, query);
      if (match) {
        console.log(JSON.stringify(match, null, 2));
      } else {
        console.log(JSON.stringify({ error: 'No matching product found' }));
        process.exit(1);
      }
      break;
    }
    
    case 'cart':
      console.log(JSON.stringify({
        checkout: data.checkoutRef,
        total: data.cartTotal
      }, null, 2));
      break;
      
    case 'products': {
      const limit = parseInt(args[1]) || data.products.length;
      const products = data.products.slice(0, limit).map(p => ({
        name: p.name,
        price: p.price,
        ref: p.ref,
        buyAgain: p.buyAgain || false
      }));
      console.log(JSON.stringify(products, null, 2));
      break;
    }
    
    default:
      console.error(`Error: Unknown command '${command}'`);
      console.error('Run with --help for usage');
      process.exit(1);
  }
}

main().catch(err => {
  console.error('Error:', err.message);
  process.exit(1);
});


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)