social-autoposter 1.6.4 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +47 -1
- package/bin/server.js +5 -3
- package/package.json +1 -1
- package/scripts/twitter_browser.py +102 -26
- package/skill/lib/twitter-backend.sh +32 -3
- package/skill/run-instagram-render.sh +36 -2
package/bin/cli.js
CHANGED
|
@@ -130,14 +130,60 @@ function installBrowserAgentConfigs() {
|
|
|
130
130
|
console.log(` browser profile dirs ready -> ${profilesDir}/{${BROWSER_PROFILES.join(',')}}`);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Detect whether we are running inside an AppMaker E2B VM. AppMaker provisions
|
|
134
|
+
// a Chromium on port 9222 behind the SOAX residential proxy at 127.0.0.1:3003,
|
|
135
|
+
// and that Chromium is the one the user logs into via the AppMaker UI (profile
|
|
136
|
+
// /root/.chromium-profile). The browser-harness Chrome on port 9555 with its
|
|
137
|
+
// own (logged-out, un-proxied) profile is wrong for this host, so we:
|
|
138
|
+
// 1. skip installBrowserHarness() entirely (saves disk + avoids a second
|
|
139
|
+
// headless Chrome ever spawning).
|
|
140
|
+
// 2. write ~/.social-autoposter-env so skill/lib/twitter-backend.sh sources
|
|
141
|
+
// TWITTER_CDP_URL=http://127.0.0.1:9222 instead of the default 9555.
|
|
142
|
+
// Detection: presence of /opt/startup.sh (the AppMaker bootstrap script that
|
|
143
|
+
// only exists on these VMs) AND a live HTTP response on 127.0.0.1:9222.
|
|
144
|
+
function isAppMakerVm() {
|
|
145
|
+
if (process.platform !== 'linux') return false;
|
|
146
|
+
if (!fs.existsSync('/opt/startup.sh')) return false;
|
|
147
|
+
// Probe Chromium DevTools on 9222. 2s timeout; if it answers, we're on AppMaker.
|
|
148
|
+
const probe = spawnSync('curl', ['-sf', '--max-time', '2', '-o', '/dev/null', 'http://127.0.0.1:9222/json/version'], { stdio: 'ignore' });
|
|
149
|
+
return probe.status === 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Write ~/.social-autoposter-env so skill/lib/twitter-backend.sh picks up the
|
|
153
|
+
// AppMaker-specific TWITTER_CDP_URL before its `${VAR:-default}` fallback hits.
|
|
154
|
+
// Idempotent: rewrites the file every invocation so a config edit on the VM
|
|
155
|
+
// can't drift away from what cli.js intends.
|
|
156
|
+
function writeAppMakerEnvFile() {
|
|
157
|
+
const envPath = path.join(HOME, '.social-autoposter-env');
|
|
158
|
+
const body = [
|
|
159
|
+
'# social-autoposter per-host env overrides',
|
|
160
|
+
'# Auto-generated by social-autoposter init/update on AppMaker E2B VMs.',
|
|
161
|
+
'# Edit by hand only if you know what you are doing; it gets rewritten on every update.',
|
|
162
|
+
'',
|
|
163
|
+
'# Point twitter pipeline at AppMaker\'s proxied Chromium (SOAX residential exit',
|
|
164
|
+
'# at 127.0.0.1:3003) instead of the harness Chrome on 9555. The Chromium on',
|
|
165
|
+
'# 9222 is the one the user logs into via the AppMaker UI.',
|
|
166
|
+
'export TWITTER_CDP_URL="http://127.0.0.1:9222"',
|
|
167
|
+
'',
|
|
168
|
+
].join('\n');
|
|
169
|
+
fs.writeFileSync(envPath, body);
|
|
170
|
+
console.log(` AppMaker VM detected -> wrote ${envPath} (TWITTER_CDP_URL=http://127.0.0.1:9222)`);
|
|
171
|
+
}
|
|
172
|
+
|
|
133
173
|
// Provision the browser-harness toolchain that backs the twitter-harness MCP:
|
|
134
174
|
// 1. install uv (Astral) if missing
|
|
135
175
|
// 2. git-clone browser-use/browser-harness
|
|
136
176
|
// 3. uv tool install -e . (provides the `browser-harness` CLI)
|
|
137
177
|
// 4. ensure `mcp` Python package is importable for server.py
|
|
138
178
|
// 5. copy our shipped server.py into ~/.claude/mcp-servers/browser-harness/
|
|
139
|
-
// All steps are idempotent.
|
|
179
|
+
// All steps are idempotent. Skipped entirely on AppMaker VMs (the proxied
|
|
180
|
+
// Chromium on 9222 replaces the harness).
|
|
140
181
|
function installBrowserHarness() {
|
|
182
|
+
if (isAppMakerVm()) {
|
|
183
|
+
console.log(' AppMaker VM detected -> skipping browser-harness install (Chromium on 9222 is canonical)');
|
|
184
|
+
writeAppMakerEnvFile();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
141
187
|
console.log(' setting up browser-harness (twitter-harness MCP backend)...');
|
|
142
188
|
|
|
143
189
|
// Step 1: uv. Try the official installer first; fall back to pip.
|
package/bin/server.js
CHANGED
|
@@ -542,7 +542,7 @@ const RUN_LINE_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\s*\|\s*(\S+)\s*\|\s*
|
|
|
542
542
|
const PLATFORM_LABELS = {
|
|
543
543
|
twitter: 'Twitter', reddit: 'Reddit', linkedin: 'LinkedIn',
|
|
544
544
|
moltbook: 'MoltBook', github: 'GitHub', dev: 'Dev',
|
|
545
|
-
hackernews: 'HackerNews', youtube: 'YouTube',
|
|
545
|
+
hackernews: 'HackerNews', youtube: 'YouTube', instagram: 'Instagram',
|
|
546
546
|
};
|
|
547
547
|
|
|
548
548
|
// Standalone jobs with no platform axis. script_name -> display label.
|
|
@@ -624,6 +624,7 @@ function classifyScript(script) {
|
|
|
624
624
|
match(/^link_edit_(\w+)$/, 'link-edit', 'Link Edit') ||
|
|
625
625
|
match(/^thread_(\w+)$/, 'post', 'Post Threads') ||
|
|
626
626
|
match(/^post_(\w+)$/, 'post-comments', 'Post Comments') ||
|
|
627
|
+
match(/^render_(\w+)$/, 'render', 'Render') ||
|
|
627
628
|
match(/^engage_(\w+)$/, 'engage', 'Engage') ||
|
|
628
629
|
match(/^dm_outreach_(\w+)$/, 'dm-outreach', 'DM Outreach') ||
|
|
629
630
|
match(/^dm_replies_(\w+)$/, 'dm-replies', 'DM Replies') ||
|
|
@@ -6164,7 +6165,7 @@ async function handleApi(req, res) {
|
|
|
6164
6165
|
const windowKey = Object.prototype.hasOwnProperty.call(WINDOW_HOURS, rawWindow) ? rawWindow : '7d';
|
|
6165
6166
|
const windowHours = WINDOW_HOURS[windowKey];
|
|
6166
6167
|
const rawPlatform = String(url.searchParams.get('platform') || '').toLowerCase().trim();
|
|
6167
|
-
const ALLOWED_PLATFORMS = new Set(['reddit', 'twitter', 'x', 'linkedin', 'moltbook', 'github']);
|
|
6168
|
+
const ALLOWED_PLATFORMS = new Set(['reddit', 'twitter', 'x', 'linkedin', 'moltbook', 'github', 'instagram']);
|
|
6168
6169
|
const platformFilter = ALLOWED_PLATFORMS.has(rawPlatform) ? rawPlatform : '';
|
|
6169
6170
|
const pc = auth.projectClause(req.user, 'pl.project_name', url.searchParams.get('project'));
|
|
6170
6171
|
if (!pc.ok) return json(res, { destinations: [], window: windowKey, platform: 'all' });
|
|
@@ -6290,7 +6291,7 @@ async function handleApi(req, res) {
|
|
|
6290
6291
|
const windowKey = Object.prototype.hasOwnProperty.call(WINDOW_HOURS, rawWindow) ? rawWindow : '7d';
|
|
6291
6292
|
const windowHours = WINDOW_HOURS[windowKey];
|
|
6292
6293
|
const rawPlatform = String(url.searchParams.get('platform') || '').toLowerCase().trim();
|
|
6293
|
-
const ALLOWED_PLATFORMS = new Set(['reddit', 'twitter', 'x', 'linkedin', 'moltbook', 'github']);
|
|
6294
|
+
const ALLOWED_PLATFORMS = new Set(['reddit', 'twitter', 'x', 'linkedin', 'moltbook', 'github', 'instagram']);
|
|
6294
6295
|
const platformFilter = ALLOWED_PLATFORMS.has(rawPlatform) ? rawPlatform : '';
|
|
6295
6296
|
const pc = auth.projectClause(req.user, 'pl.project_name', url.searchParams.get('project'));
|
|
6296
6297
|
if (!pc.ok) return json(res, { links: [], window: windowKey, platform: 'all' });
|
|
@@ -16559,6 +16560,7 @@ const PROJECT_STATUS_SORT_FIELDS = {
|
|
|
16559
16560
|
linkedin: { type: 'numeric', value: r => Number(r.by_platform && r.by_platform.linkedin) || 0 },
|
|
16560
16561
|
moltbook: { type: 'numeric', value: r => Number(r.by_platform && r.by_platform.moltbook) || 0 },
|
|
16561
16562
|
github: { type: 'numeric', value: r => Number(r.by_platform && r.by_platform.github) || 0 },
|
|
16563
|
+
instagram: { type: 'numeric', value: r => Number(r.by_platform && r.by_platform.instagram) || 0 },
|
|
16562
16564
|
};
|
|
16563
16565
|
function _sortProjectRows(rows) {
|
|
16564
16566
|
const { field, dir } = _projectStatusSort;
|
package/package.json
CHANGED
|
@@ -498,6 +498,67 @@ def _collect_our_reply_links(page):
|
|
|
498
498
|
}}"""))
|
|
499
499
|
|
|
500
500
|
|
|
501
|
+
def _wait_for_reply_textbox(page, total_timeout_ms=45000):
|
|
502
|
+
"""Wait for the reply composer textbox to mount. Returns a locator or None.
|
|
503
|
+
|
|
504
|
+
Polls multiple selectors because the React composer sometimes attaches late
|
|
505
|
+
on slow egress (E2B sandbox) and the aria-label has historically varied
|
|
506
|
+
("Post text" / "Tweet your reply" / "Post your reply"). The data-testid
|
|
507
|
+
`tweetTextarea_0` has been stable for years and is the primary signal.
|
|
508
|
+
"""
|
|
509
|
+
import time as _t
|
|
510
|
+
selectors = (
|
|
511
|
+
'[data-testid="tweetTextarea_0"]',
|
|
512
|
+
'[role="textbox"][aria-label="Post text"]',
|
|
513
|
+
'[role="textbox"][aria-label="Tweet your reply"]',
|
|
514
|
+
'[role="textbox"][aria-label="Post your reply"]',
|
|
515
|
+
)
|
|
516
|
+
deadline = _t.monotonic() + (total_timeout_ms / 1000.0)
|
|
517
|
+
while _t.monotonic() < deadline:
|
|
518
|
+
for sel in selectors:
|
|
519
|
+
try:
|
|
520
|
+
loc = page.locator(sel).first
|
|
521
|
+
if loc.count() > 0 and loc.is_visible():
|
|
522
|
+
return loc
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
page.wait_for_timeout(500)
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _dump_reply_failure_diag(page, tweet_url):
|
|
530
|
+
"""Dump screenshot + DOM state on reply_box_not_found. Returns a diag dict."""
|
|
531
|
+
import time as _t
|
|
532
|
+
ts = int(_t.time())
|
|
533
|
+
diag = {"ts": ts, "tweet_url": tweet_url}
|
|
534
|
+
try:
|
|
535
|
+
diag["final_url"] = page.url
|
|
536
|
+
except Exception as _e:
|
|
537
|
+
diag["final_url_err"] = str(_e)
|
|
538
|
+
try:
|
|
539
|
+
png_path = f"/tmp/twitter_reply_failure_{ts}.png"
|
|
540
|
+
page.screenshot(path=png_path, full_page=False)
|
|
541
|
+
diag["screenshot"] = png_path
|
|
542
|
+
except Exception as _e:
|
|
543
|
+
diag["screenshot_err"] = str(_e)
|
|
544
|
+
try:
|
|
545
|
+
diag["dom"] = page.evaluate("""() => {
|
|
546
|
+
const tbs = Array.from(document.querySelectorAll('[role="textbox"]'));
|
|
547
|
+
return {
|
|
548
|
+
title: (document.title || '').slice(0, 120),
|
|
549
|
+
textbox_count: tbs.length,
|
|
550
|
+
textbox_labels: tbs.map(t => t.getAttribute('aria-label')),
|
|
551
|
+
has_tweetTextarea_0: !!document.querySelector('[data-testid="tweetTextarea_0"]'),
|
|
552
|
+
has_login_modal: !!document.querySelector('[data-testid="loginButton"]'),
|
|
553
|
+
has_age_gate: !!document.querySelector('[data-testid="sensitive-media-button"]'),
|
|
554
|
+
page_text_snippet: (document.body && document.body.innerText || '').slice(0, 300),
|
|
555
|
+
};
|
|
556
|
+
}""")
|
|
557
|
+
except Exception as _e:
|
|
558
|
+
diag["dom_err"] = str(_e)
|
|
559
|
+
return diag
|
|
560
|
+
|
|
561
|
+
|
|
501
562
|
def reply_to_tweet(tweet_url, text, apply_campaigns=True):
|
|
502
563
|
"""Reply to a tweet.
|
|
503
564
|
|
|
@@ -627,39 +688,54 @@ def reply_to_tweet(tweet_url, text, apply_campaigns=True):
|
|
|
627
688
|
except Exception:
|
|
628
689
|
pass
|
|
629
690
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
691
|
+
# Navigate + locate reply box. Composer mount is flaky on E2B
|
|
692
|
+
# sandbox egress (~1-in-5 misses on first attempt). Strategy:
|
|
693
|
+
# up to 2 navigation attempts; on miss, scroll-nudge once before
|
|
694
|
+
# re-navigating. On final miss, dump diagnostics for triage.
|
|
695
|
+
reply_box = None
|
|
696
|
+
tweet_not_found = False
|
|
697
|
+
for nav_attempt in (1, 2):
|
|
633
698
|
try:
|
|
634
|
-
page.goto(tweet_url, wait_until="
|
|
699
|
+
page.goto(tweet_url, wait_until="load", timeout=60000)
|
|
635
700
|
except Exception:
|
|
636
|
-
|
|
637
|
-
|
|
701
|
+
try:
|
|
702
|
+
page.goto(tweet_url, wait_until="domcontentloaded", timeout=60000)
|
|
703
|
+
except Exception:
|
|
704
|
+
pass
|
|
705
|
+
page.wait_for_timeout(15000 if nav_attempt == 1 else 8000)
|
|
638
706
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
707
|
+
page_text = page.text_content("main") or ""
|
|
708
|
+
if "this page doesn't exist" in page_text.lower():
|
|
709
|
+
tweet_not_found = True
|
|
710
|
+
break
|
|
643
711
|
|
|
644
|
-
|
|
645
|
-
|
|
712
|
+
reply_box = _wait_for_reply_textbox(page, total_timeout_ms=45000)
|
|
713
|
+
if reply_box:
|
|
714
|
+
break
|
|
646
715
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
try:
|
|
652
|
-
reply_box = page.get_by_role("textbox", name="Post text")
|
|
653
|
-
reply_box.wait_for(timeout=30000)
|
|
654
|
-
except Exception:
|
|
655
|
-
# Scroll down to find the reply box
|
|
656
|
-
page.evaluate("window.scrollBy(0, 500)")
|
|
657
|
-
page.wait_for_timeout(3000)
|
|
716
|
+
# Nudge: small scroll + scroll back; sometimes coaxes the
|
|
717
|
+
# composer to attach when React stalled on the initial mount.
|
|
718
|
+
print(f"[reply_to_tweet] reply_box missing on nav_attempt={nav_attempt}; "
|
|
719
|
+
f"nudging + re-navigating", file=sys.stderr)
|
|
658
720
|
try:
|
|
659
|
-
|
|
660
|
-
|
|
721
|
+
page.evaluate("window.scrollBy(0, 400)")
|
|
722
|
+
page.wait_for_timeout(1500)
|
|
723
|
+
page.evaluate("window.scrollTo(0, 0)")
|
|
724
|
+
page.wait_for_timeout(1500)
|
|
661
725
|
except Exception:
|
|
662
|
-
|
|
726
|
+
pass
|
|
727
|
+
|
|
728
|
+
if tweet_not_found:
|
|
729
|
+
return {"ok": False, "error": "tweet_not_found"}
|
|
730
|
+
|
|
731
|
+
if not reply_box:
|
|
732
|
+
diag = _dump_reply_failure_diag(page, tweet_url)
|
|
733
|
+
print(f"[reply_to_tweet] reply_box_not_found diag: "
|
|
734
|
+
f"{json.dumps(diag, default=str)}", file=sys.stderr)
|
|
735
|
+
return {"ok": False, "error": "reply_box_not_found", "diag": diag}
|
|
736
|
+
|
|
737
|
+
# Snapshot our reply links right before posting (to detect the new one)
|
|
738
|
+
links_before = _collect_our_reply_links(page)
|
|
663
739
|
|
|
664
740
|
# Click and type the reply
|
|
665
741
|
reply_box.click()
|
|
@@ -30,10 +30,26 @@
|
|
|
30
30
|
# us. Kept as a function only so callers don't have to change.
|
|
31
31
|
|
|
32
32
|
MCP_CONFIG_FILE="$HOME/.claude/browser-agent-configs/twitter-harness-mcp.json"
|
|
33
|
+
|
|
34
|
+
# Per-host env override (written by bin/cli.js when installing on an AppMaker
|
|
35
|
+
# VM, where the canonical browser is Chromium on port 9222 behind the SOAX
|
|
36
|
+
# residential proxy at 127.0.0.1:3003, NOT the harness Chrome on 9555). On a
|
|
37
|
+
# Mac dev box this file does not exist, so the default below kicks in.
|
|
38
|
+
if [ -f "$HOME/.social-autoposter-env" ]; then
|
|
39
|
+
# shellcheck disable=SC1091
|
|
40
|
+
. "$HOME/.social-autoposter-env"
|
|
41
|
+
fi
|
|
42
|
+
|
|
33
43
|
# Tell twitter_browser.py (and any other Python helper that honors this env
|
|
34
|
-
# var) to skip ps-based discovery and connect directly to the
|
|
35
|
-
#
|
|
36
|
-
|
|
44
|
+
# var) to skip ps-based discovery and connect directly to the configured CDP
|
|
45
|
+
# endpoint. Default 9555 (Mac harness Chrome). AppMaker VMs pre-set this to
|
|
46
|
+
# http://127.0.0.1:9222 via ~/.social-autoposter-env above.
|
|
47
|
+
export TWITTER_CDP_URL="${TWITTER_CDP_URL:-http://127.0.0.1:9555}"
|
|
48
|
+
|
|
49
|
+
# Default harness URL — used by ensure_twitter_browser_for_backend +
|
|
50
|
+
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
51
|
+
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
52
|
+
_BH_DEFAULT_URL="http://127.0.0.1:9555"
|
|
37
53
|
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
38
54
|
BROWSER BACKEND: twitter-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
39
55
|
port 9555, profile ~/.claude/browser-profiles/browser-harness). The Chrome is already
|
|
@@ -131,6 +147,19 @@ _resolve_chrome_bin() {
|
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
ensure_twitter_browser_for_backend() {
|
|
150
|
+
# AppMaker / BYO Chrome: TWITTER_CDP_URL points at something other than our
|
|
151
|
+
# default harness URL. Don't touch that browser; just probe it and bail.
|
|
152
|
+
# The AppMaker bootstrap (and any future BYO setup) is responsible for
|
|
153
|
+
# keeping the externally-managed Chrome alive.
|
|
154
|
+
if [ "${TWITTER_CDP_URL:-$_BH_DEFAULT_URL}" != "$_BH_DEFAULT_URL" ]; then
|
|
155
|
+
local _ext_url="${TWITTER_CDP_URL}"
|
|
156
|
+
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
157
|
+
echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
echo "[$(date +%H:%M:%S)] ERROR: TWITTER_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host (AppMaker /opt/startup.sh, etc)." >&2
|
|
161
|
+
return 1
|
|
162
|
+
fi
|
|
134
163
|
# Probe + launch harness Chrome on port 9555 if needed.
|
|
135
164
|
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
136
165
|
echo "[$(date +%H:%M:%S)] Harness Chrome down on port 9555, launching..." >&2
|
|
@@ -45,7 +45,32 @@ fi
|
|
|
45
45
|
|
|
46
46
|
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
# Run accounting for dashboard Job History (Render · Instagram). Matches the
|
|
49
|
+
# pattern in run-instagram-daily.sh / run-twitter-threads.sh / run-reddit-threads.sh:
|
|
50
|
+
# each exit site updates POSTED_CT / SKIPPED_CT / FAILED_CT; the EXIT trap
|
|
51
|
+
# always emits one log_run.py line so the run shows up under render_instagram.
|
|
52
|
+
# "posted" here means "rendered a fresh draft"; "skipped" means buffer was
|
|
53
|
+
# healthy (>=3 drafts) or the lock was already held; "failed" is any real
|
|
54
|
+
# error path.
|
|
55
|
+
RUN_START_EPOCH=$(date +%s)
|
|
56
|
+
POSTED_CT=0
|
|
57
|
+
SKIPPED_CT=0
|
|
58
|
+
FAILED_CT=0
|
|
59
|
+
|
|
60
|
+
cleanup() {
|
|
61
|
+
local rc=$?
|
|
62
|
+
rm -f "$PICK_FILE" "${UNPROVEN_JSON_FILE:-}"
|
|
63
|
+
if [ "$POSTED_CT" -eq 0 ] && [ "$SKIPPED_CT" -eq 0 ] && [ "$FAILED_CT" -eq 0 ]; then
|
|
64
|
+
if [ "$rc" -eq 0 ]; then SKIPPED_CT=1; else FAILED_CT=1; fi
|
|
65
|
+
fi
|
|
66
|
+
local elapsed=$(( $(date +%s) - RUN_START_EPOCH ))
|
|
67
|
+
local cost
|
|
68
|
+
cost=$(/usr/bin/python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START_EPOCH" --scripts "run-instagram-render" 2>/dev/null || echo "0.0000")
|
|
69
|
+
/usr/bin/python3 "$REPO_DIR/scripts/log_run.py" \
|
|
70
|
+
--script "render_instagram" \
|
|
71
|
+
--posted "$POSTED_CT" --skipped "$SKIPPED_CT" --failed "$FAILED_CT" \
|
|
72
|
+
--cost "$cost" --elapsed "$elapsed" >/dev/null 2>&1 || true
|
|
73
|
+
}
|
|
49
74
|
trap cleanup EXIT INT TERM HUP
|
|
50
75
|
|
|
51
76
|
log "=== instagram-render fire: $(date) ==="
|
|
@@ -68,6 +93,7 @@ else
|
|
|
68
93
|
fi
|
|
69
94
|
if [ -z "$TARGET_ACCOUNT" ]; then
|
|
70
95
|
log "ERROR: pick_ig_account.py returned empty; no enabled accounts?"
|
|
96
|
+
FAILED_CT=1
|
|
71
97
|
exit 1
|
|
72
98
|
fi
|
|
73
99
|
export TARGET_ACCOUNT
|
|
@@ -318,6 +344,7 @@ PY
|
|
|
318
344
|
|
|
319
345
|
if [ ! -s "$PICK_FILE" ]; then
|
|
320
346
|
log "ERROR: pick query produced no output"
|
|
347
|
+
FAILED_CT=1
|
|
321
348
|
exit 1
|
|
322
349
|
fi
|
|
323
350
|
|
|
@@ -333,6 +360,7 @@ log "target=$TARGET selected_project=${SELECTED_PROJECT:-<null/organic>} draft_c
|
|
|
333
360
|
# Override with FORCE_RENDER=1 for manual / first-fire runs.
|
|
334
361
|
if [ "${FORCE_RENDER:-0}" != "1" ] && [ "$DRAFT_COUNT" -ge 3 ]; then
|
|
335
362
|
log "skipped: $DRAFT_COUNT drafts of $TARGET already in queue (>= 3 buffer); no render needed"
|
|
363
|
+
SKIPPED_CT=1
|
|
336
364
|
exit 0
|
|
337
365
|
fi
|
|
338
366
|
|
|
@@ -612,9 +640,11 @@ if ! "$REPO_DIR/scripts/run_claude.sh" "run-instagram-render" \
|
|
|
612
640
|
rm -f "$PROMPT_FILE"
|
|
613
641
|
if [ "$rc" -eq 79 ]; then
|
|
614
642
|
log "claude blocked by quota stamp; will retry next cycle"
|
|
643
|
+
SKIPPED_CT=1
|
|
615
644
|
exit 0
|
|
616
645
|
fi
|
|
617
646
|
log "render failed"
|
|
647
|
+
FAILED_CT=1
|
|
618
648
|
exit 1
|
|
619
649
|
fi
|
|
620
650
|
rm -f "$PROMPT_FILE"
|
|
@@ -629,10 +659,12 @@ log " expected: $OUT_CAP"
|
|
|
629
659
|
|
|
630
660
|
if [ ! -f "$OUT_MP4" ]; then
|
|
631
661
|
log "ERROR: $OUT_MP4 missing"
|
|
662
|
+
FAILED_CT=1
|
|
632
663
|
exit 1
|
|
633
664
|
fi
|
|
634
665
|
if [ ! -f "$OUT_CAP" ]; then
|
|
635
666
|
log "ERROR: $OUT_CAP missing"
|
|
667
|
+
FAILED_CT=1
|
|
636
668
|
exit 1
|
|
637
669
|
fi
|
|
638
670
|
|
|
@@ -662,7 +694,7 @@ PY
|
|
|
662
694
|
|
|
663
695
|
case "$ROW_OK" in
|
|
664
696
|
OK*) log "DB row OK: $ROW_OK" ;;
|
|
665
|
-
*) log "ERROR: DB row check failed: $ROW_OK"; exit 1 ;;
|
|
697
|
+
*) log "ERROR: DB row check failed: $ROW_OK"; FAILED_CT=1; exit 1 ;;
|
|
666
698
|
esac
|
|
667
699
|
|
|
668
700
|
VARIANT=$(echo "$ROW_OK" | sed 's/^OK variant=//')
|
|
@@ -754,6 +786,7 @@ PY
|
|
|
754
786
|
if [ "$CAP_LEN" -gt "$CAP_LIMIT" ]; then
|
|
755
787
|
log "ERROR: caption still over limit after 3 tighten attempts (final len=${CAP_LEN})"
|
|
756
788
|
log "flipping media_posts row to status='caption_too_long' so picker skips it"
|
|
789
|
+
FAILED_CT=1
|
|
757
790
|
/opt/homebrew/bin/python3.11 - "$NEXT_NUM" "$CAP_LEN" 2>>"$LOG_FILE" <<'PY'
|
|
758
791
|
import os, sys, psycopg2
|
|
759
792
|
env = {}
|
|
@@ -797,4 +830,5 @@ PY
|
|
|
797
830
|
fi
|
|
798
831
|
|
|
799
832
|
log "=== rendered post-${NNN} (${TARGET}, variant=${VARIANT}) successfully ==="
|
|
833
|
+
POSTED_CT=1
|
|
800
834
|
exit 0
|