← back to Norma Platform
IG: add serialized, per-account-aware Spoonflower delete script (openclaw)
c90044bf6ecf45b77b93175318039dd52dd6bd71 · 2026-08-17 07:58:15 -0700 · Steve Abrams
Reads the confirmed hitlist and deletes via logged-in Chrome (··· -> Delete ->
confirm -> verify gone). Serialized + fails loudly if not logged in as the
owning account. --dry does a reachability probe that deletes nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A agents/instagram-agent/delete-spoonflower.js
Diff
commit c90044bf6ecf45b77b93175318039dd52dd6bd71
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 17 07:58:15 2026 -0700
IG: add serialized, per-account-aware Spoonflower delete script (openclaw)
Reads the confirmed hitlist and deletes via logged-in Chrome (··· -> Delete ->
confirm -> verify gone). Serialized + fails loudly if not logged in as the
owning account. --dry does a reachability probe that deletes nothing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
agents/instagram-agent/delete-spoonflower.js | 118 +++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
diff --git a/agents/instagram-agent/delete-spoonflower.js b/agents/instagram-agent/delete-spoonflower.js
new file mode 100644
index 0000000..35eb023
--- /dev/null
+++ b/agents/instagram-agent/delete-spoonflower.js
@@ -0,0 +1,118 @@
+#!/usr/bin/env node
+/**
+ * delete-spoonflower.js — delete the discovered Spoonflower posts on Instagram via openclaw
+ * (logged-in real Chrome). The Graph API cannot delete a published post, so this drives the
+ * ··· → Delete → confirm flow and VERIFIES the post is gone before recording it.
+ *
+ * Reads data/spoonflower-hitlist.jsonl (the 4 confirmed targets).
+ * Writes data/deleted-posts.jsonl (tombstones) + data/spoonflower-delete-results.jsonl.
+ *
+ * SAFETY:
+ * - Serialized (one delete at a time) — rapid browser deletes are Meta's #1 ban trigger.
+ * - Per-post it detects the ··· menu; if the logged-in account does NOT own the post,
+ * it SKIPS loudly (never guesses) so you can switch accounts and re-run.
+ * - Only tombstones a post it VERIFIED is gone (permalink 404s afterward).
+ *
+ * Usage:
+ * node delete-spoonflower.js --dry # navigate + report reachability, delete NOTHING
+ * node delete-spoonflower.js # delete every reachable target
+ * node delete-spoonflower.js grassclothwallpaper # only this account's posts
+ */
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const DATA = path.join(__dirname, 'data');
+const HITLIST = path.join(DATA, 'spoonflower-hitlist.jsonl');
+const TOMBS = path.join(DATA, 'deleted-posts.jsonl');
+const RESULTS = path.join(DATA, 'spoonflower-delete-results.jsonl');
+
+const args = process.argv.slice(2);
+const DRY = args.includes('--dry');
+const onlyHandle = args.find((a) => !a.startsWith('--')) || null;
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+function oc(cmd) { return execSync(`openclaw browser ${cmd}`, { encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] }); }
+let TAB = null;
+function ocOpen(url) { const o = oc(`open ${JSON.stringify(url)} --timeout 30000`); TAB = (o.match(/id:\s*([A-F0-9]+)/i) || [])[1] || TAB; return TAB; }
+function ocNav(url) { if (!TAB) return ocOpen(url); oc(`navigate ${JSON.stringify(url)} --target-id ${TAB}`); }
+function ocSnap() { try { return oc(`snapshot --format ai --limit 800 ${TAB ? `--target-id ${TAB}` : ''}`); } catch { return ''; } }
+function ocFindRef(snap, re) {
+ for (const line of String(snap).split('\n')) {
+ if (re.test(line)) { const m = line.match(/\[ref=([A-Za-z0-9_]+)\]/); if (m) return m[1]; }
+ }
+ return null;
+}
+function ocClick(ref) { oc(`click ${ref} --target-id ${TAB}`); }
+
+async function loggedIn() {
+ ocOpen('https://www.instagram.com/'); await sleep(2500);
+ const s = ocSnap();
+ if (/Enter the code|Log in|Log In|Phone number, username|Create new account/i.test(s) && !/\bHome\b|\bSearch\b|\bProfile\b/i.test(s)) return false;
+ return true;
+}
+
+// Delete one post; returns {status, detail}. status ∈ deleted|not-owner|no-menu|still-present|error
+async function del(permalink) {
+ ocNav(permalink); await sleep(2800);
+ let snap = ocSnap();
+ if (/isn't available|Sorry, this page|Page Not Found/i.test(snap)) return { status: 'already-gone' };
+ const more = ocFindRef(snap, /More options|^\s*-?\s*button "More"|More$/i);
+ if (!more) return { status: 'no-menu', detail: 'no ··· menu — Chrome not logged in as the owning account' };
+ ocClick(more); await sleep(1400);
+ const del1 = ocFindRef(ocSnap(), /\bDelete\b/);
+ if (!del1) return { status: 'not-owner', detail: 'no Delete item — logged-in account does not own this post' };
+ ocClick(del1); await sleep(1400);
+ const confirm = ocFindRef(ocSnap(), /\bDelete\b/);
+ if (!confirm) return { status: 'error', detail: 'no confirm Delete button appeared' };
+ ocClick(confirm); await sleep(3200);
+ ocNav(permalink); await sleep(2800);
+ const gone = /isn't available|Sorry, this page|Page Not Found/i.test(ocSnap());
+ return gone ? { status: 'deleted' } : { status: 'still-present', detail: 'clicked Delete but post still resolves' };
+}
+
+(async () => {
+ let targets = fs.readFileSync(HITLIST, 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
+ if (onlyHandle) targets = targets.filter((t) => t.handle === onlyHandle.replace(/^@/, ''));
+ console.log(`${DRY ? 'DRY-RUN — ' : ''}${targets.length} target(s)${onlyHandle ? ` on @${onlyHandle}` : ''}:\n`);
+
+ if (!(await loggedIn())) {
+ console.error('✋ Instagram is NOT logged in (login/2FA screen detected). Complete the login in the openclaw Chrome first, then re-run.');
+ process.exit(2);
+ }
+ console.log('✓ Instagram session is logged in.\n');
+
+ const results = [];
+ for (const t of targets) {
+ process.stdout.write(`@${t.handle} ${t.permalink} … `);
+ if (DRY) {
+ // reachability probe only: open, look for ··· menu, DO NOT delete
+ ocNav(t.permalink); await sleep(2600);
+ const s = ocSnap();
+ const gone = /isn't available|Sorry, this page|Page Not Found/i.test(s);
+ const reach = !!ocFindRef(s, /More options|More$/i);
+ const status = gone ? 'already-gone' : reach ? 'reachable(owner)' : 'not-reachable(switch account)';
+ console.log(status);
+ results.push({ ...t, dry: true, status });
+ continue;
+ }
+ let r;
+ try { r = await del(t.permalink); } catch (e) { r = { status: 'error', detail: e.message.slice(0, 120) }; }
+ console.log(r.status + (r.detail ? ` — ${r.detail}` : ''));
+ if (r.status === 'deleted' || r.status === 'already-gone') {
+ fs.appendFileSync(TOMBS, JSON.stringify({ id: t.permalink, permalink: t.permalink, media_id: t.media_id || '',
+ handle: t.handle, ts: new Date().toISOString(), mode: 'live', reason: 'spoonflower-purge', verified: r.status === 'deleted' }) + '\n');
+ }
+ results.push({ ...t, status: r.status, detail: r.detail || '' });
+ await sleep(4000); // pace between deletes — ban avoidance
+ }
+
+ fs.appendFileSync(RESULTS, results.map((r) => JSON.stringify({ ...r, at: new Date().toISOString() })).join('\n') + '\n');
+ const done = results.filter((r) => r.status === 'deleted' || r.status === 'already-gone').length;
+ const stuck = results.filter((r) => ['not-owner', 'no-menu', 'not-reachable(switch account)'].includes(r.status));
+ console.log(`\n=== ${DRY ? 'DRY-RUN complete' : `${done}/${targets.length} removed`} ===`);
+ if (stuck.length) {
+ console.log(`\n⚠ ${stuck.length} post(s) need a different account logged in:`);
+ for (const s of stuck) console.log(` @${s.handle} ${s.permalink} → log into/switch to @${s.handle}, then re-run: node delete-spoonflower.js ${s.handle}`);
+ }
+})();
← ffc9303 Bulk-select delete on IG auto-post viewer
·
back to Norma Platform
·
auto-data-snapshot: 2026-08-17T08:03:25 (1 data files) — age a843c5b →