← back to AbramsEgo
spend-reviews: short product names in drafts + robust Amazon ingest
f7369eb515f212eaf0fd84926ff1e22cfd03ee28 · 2026-09-11 09:58:53 -0700 · Steve
- drafts.js: shortProduct() trims Amazon's 200-char keyword-soup titles to a natural short name (used in review title/body + seller email), so drafts read human not robotic
- amazon.js: point at current /your-orders/orders URL + add settle-retry loop (poll until the SPA renders order cards) — fixes the evaluate-before-render race that scraped 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M lib/spend-reviews/adapters/amazon.jsM lib/spend-reviews/drafts.js
Diff
commit f7369eb515f212eaf0fd84926ff1e22cfd03ee28
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Sep 11 09:58:53 2026 -0700
spend-reviews: short product names in drafts + robust Amazon ingest
- drafts.js: shortProduct() trims Amazon's 200-char keyword-soup titles to a natural short name (used in review title/body + seller email), so drafts read human not robotic
- amazon.js: point at current /your-orders/orders URL + add settle-retry loop (poll until the SPA renders order cards) — fixes the evaluate-before-render race that scraped 0
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/spend-reviews/adapters/amazon.js | 18 +++++++++++++-----
lib/spend-reviews/drafts.js | 19 ++++++++++++++++---
2 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/lib/spend-reviews/adapters/amazon.js b/lib/spend-reviews/adapters/amazon.js
index 24aeeada..ef941bf6 100644
--- a/lib/spend-reviews/adapters/amazon.js
+++ b/lib/spend-reviews/adapters/amazon.js
@@ -13,7 +13,7 @@
const { execFile } = require('child_process');
const cfg = require('../config');
-const ORDERS_URL = 'https://www.amazon.com/gp/css/order-history';
+const ORDERS_URL = 'https://www.amazon.com/your-orders/orders';
function openclaw(args, timeoutMs = 45000) {
return new Promise((resolve) => {
@@ -96,14 +96,22 @@ async function ingest(opts) {
return { ok: false, items: [], meta: { source: 'amazon', needsAction: 'openclaw-open-failed',
note: 'openclaw could not open the order-history page.', detail: open.stderr } };
}
- // 3. scrape via evaluate
- const evalRes = await openclaw(['browser', 'evaluate', '--fn', SCRAPE_FN], 45000);
- const data = extractJson(evalRes.stdout);
+ // 3. scrape via evaluate — the orders page is an SPA that renders cards AFTER a
+ // redirect + async load, so poll a few times (settle) before giving up on an
+ // empty read. Stop early once we see the login wall OR real orders.
+ let data = null; let lastRaw = '';
+ for (let attempt = 0; attempt < 6; attempt++) {
+ await new Promise((r) => setTimeout(r, attempt === 0 ? 2500 : 2000));
+ const evalRes = await openclaw(['browser', 'evaluate', '--fn', SCRAPE_FN], 45000);
+ lastRaw = evalRes.stdout || '';
+ data = extractJson(lastRaw);
+ if (data && (data.login || (data.orders && data.orders.length))) break;
+ }
if (!data) {
// degrade: could not read structured data (markup changed / eval blocked)
return { ok: true, items: [], meta: { source: 'amazon',
note: 'opened order history but could not read structured orders (Amazon DOM may have changed) — adjust SCRAPE_FN selectors.',
- raw: (evalRes.stdout || '').slice(0, 400) } };
+ raw: lastRaw.slice(0, 400) } };
}
if (data.login) {
return { ok: false, items: [], meta: { source: 'amazon', needsAction: 'amazon-login',
diff --git a/lib/spend-reviews/drafts.js b/lib/spend-reviews/drafts.js
index 06d9341e..58c3a924 100644
--- a/lib/spend-reviews/drafts.js
+++ b/lib/spend-reviews/drafts.js
@@ -37,6 +37,19 @@ function deslop(text) {
function hash(s) { let h = 0; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) | 0; return h; }
function pick(arr, seed) { return arr[Math.abs(hash(seed)) % arr.length]; }
+// Amazon titles are 150-200 chars of keyword soup ("Maldon - Sea Salt Flakes
+// Tub, 3.1 lb (1.4kg) - Unique Pyramid Shaped..."). A human review says "the
+// Maldon sea salt", not the whole SKU. Trim to a short, natural name.
+function shortProduct(p) {
+ if (!p) return '';
+ let s = String(p).split(/[,(|;]/)[0].trim(); // drop everything after the first , ( | ;
+ s = s.replace(/\s*[-–—]\s*/g, ' ').replace(/\s{2,}/g, ' ').trim(); // flatten internal dashes
+ const words = s.split(/\s+/);
+ if (words.length > 7) s = words.slice(0, 7).join(' ');
+ if (s.length > 48) s = s.slice(0, 48).replace(/\s+\S*$/, '').trim();
+ return s || String(p).slice(0, 40).trim();
+}
+
// ---- sentence banks (wide, so batches don't read as clones) -------------------
const OPEN = [
'Really happy with this one.', 'Genuinely pleased with the whole experience.',
@@ -82,7 +95,7 @@ const RATING_LINE = [
function fill(t, item) {
const merch = item.display_merchant || item.merchant || 'them';
- return t.replace(/\{product\}/g, item.product || 'item').replace(/\{merchant\}/g, merch);
+ return t.replace(/\{product\}/g, shortProduct(item.product) || 'item').replace(/\{merchant\}/g, merch);
}
// choose one of a few STRUCTURES per item, plus whether to append a rating line
@@ -104,7 +117,7 @@ function genGoogleReview(item) {
function genAmazonReview(item) {
const s = Math.abs(hash(item.id + 'ashape')) % 3;
- const prod = item.product || 'this item';
+ const prod = shortProduct(item.product) || 'this item';
const specifics = [];
if (item.product) specifics.push(`The ${prod} is exactly as described`);
if (item.asin) specifics.push('it arrived quickly and well packed');
@@ -122,7 +135,7 @@ function genAmazonReview(item) {
function genSellerEmail(item) {
const who = item.seller || item.display_merchant || item.merchant || 'there';
- const thing = item.product || 'the product';
+ const thing = shortProduct(item.product) || 'the product';
const subjBank = [`Thank you — ${thing}`, `Really pleased with the ${thing}`, `A quick thank-you for the ${thing}`, `Great experience with the ${thing}`];
const subject = deslop(pick(subjBank, item.id + 'st'));
const openBank = [
← cb0d3731 auto-data-snapshot: 2026-09-11T09:31:49 (1 data files) — dat
·
back to AbramsEgo
·
auto-data-snapshot: 2026-09-11T10:08:53 (1 data files) — dat 79d5a95a →