← 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
M .gitignoreM SKILL.mdA config.example.jsonA references/platform-notes.mdA scripts/enter-code.jsA scripts/launch.jsA scripts/lib.jsA scripts/restart-login.jsA scripts/state.js
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 →