← back to Norma
agents/instagram-agent/redo-last5.js
89 lines
#!/usr/bin/env node
/**
* redo-last5.js — re-publish the last-5-days fleet posts with the CORRECTED caption
* (Name + Color + Model/SKU, no http link). TK-10574.
*
* Reads the FROZEN delete-list (data/redo-originals-<date>.jsonl) — the 85 originals
* captured before any repost — and, for each, reposts the SAME product via post-to.js,
* which now composes the fixed caption from content.js.
*
* node redo-last5.js # DRY: preview every corrected caption, publish nothing
* node redo-last5.js --go # LIVE: repost each (customer-facing — run this yourself)
* node redo-last5.js --go --only designerwallcoverings # just one account
*
* On --go it writes data/redo-results-<date>.jsonl mapping each ORIGINAL
* (handle, old media_id, old permalink) -> the NEW permalink, so the openclaw
* deletion step deletes exactly the right originals.
*/
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const content = require('./content');
const DATE = '20260814';
const SRC = path.join(__dirname, 'data', `redo-originals-${DATE}.jsonl`);
const OUT = path.join(__dirname, 'data', `redo-results-${DATE}.jsonl`);
const PACE_MS = 4000; // gentle spacing so a fleet-wide sweep never hammers the Graph API
const args = process.argv.slice(2);
const GO = args.includes('--go');
const only = (() => { const i = args.indexOf('--only'); return i >= 0 ? args[i + 1] : null; })();
// Already-reposted keys (idempotent + crash-resumable) — skip anything already in redo-results.
const done = new Set();
if (fs.existsSync(OUT)) {
for (const l of fs.readFileSync(OUT, 'utf8').trim().split('\n').filter(Boolean)) {
try { const r = JSON.parse(l); done.add(`${r.handle}|${r.product_handle}`); } catch { /* skip */ }
}
}
const rows = fs.readFileSync(SRC, 'utf8').trim().split('\n')
.map((l) => JSON.parse(l))
.filter((r) => (only ? r.handle === only : true))
.filter((r) => !done.has(`${r.handle}|${r.product_handle}`)); // never double-post
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
console.log(`${GO ? 'LIVE REPOST' : 'DRY PREVIEW'} — ${rows.length} post(s)${only ? ` (@${only})` : ' across the fleet'}\n`);
let ok = 0, fail = 0, skip = 0;
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const tag = `[${i + 1}/${rows.length}] @${r.handle} ${r.product_handle}`;
// Preview the corrected caption (also surfaces products that now fail to resolve)
let preview;
try { preview = await content.resolveProduct(r.product_handle); }
catch (e) { console.log(` ⚠ SKIP ${tag} — ${e.message}`); skip++; continue; }
if (!GO) {
console.log(`── ${tag}`);
console.log(preview.caption.split('\n').slice(0, 3).join('\n')); // name/color/sku lines
console.log('');
await sleep(700); // pace storefront reads so a full sweep doesn't hit the rate limiter
continue;
}
// LIVE: hand off to the sanctioned publisher (its own --confirm gate + ledger append)
try {
const out = execFileSync(process.execPath,
['post-to.js', r.handle, '--product', r.product_handle, '--confirm'],
{ cwd: __dirname, encoding: 'utf8' });
const m = out.match(/posted IMAGE (\S+)/);
const newPermalink = m ? m[1] : null;
fs.appendFileSync(OUT, JSON.stringify({
handle: r.handle, product_handle: r.product_handle,
old_media_id: r.media_id, old_permalink: r.permalink,
new_permalink: newPermalink, ts: new Date().toISOString(),
}) + '\n');
console.log(` ✓ ${tag} — new ${newPermalink || '(permalink pending)'}`);
ok++;
} catch (e) {
console.log(` ✗ ${tag} — ${String(e.stderr || e.message).trim().split('\n').pop()}`);
fail++;
}
if (i < rows.length - 1) await sleep(PACE_MS);
}
console.log(`\n${GO ? 'Reposted' : 'Previewed'}: ${ok || rows.length - skip} ok, ${fail} failed, ${skip} skipped.`);
if (GO) console.log(`Mapping written -> ${OUT}\nNext: delete the ${ok} originals (openclaw), keyed by old_permalink.`);
})();