[object Object]

← back to Delivery Address Fix

absorb delivery-address-skill (TK-10144): generic launch/state/enter-code/restart-login scripts, config.example, platform-notes — canonical merge per Steve email go

f519a12ba003c26c6d2fcb1d542e9ded97c509c6 · 2026-08-02 15:47:48 -0700 · Steve Abrams

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit f519a12ba003c26c6d2fcb1d542e9ded97c509c6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 15:47:48 2026 -0700

    absorb delivery-address-skill (TK-10144): generic launch/state/enter-code/restart-login scripts, config.example, platform-notes — canonical merge per Steve email go
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 .gitignore                   |  2 ++
 SKILL.md                     | 25 ++++++++++++++
 config.example.json          |  8 +++++
 references/platform-notes.md | 57 ++++++++++++++++++++++++++++++
 scripts/enter-code.js        | 34 ++++++++++++++++++
 scripts/launch.js            | 29 ++++++++++++++++
 scripts/lib.js               | 82 ++++++++++++++++++++++++++++++++++++++++++++
 scripts/restart-login.js     | 27 +++++++++++++++
 scripts/state.js             | 13 +++++++
 9 files changed, 277 insertions(+)

diff --git a/.gitignore b/.gitignore
index 1924158..beabffe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ tmp/
 dist/
 build/
 .next/
+config.json
+profiles/
diff --git a/SKILL.md b/SKILL.md
index bbb2bbf..a7ab392 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -104,3 +104,28 @@ adapter contract: `login() → passGates() → liveOrderCheck() → saveAddress(
 Copy `references/_adapter-template.md`, fill in: auth URL, login selectors,
 2FA gate types observed, address-book path, order-help path, quirks. Mark
 UNVERIFIED until a real run proves each step; promote to PROVEN with dates.
+
+## Runnable scripts (merged from TK-10144, 2026-08-02)
+
+Generic, parameterized — nothing user-specific hardcoded. All take
+`--platform <ubereats|doordash|instacart> --user <email> --port <cdp> --match <url-substring>`;
+per-user persistent Chrome profiles live under `profiles/<user>-<platform>/`
+(gitignored) so a completed login survives restarts.
+
+- `scripts/launch.js` — headed real Chrome (Playwright persistent context,
+  `channel: 'chrome'`), CDP exposed, navigates to the provider's auth URL.
+  Supersedes the per-run `browser-server.js` template (kept for reference).
+- `scripts/state.js` — screenshot + page text of the target tab (selected by
+  `--match`, never `pages()[0]`).
+- `scripts/enter-code.js` — types a 2FA code / card digits into the parked gate.
+- `scripts/restart-login.js` — restarts an expired auth flow (auto-fires a
+  fresh code; remind the user the NEWEST code wins).
+- `scripts/lib.js` — shared CDP attach + tab-select helpers.
+
+Per-user setup: `config.example.json` → `config.json` (gitignored).
+Extra flow notes and failure modes: `references/platform-notes.md`.
+
+> Consolidation note: this skill absorbed `~/Projects/delivery-address-skill`
+> (TK-10144) on 2026-08-02 per Steve's "work with the other terminal" email —
+> two parallel sessions had built the same generalization. This repo is
+> CANONICAL; the other is deprecated with a pointer.
diff --git a/config.example.json b/config.example.json
new file mode 100644
index 0000000..e1debd6
--- /dev/null
+++ b/config.example.json
@@ -0,0 +1,8 @@
+{
+  "user_email": "user@example.com",
+  "platform": "ubereats",
+  "cdp_port": 9223,
+  "reply_channel": "email",
+  "george_account": "",
+  "session_dir": ""
+}
diff --git a/references/platform-notes.md b/references/platform-notes.md
new file mode 100644
index 0000000..591143d
--- /dev/null
+++ b/references/platform-notes.md
@@ -0,0 +1,57 @@
+# Platform flow notes & failure modes
+
+Everything below UberEats was proven live on 2026-08-02 (TK-10140); DoorDash and
+Instacart entries are the expected shapes — verify with `state.js` at every gate
+and update this file as sessions teach us more.
+
+## UberEats (proven live)
+
+Login chain observed: email → email code → SMS 4-digit code → (sometimes) card
+missing-digits gate → logged in.
+
+- Login URL: `https://auth.uber.com/v2/?next_url=https%3A%2F%2Fwww.ubereats.com%2Flogin-redirect`
+  Email field `#PHONE_NUMBER_or_EMAIL_ADDRESS`, submit `#forward-button`.
+- **Session expiry is brutal** — well under 30 min. A code entered after expiry
+  returns "Bad request — Current session has timed out." Restart the flow; Uber
+  often skips already-passed steps and goes straight to the next gate, auto-firing
+  a fresh code (cooldown timer visible = code fired).
+- **SMS rate-limit**: "Error sending code / Bad request / Please use another
+  method" — restart the flow; Uber rotates to phone-call ("Call me with code")
+  or email code by itself. Email code is the best outcome (self-serve via George).
+- **Card missing-digits gate**: after 2FA, Uber may ask for N digits of a saved
+  card (e.g. "____-5001" = the 4 digits immediately before the last 4; on a
+  15-digit Amex that's digits 8–11). STATIC data, no expiry race. Exactly one
+  text input + Next; there is no alternative path — you need the user.
+- **Email codes via George**: strip `<style>` blocks before searching the body —
+  Uber's marketing CSS and its street address (e.g. "1725") look like codes.
+- Addresses: Account → Addresses → add; a pin editor may appear (drag only where
+  the USER said — never infer). Mid-order address edits are hard-blocked; the
+  only live-order path is order Help → change delivery address, when offered.
+
+## DoorDash (expected shape — verify live)
+
+- Login: `https://identity.doordash.com/auth` — email first; then either
+  password, a 6-digit emailed code, or a magic link. A magic link must be
+  clicked in OUR browser session: ask the user to paste the link/landing URL,
+  then `page.goto()` it in the matched tab.
+- Addresses: Account → Addresses. Live order: order page → Help → may allow
+  address change before pickup.
+- Watch for PerimeterX-style bot walls; we run REAL Chrome headed, which
+  usually passes. If hard-blocked, escalate to `browserbase` (metered).
+
+## Instacart (expected shape — verify live)
+
+- Login: `https://www.instacart.com/login` — email → 6-digit emailed code.
+- Address lives in two places: Account settings AND per-order. A live order's
+  address is editable only until shopping starts ("Your orders" → order →
+  delivery details). Fix BOTH the account default and the live order when asked.
+
+## Universal failure modes
+
+| symptom | meaning | action |
+|---|---|---|
+| code rejected, "session timed out" | auth session expired (<30 min) | restart-login.js; tell user fresh code coming |
+| "Error sending code / use another method" | channel rate-limited | restart-login.js; platform rotates channel |
+| page reset to email/phone entry | parked state expired | restart-login.js |
+| tab index changed / wrong page | another automation opened tabs | ALWAYS select by `--match`, never index |
+| number in email looks like a code | CSS/address noise | strip `<style>`, prefer the code near "code" text |
diff --git a/scripts/enter-code.js b/scripts/enter-code.js
new file mode 100644
index 0000000..a367d00
--- /dev/null
+++ b/scripts/enter-code.js
@@ -0,0 +1,34 @@
+// Enter a 2FA / verification code into whatever code UI the page shows:
+// N separate one-digit boxes, or a single text input. Then click Next/Submit.
+// Usage: node enter-code.js --port 9223 --match uber.com --code 1234
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const code = args.code;
+  if (!code || !/^\d{3,8}$/.test(code)) { console.error('Need --code <3-8 digits>'); process.exit(2); }
+  const match = args.match || (args.platform && platform(args.platform).match);
+  if (!match) { console.error('Need --match <url-substring> or --platform'); process.exit(2); }
+
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, match);
+  const inputs = await page.$$('input:visible');
+  console.log('visible inputs:', inputs.length);
+  if (inputs.length >= code.length) {
+    for (let i = 0; i < code.length; i++) await inputs[i].fill(code[i]);
+  } else if (inputs.length >= 1) {
+    await inputs[0].click();
+    await page.keyboard.type(code, { delay: 120 });
+  } else {
+    console.error('No visible input to enter the code into — run state.js first.');
+    process.exit(2);
+  }
+  await page.waitForTimeout(1500);
+  for (const label of ['Next', 'Submit', 'Verify', 'Continue', 'Sign in']) {
+    const btn = await page.$(`button:has-text("${label}")`);
+    if (btn) { await btn.click().catch(() => {}); break; }
+  }
+  await page.waitForTimeout(6000);
+  await dumpState(page, sessionDir(cfg, args), 'after-code');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
diff --git a/scripts/launch.js b/scripts/launch.js
new file mode 100644
index 0000000..0ccec9b
--- /dev/null
+++ b/scripts/launch.js
@@ -0,0 +1,29 @@
+// Launch a long-lived headed real-Chrome session with CDP for the relay scripts.
+// Usage: node launch.js --platform ubereats --user me@example.com [--port 9223]
+const path = require('path');
+const { chromium } = require('playwright');
+const { parseArgs, loadConfig, platform } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const plat = platform(args.platform || cfg.platform);
+  const user = args.user || cfg.user_email;
+  if (!user) { console.error('Need --user <email> (or user_email in config.json)'); process.exit(2); }
+  const port = args.port || cfg.cdp_port || 9223;
+  const profile = path.join(__dirname, '..', 'profiles',
+    `${user.replace(/[@.]/g, '_')}-${args.platform || cfg.platform}`);
+
+  const ctx = await chromium.launchPersistentContext(profile, {
+    channel: 'chrome',
+    headless: false,
+    viewport: { width: 1360, height: 900 },
+    args: [`--remote-debugging-port=${port}`],
+  });
+  const page = ctx.pages()[0] || (await ctx.newPage());
+  await page.goto(plat.home, { waitUntil: 'domcontentloaded', timeout: 60000 })
+    .catch(e => console.log('nav err', e.message));
+  console.log(`READY cdp=http://127.0.0.1:${port} profile=${profile}`);
+  console.log('URL:', page.url());
+  await new Promise(() => {}); // keep alive
+})().catch(e => { console.error('LAUNCH FAIL', e.message); process.exit(1); });
diff --git a/scripts/lib.js b/scripts/lib.js
new file mode 100644
index 0000000..ce04202
--- /dev/null
+++ b/scripts/lib.js
@@ -0,0 +1,82 @@
+// Shared helpers for delivery-address scripts. Zero deps beyond playwright.
+const fs = require('fs');
+const path = require('path');
+
+const PLATFORMS = {
+  ubereats: {
+    home: 'https://www.ubereats.com/',
+    login: 'https://auth.uber.com/v2/?next_url=https%3A%2F%2Fwww.ubereats.com%2Flogin-redirect',
+    match: 'uber.com',
+    emailField: '#PHONE_NUMBER_or_EMAIL_ADDRESS',
+    emailSubmit: '#forward-button',
+  },
+  doordash: {
+    home: 'https://www.doordash.com/',
+    login: 'https://identity.doordash.com/auth',
+    match: 'doordash.com',
+    emailField: 'input[type="email"]',
+    emailSubmit: 'button[type="submit"]',
+  },
+  instacart: {
+    home: 'https://www.instacart.com/',
+    login: 'https://www.instacart.com/login',
+    match: 'instacart.com',
+    emailField: 'input[type="email"]',
+    emailSubmit: 'button[type="submit"]',
+  },
+};
+
+function parseArgs(argv) {
+  const args = {};
+  for (let i = 2; i < argv.length; i++) {
+    const m = argv[i].match(/^--([a-z-]+)$/);
+    if (m) { args[m[1]] = argv[i + 1]; i++; }
+  }
+  return args;
+}
+
+function loadConfig() {
+  const p = path.join(__dirname, '..', 'config.json');
+  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {};
+}
+
+function platform(name) {
+  const p = PLATFORMS[name];
+  if (!p) { console.error(`Unknown platform "${name}". One of: ${Object.keys(PLATFORMS).join(', ')}`); process.exit(2); }
+  return p;
+}
+
+function sessionDir(cfg, args) {
+  const dir = args['session-dir'] || cfg.session_dir ||
+    path.join(__dirname, '..', 'sessions', `${(args.user || cfg.user_email || 'user').replace(/[@.]/g, '_')}-${args.platform || 'session'}`);
+  fs.mkdirSync(dir, { recursive: true });
+  return dir;
+}
+
+// HARD RULE: select the tab by URL substring, never pages()[0] — other
+// automations share the browser (Tesla tab hijacked slot 0 on 2026-08-02).
+async function findTab(port, match) {
+  const { chromium } = require('playwright');
+  const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
+  const pages = browser.contexts()[0].pages();
+  const page = pages.find(p => p.url().includes(match));
+  if (!page) {
+    console.error(`NO TAB matching "${match}". Open tabs: ${pages.map(p => p.url()).join(' | ')}`);
+    process.exit(2);
+  }
+  return { browser, page };
+}
+
+async function dumpState(page, dir, label) {
+  const shot = path.join(dir, `${label}-${Date.now()}.png`);
+  await page.screenshot({ path: shot }).catch(() => {});
+  console.log('URL:', page.url());
+  console.log('SHOT:', shot);
+  console.log(await page.evaluate(() => document.body.innerText.slice(0, 1000)));
+  const inputs = await page.evaluate(() =>
+    Array.from(document.querySelectorAll('input')).map(i =>
+      ({ t: i.type, ph: i.placeholder, val: i.value ? '<set>' : '', vis: !!i.offsetParent })));
+  console.log('INPUTS:', JSON.stringify(inputs.filter(i => i.vis)));
+}
+
+module.exports = { PLATFORMS, parseArgs, loadConfig, platform, sessionDir, findTab, dumpState };
diff --git a/scripts/restart-login.js b/scripts/restart-login.js
new file mode 100644
index 0000000..36dc331
--- /dev/null
+++ b/scripts/restart-login.js
@@ -0,0 +1,27 @@
+// Restart an expired auth flow: reload the platform's login URL and resubmit
+// the user's email. Usually auto-fires a FRESH code (which invalidates all
+// prior codes — TELL THE USER "newest code wins").
+// Usage: node restart-login.js --platform ubereats --user me@example.com --port 9223
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const plat = platform(args.platform || cfg.platform);
+  const user = args.user || cfg.user_email;
+  if (!user) { console.error('Need --user <email> (or user_email in config.json)'); process.exit(2); }
+
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, args.match || plat.match);
+  await page.goto(plat.login, { waitUntil: 'domcontentloaded' });
+  await page.waitForTimeout(4000);
+  const emailInput = await page.$(plat.emailField);
+  if (emailInput) {
+    await page.fill(plat.emailField, user);
+    await page.click(plat.emailSubmit);
+    await page.waitForTimeout(7000);
+  } else {
+    console.log('No email input — flow may have skipped straight to a code gate:');
+  }
+  await dumpState(page, sessionDir(cfg, args), 'restart');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
diff --git a/scripts/state.js b/scripts/state.js
new file mode 100644
index 0000000..2780f0d
--- /dev/null
+++ b/scripts/state.js
@@ -0,0 +1,13 @@
+// Diagnose the current gate: screenshot + page text + visible inputs.
+// Usage: node state.js --port 9223 --match uber.com [--platform ubereats]
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const match = args.match || (args.platform && platform(args.platform).match);
+  if (!match) { console.error('Need --match <url-substring> or --platform'); process.exit(2); }
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, match);
+  await dumpState(page, sessionDir(cfg, args), 'state');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });

← 4fddd9a delivery-address-fix skill: engine + ubereats(PROVEN)/doorda  ·  back to Delivery Address Fix  ·  5x sweep 1: playwright global-root fallback resolver (repo h 1b1b314 →