@m13v/s4l 1.7.5-rc.2 → 1.7.5-rc.21
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/mcp/dist/index.js +571 -79
- package/mcp/dist/panel.html +2 -3
- package/mcp/dist/redditAuth.js +115 -0
- package/mcp/dist/runtime.js +83 -2
- package/mcp/dist/setup.js +142 -5
- package/mcp/dist/telemetry.js +13 -3
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_card.py +32 -6
- package/mcp/menubar/s4l_menubar.py +42 -111
- package/mcp/menubar/s4l_state.py +54 -187
- package/mcp/package.json +1 -1
- package/mcp/shared/onboarding-ledger.cjs +63 -4
- package/package.json +1 -1
- package/scripts/autopilot_stall_watch.py +3 -6
- package/scripts/browser_mutex.py +316 -0
- package/scripts/claude_job.py +21 -0
- package/scripts/config.py +118 -0
- package/scripts/counterparty_history.py +44 -6
- package/scripts/engage_github.py +7 -3
- package/scripts/engage_reddit.py +9 -3
- package/scripts/engagement_signal_feedback.py +256 -0
- package/scripts/learned_preferences.py +37 -14
- package/scripts/memory_snapshot.py +45 -0
- package/scripts/merge_review_queue.py +67 -3
- package/scripts/post_reddit.py +289 -24
- package/scripts/reddit_browser.py +62 -63
- package/scripts/reddit_browser_fetch.py +32 -0
- package/scripts/release-mcpb.sh +17 -0
- package/scripts/reset-test-machine.sh +13 -7
- package/scripts/scan_twitter_thread_followups.py +7 -1
- package/scripts/schedule_state.py +1 -1
- package/scripts/setup_reddit_auth.py +925 -0
- package/scripts/snapshot.py +96 -4
- package/scripts/stats.py +37 -13
- package/scripts/test_no_silent_fallbacks.py +281 -0
- package/scripts/trigger_promo_lane_test.sh +91 -0
- package/scripts/twitter_browser.py +183 -342
- package/scripts/twitter_scan.py +75 -10
- package/scripts/watchdog_hung_runs.py +15 -9
- package/skill/lib/harness-common.sh +344 -0
- package/skill/lib/linkedin-backend.sh +61 -161
- package/skill/lib/reddit-backend.sh +39 -125
- package/skill/lib/twitter-backend.sh +51 -273
- package/skill/lock.sh +57 -0
- package/skill/run-reddit-search-launchd.sh +4 -1
- package/skill/run-reddit-search.sh +51 -2
- package/skill/run-twitter-cycle.sh +44 -21
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# linkedin-backend.sh - LinkedIn pipeline browser bootstrap (linkedin-harness,
|
|
3
|
-
# mirrors twitter-backend.sh post the 2026-05-19 Twitter harness migration
|
|
3
|
+
# mirrors twitter-backend.sh post the 2026-05-19 Twitter harness migration;
|
|
4
|
+
# the shared machinery lives in skill/lib/harness-common.sh since 2026-07-14).
|
|
4
5
|
#
|
|
5
6
|
# Source this AFTER lock.sh, BEFORE any acquire_lock / browser pre-flight /
|
|
6
7
|
# claude -p subprocess calls. Sets these for the caller:
|
|
@@ -21,8 +22,10 @@
|
|
|
21
22
|
#
|
|
22
23
|
# ensure_linkedin_browser_for_backend
|
|
23
24
|
# Call AFTER acquire_lock "linkedin-browser". Probes harness Chrome on
|
|
24
|
-
# port 9556 and launches it idempotently if down,
|
|
25
|
-
# tabs from prior runs.
|
|
25
|
+
# port 9556 and launches it idempotently if down (wedge-aware, focus-safe;
|
|
26
|
+
# see harness-common.sh), then cleans leftover tabs from prior runs.
|
|
27
|
+
# Also acquires the cross-pipeline whole-run lock (rc=78 skip code) and
|
|
28
|
+
# runs the per-run logout detect-gate.
|
|
26
29
|
#
|
|
27
30
|
# defer_if_foreign_for_backend [log_file]
|
|
28
31
|
# No-op. Harness CDP supports multiple concurrent clients on the same
|
|
@@ -54,12 +57,6 @@ fi
|
|
|
54
57
|
# endpoint. Default 9556 (Mac harness Chrome, separate port from Twitter's 9555).
|
|
55
58
|
export LINKEDIN_CDP_URL="${LINKEDIN_CDP_URL:-http://127.0.0.1:9556}"
|
|
56
59
|
|
|
57
|
-
# Repo root for helper scripts. Honors S4L_REPO_DIR (managed installs run from
|
|
58
|
-
# ~/.social-autoposter-mcp/repo/package, NOT ~/social-autoposter); a $HOME
|
|
59
|
-
# hardcode here silently no-ops on customer boxes (same bug twitter-backend.sh
|
|
60
|
-
# fixed; see its header comment).
|
|
61
|
-
_BH_REPO_DIR="${S4L_REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
62
|
-
|
|
63
60
|
# Resolve a Playwright-capable Python for the browser-path SERP search
|
|
64
61
|
# (discover_linkedin_candidates.py CDP-attaches to the harness Chrome via
|
|
65
62
|
# playwright.sync_api). The agent's bare `python3` resolves to whatever is
|
|
@@ -83,11 +80,15 @@ if [ -z "${LINKEDIN_DISCOVER_PYTHON:-}" ]; then
|
|
|
83
80
|
export LINKEDIN_DISCOVER_PYTHON="${LINKEDIN_DISCOVER_PYTHON:-python3}"
|
|
84
81
|
fi
|
|
85
82
|
|
|
86
|
-
# Default harness URL - used
|
|
87
|
-
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
83
|
+
# Default harness URL - used to decide whether we own this Chrome (and should
|
|
88
84
|
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
89
85
|
_BH_LINKEDIN_DEFAULT_URL="http://127.0.0.1:9556"
|
|
90
86
|
|
|
87
|
+
# Shared engine: _BH_REPO_DIR, hc_ensure_browser, hc_cleanup_tabs,
|
|
88
|
+
# _resolve_chrome_bin, wedge detection, focus-safe launch.
|
|
89
|
+
# shellcheck disable=SC1091
|
|
90
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/harness-common.sh"
|
|
91
|
+
|
|
91
92
|
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
92
93
|
BROWSER BACKEND: linkedin-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
93
94
|
port 9556, profile ~/.claude/browser-profiles/browser-harness-linkedin). The Chrome is
|
|
@@ -152,21 +153,7 @@ BROWSER_HARNESS_EOF
|
|
|
152
153
|
)
|
|
153
154
|
|
|
154
155
|
cleanup_harness_tabs() {
|
|
155
|
-
|
|
156
|
-
# but scoped to the LinkedIn harness Chrome on port 9556.
|
|
157
|
-
#
|
|
158
|
-
# Health-check gate: 10s timeout + ONE retry; log skips so they are not silent.
|
|
159
|
-
local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:9556/json/version"
|
|
160
|
-
if ! $_probe 2>/dev/null; then
|
|
161
|
-
sleep 1
|
|
162
|
-
if ! $_probe 2>/dev/null; then
|
|
163
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (linkedin-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
164
|
-
return 0
|
|
165
|
-
fi
|
|
166
|
-
fi
|
|
167
|
-
# Reuse the same cleanup script as Twitter; it just iterates /json on the
|
|
168
|
-
# default port. Pass the port via env so a single script can serve both.
|
|
169
|
-
BH_CLEANUP_PORT=9556 python3 "$_BH_REPO_DIR/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
156
|
+
hc_cleanup_tabs 9556 linkedin-harness
|
|
170
157
|
}
|
|
171
158
|
|
|
172
159
|
# ===== Cross-pipeline whole-run lock (2026-05-30) =====
|
|
@@ -210,6 +197,24 @@ cleanup_harness_tabs() {
|
|
|
210
197
|
# real pipelines never set this and share the one /tmp path.
|
|
211
198
|
_LI_PIPELINE_LOCK_DIR="${_LI_PIPELINE_LOCK_DIR:-/tmp/s4l-linkedin-pipeline.lock}"
|
|
212
199
|
_acquire_linkedin_pipeline_lock() {
|
|
200
|
+
# 2026-07-14: the normal path delegates to lock.sh's
|
|
201
|
+
# acquire_pipeline_singleton — the GENERALIZED version of this exact
|
|
202
|
+
# function (same /tmp/s4l-linkedin-pipeline.lock dir, same rc=78 contract,
|
|
203
|
+
# same dead-holder reclaim and $$-re-entrancy), so any platform can now
|
|
204
|
+
# request one-driver-per-Chrome semantics. The inline body below survives
|
|
205
|
+
# ONLY for the test-override path (_LI_PIPELINE_LOCK_DIR pointing at a
|
|
206
|
+
# scratch dir) and for a caller that somehow sourced this backend without
|
|
207
|
+
# lock.sh (documented sourcing order says lock.sh comes first).
|
|
208
|
+
if declare -F acquire_pipeline_singleton >/dev/null 2>&1 \
|
|
209
|
+
&& [ "$_LI_PIPELINE_LOCK_DIR" = "/tmp/s4l-linkedin-pipeline.lock" ]; then
|
|
210
|
+
if [ "${_LI_PIPELINE_LOCK_HELD:-0}" = "1" ]; then
|
|
211
|
+
return 0
|
|
212
|
+
fi
|
|
213
|
+
acquire_pipeline_singleton linkedin
|
|
214
|
+
local _li_rc=$?
|
|
215
|
+
[ "$_li_rc" = "0" ] && export _LI_PIPELINE_LOCK_HELD=1
|
|
216
|
+
return $_li_rc
|
|
217
|
+
fi
|
|
213
218
|
# Already held by THIS process (re-entry across phases) -> proceed.
|
|
214
219
|
if [ "${_LI_PIPELINE_LOCK_HELD:-0}" = "1" ]; then
|
|
215
220
|
return 0
|
|
@@ -251,141 +256,21 @@ _acquire_linkedin_pipeline_lock() {
|
|
|
251
256
|
done
|
|
252
257
|
}
|
|
253
258
|
|
|
254
|
-
_resolve_chrome_bin() {
|
|
255
|
-
# Auto-detect Chrome/Chromium so the same script launches the harness on
|
|
256
|
-
# macOS dev boxes AND Linux VMs. Override with BH_CHROME_BIN.
|
|
257
|
-
if [ -n "${BH_CHROME_BIN:-}" ] && [ -x "$BH_CHROME_BIN" ]; then
|
|
258
|
-
echo "$BH_CHROME_BIN"; return 0
|
|
259
|
-
fi
|
|
260
|
-
for _p in \
|
|
261
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
262
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
263
|
-
"/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
|
|
264
|
-
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
|
|
265
|
-
do
|
|
266
|
-
if [ -x "$_p" ]; then echo "$_p"; return 0; fi
|
|
267
|
-
done
|
|
268
|
-
for _n in google-chrome google-chrome-stable chromium chromium-browser; do
|
|
269
|
-
_which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
|
|
270
|
-
done
|
|
271
|
-
echo ""; return 1
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
ensure_linkedin_browser_for_backend() {
|
|
275
|
-
# AppMaker / BYO Chrome: LINKEDIN_CDP_URL points at something other than our
|
|
276
|
-
# default harness URL. Don't touch that browser; just probe it and bail.
|
|
277
|
-
if [ "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" != "$_BH_LINKEDIN_DEFAULT_URL" ]; then
|
|
278
|
-
local _ext_url="${LINKEDIN_CDP_URL}"
|
|
279
|
-
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
280
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
281
|
-
return 0
|
|
282
|
-
fi
|
|
283
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LINKEDIN_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
|
|
284
|
-
return 1
|
|
285
|
-
fi
|
|
286
|
-
# Cross-pipeline whole-run lock: only one LinkedIn browser pipeline drives
|
|
287
|
-
# the 9556 harness Chrome at a time. Acquired here (the single chokepoint
|
|
288
|
-
# every browser pipeline calls). Skipped above for externally-managed
|
|
289
|
-
# (AppMaker/BYO) Chrome, which is not ours to serialize. A held-by-peer
|
|
290
|
-
# lock returns the reserved skip code 78; propagate it so the CALLER's
|
|
291
|
-
# parent shell can exit 0 (an exit here would only kill the subshell most
|
|
292
|
-
# call sites wrap around this function; see the 2026-07-06 incident note
|
|
293
|
-
# above the lock function).
|
|
294
|
-
_acquire_linkedin_pipeline_lock || return $?
|
|
295
|
-
# Probe + launch harness Chrome on port 9556 if needed.
|
|
296
|
-
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
297
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome down on port 9556, launching..." >&2
|
|
298
|
-
# Dated relaunch stamp for central observability (see twitter-backend.sh).
|
|
299
|
-
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) port=9556" \
|
|
300
|
-
>> "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/logs/chrome-relaunch-events.log" 2>/dev/null || true
|
|
301
|
-
local _chrome_bin
|
|
302
|
-
_chrome_bin=$(_resolve_chrome_bin)
|
|
303
|
-
if [ -z "$_chrome_bin" ]; then
|
|
304
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
305
|
-
return 1
|
|
306
|
-
fi
|
|
307
|
-
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
308
|
-
local _extra=()
|
|
309
|
-
case "$(uname -s)" in
|
|
310
|
-
Linux)
|
|
311
|
-
_extra+=(--no-sandbox --disable-dev-shm-usage)
|
|
312
|
-
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
|
313
|
-
_extra+=(--headless=new --disable-gpu)
|
|
314
|
-
fi
|
|
315
|
-
;;
|
|
316
|
-
Darwin)
|
|
317
|
-
# Default position captured 2026-05-26 from the user's
|
|
318
|
-
# secondary monitor; overridable via BH_LINKEDIN_WINDOW_POS.
|
|
319
|
-
_extra+=(--window-position="${BH_LINKEDIN_WINDOW_POS:-3814,-1050}")
|
|
320
|
-
_extra+=(--window-size="${BH_LINKEDIN_WINDOW_SIZE:-1024,1013}")
|
|
321
|
-
;;
|
|
322
|
-
esac
|
|
323
|
-
# Self-heal (2026-06-03): reap any stale Chrome holding THIS profile dir
|
|
324
|
-
# but not answering CDP on our port, else the relaunch hands off via the
|
|
325
|
-
# SingletonLock and loops "failed to start within 12s". Exact-dir match
|
|
326
|
-
# (trailing space) so this never touches the twitter browser-harness
|
|
327
|
-
# profile. See twitter-backend.sh for the regression that motivated this.
|
|
328
|
-
local _prof_dir="$HOME/.claude/browser-profiles/browser-harness-linkedin"
|
|
329
|
-
local _stale_pids
|
|
330
|
-
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
331
|
-
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
332
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
333
|
-
kill $_stale_pids 2>/dev/null || true
|
|
334
|
-
sleep 2
|
|
335
|
-
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
336
|
-
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
337
|
-
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
338
|
-
fi
|
|
339
|
-
# The occlusion/backgrounding flags matter: the window sits offscreen,
|
|
340
|
-
# and without them Chrome stops laying out SPA-rendered content, so
|
|
341
|
-
# every element measures 0x0 and clicks become impossible (2026-07-03).
|
|
342
|
-
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh) — see
|
|
343
|
-
# that file for the clean-exit stamp + no-focus-steal + detach
|
|
344
|
-
# rationale. Do NOT hand-roll launch logic per backend.
|
|
345
|
-
# shellcheck disable=SC1091
|
|
346
|
-
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
347
|
-
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
348
|
-
--remote-debugging-port=9556 \
|
|
349
|
-
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
|
|
350
|
-
--no-first-run --no-default-browser-check \
|
|
351
|
-
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
|
|
352
|
-
--disable-backgrounding-occluded-windows \
|
|
353
|
-
--disable-renderer-backgrounding \
|
|
354
|
-
--disable-background-timer-throttling \
|
|
355
|
-
"${_extra[@]}" \
|
|
356
|
-
about:blank
|
|
357
|
-
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
358
|
-
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null && break
|
|
359
|
-
sleep 1
|
|
360
|
-
done
|
|
361
|
-
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
362
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: LinkedIn harness Chrome failed to start within 12s" >&2
|
|
363
|
-
return 1
|
|
364
|
-
fi
|
|
365
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] LinkedIn harness Chrome up on port 9556" >&2
|
|
366
|
-
fi
|
|
367
|
-
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
368
|
-
# "linkedin-browser" serialization.
|
|
369
|
-
cleanup_harness_tabs
|
|
370
|
-
|
|
371
|
-
# Per-run logout detection (2026-06-03). Every browser pipeline funnels
|
|
372
|
-
# through here before it touches LinkedIn, so this single call makes ANY
|
|
373
|
-
# pipeline trip the killswitch on its natural next fire if the harness
|
|
374
|
-
# Chrome has been logged out (999 / authwall / checkpoint), without editing
|
|
375
|
-
# the chflags-locked top-level scripts. detect-gate is a no-op when the
|
|
376
|
-
# killswitch is already active, and only ENGAGES on a CONCLUSIVE /feed/
|
|
377
|
-
# redirect to auth (infra hiccups -> proceed, so a flaky render never
|
|
378
|
-
# strands the pipeline). On a confirmed logout it engages the flag (which
|
|
379
|
-
# pauses every pipeline on its next fire + starts the 24h recovery clock)
|
|
380
|
-
# and returns 2, so we abort this fire instead of burning a Claude session
|
|
381
|
-
# on a dead session.
|
|
382
|
-
_linkedin_session_detect_gate
|
|
383
|
-
}
|
|
384
|
-
|
|
385
259
|
# Once-per-process guard mirrors _LI_PIPELINE_LOCK_HELD: run-linkedin.sh calls
|
|
386
260
|
# ensure_linkedin_browser_for_backend in both Phase A and Phase B, and we do not
|
|
387
261
|
# want two /feed/ probes per fire.
|
|
388
262
|
_linkedin_session_detect_gate() {
|
|
263
|
+
# Per-run logout detection (2026-06-03). Every browser pipeline funnels
|
|
264
|
+
# through ensure_linkedin_browser_for_backend before it touches LinkedIn,
|
|
265
|
+
# so this single call makes ANY pipeline trip the killswitch on its natural
|
|
266
|
+
# next fire if the harness Chrome has been logged out (999 / authwall /
|
|
267
|
+
# checkpoint), without editing the chflags-locked top-level scripts.
|
|
268
|
+
# detect-gate is a no-op when the killswitch is already active, and only
|
|
269
|
+
# ENGAGES on a CONCLUSIVE /feed/ redirect to auth (infra hiccups ->
|
|
270
|
+
# proceed, so a flaky render never strands the pipeline). On a confirmed
|
|
271
|
+
# logout it engages the flag (which pauses every pipeline on its next fire
|
|
272
|
+
# + starts the 24h recovery clock) and returns 2, so we abort this fire
|
|
273
|
+
# instead of burning a Claude session on a dead session.
|
|
389
274
|
if [ "${_LI_SESSION_PROBED:-0}" = "1" ]; then
|
|
390
275
|
return 0
|
|
391
276
|
fi
|
|
@@ -403,9 +288,24 @@ _linkedin_session_detect_gate() {
|
|
|
403
288
|
return 0
|
|
404
289
|
}
|
|
405
290
|
|
|
291
|
+
ensure_linkedin_browser_for_backend() {
|
|
292
|
+
# --disable-renderer-backgrounding + --disable-background-timer-throttling:
|
|
293
|
+
# LinkedIn's SPA stops laying out in a backgrounded renderer even with the
|
|
294
|
+
# occlusion flags, so elements measure 0x0 (2026-07-03 fix).
|
|
295
|
+
HC_PLATFORM=linkedin \
|
|
296
|
+
HC_PORT=9556 \
|
|
297
|
+
HC_PROFILE_DIR="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
|
|
298
|
+
HC_DEFAULT_URL="$_BH_LINKEDIN_DEFAULT_URL" \
|
|
299
|
+
HC_CDP_URL="${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" \
|
|
300
|
+
HC_LAUNCH_URL="about:blank" \
|
|
301
|
+
HC_WINDOW_POS="${BH_LINKEDIN_WINDOW_POS:-3814,-1050}" \
|
|
302
|
+
HC_WINDOW_SIZE="${BH_LINKEDIN_WINDOW_SIZE:-1024,1013}" \
|
|
303
|
+
HC_EXTRA_FLAGS="--disable-renderer-backgrounding --disable-background-timer-throttling" \
|
|
304
|
+
HC_PRE_LAUNCH_HOOK=_acquire_linkedin_pipeline_lock \
|
|
305
|
+
HC_POST_CLEANUP_HOOK=_linkedin_session_detect_gate \
|
|
306
|
+
hc_ensure_browser
|
|
307
|
+
}
|
|
308
|
+
|
|
406
309
|
defer_if_foreign_for_backend() {
|
|
407
|
-
|
|
408
|
-
# browser-harness-linkedin profile, so a foreign MCP wrapper cannot cause
|
|
409
|
-
# SingletonLock contention. Always return 1 (do not defer).
|
|
410
|
-
return 1
|
|
310
|
+
hc_defer_if_foreign
|
|
411
311
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# reddit-backend.sh - Reddit pipeline browser bootstrap (reddit-harness,
|
|
3
|
-
# mirrors twitter-backend.sh / linkedin-backend.sh
|
|
3
|
+
# mirrors twitter-backend.sh / linkedin-backend.sh; the shared machinery
|
|
4
|
+
# lives in skill/lib/harness-common.sh since 2026-07-14).
|
|
4
5
|
#
|
|
5
6
|
# 2026-05-29 migration: Reddit's discovery path (reddit_tools.py) fetched
|
|
6
7
|
# Reddit's *.json via Python urllib, which Reddit began 403ing from residential
|
|
@@ -31,8 +32,8 @@
|
|
|
31
32
|
#
|
|
32
33
|
# ensure_reddit_browser_for_backend
|
|
33
34
|
# Call AFTER acquire_lock "reddit-browser". Probes harness Chrome on
|
|
34
|
-
# port 9557 and launches it idempotently if down,
|
|
35
|
-
# tabs from prior runs.
|
|
35
|
+
# port 9557 and launches it idempotently if down (wedge-aware, focus-safe;
|
|
36
|
+
# see harness-common.sh), then cleans leftover tabs from prior runs.
|
|
36
37
|
#
|
|
37
38
|
# defer_if_foreign_for_backend [log_file]
|
|
38
39
|
# No-op. Harness CDP supports multiple concurrent clients on the same
|
|
@@ -53,17 +54,15 @@ fi
|
|
|
53
54
|
# Twitter's 9555 and LinkedIn's 9556).
|
|
54
55
|
export REDDIT_CDP_URL="${REDDIT_CDP_URL:-http://127.0.0.1:9557}"
|
|
55
56
|
|
|
56
|
-
#
|
|
57
|
-
# ~/.social-autoposter-mcp/repo/package, NOT ~/social-autoposter); a $HOME
|
|
58
|
-
# hardcode here silently no-ops on customer boxes (same bug twitter-backend.sh
|
|
59
|
-
# fixed; see its header comment).
|
|
60
|
-
_BH_REPO_DIR="${S4L_REPO_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
61
|
-
|
|
62
|
-
# Default harness URL - used by ensure_reddit_browser_for_backend +
|
|
63
|
-
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
57
|
+
# Default harness URL - used to decide whether we own this Chrome (and should
|
|
64
58
|
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
65
59
|
_BH_REDDIT_DEFAULT_URL="http://127.0.0.1:9557"
|
|
66
60
|
|
|
61
|
+
# Shared engine: _BH_REPO_DIR, hc_ensure_browser, hc_cleanup_tabs,
|
|
62
|
+
# _resolve_chrome_bin, wedge detection, focus-safe launch.
|
|
63
|
+
# shellcheck disable=SC1091
|
|
64
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/harness-common.sh"
|
|
65
|
+
|
|
67
66
|
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
68
67
|
BROWSER BACKEND: reddit-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
69
68
|
port 9557, profile ~/.claude/browser-profiles/reddit-harness). The Chrome is already
|
|
@@ -121,127 +120,42 @@ BROWSER_HARNESS_EOF
|
|
|
121
120
|
)
|
|
122
121
|
|
|
123
122
|
cleanup_harness_tabs() {
|
|
124
|
-
|
|
125
|
-
# backend, scoped to the Reddit harness Chrome on port 9557.
|
|
126
|
-
#
|
|
127
|
-
# Health-check gate: 10s timeout + ONE retry; log skips so they are not silent.
|
|
128
|
-
local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:9557/json/version"
|
|
129
|
-
if ! $_probe 2>/dev/null; then
|
|
130
|
-
sleep 1
|
|
131
|
-
if ! $_probe 2>/dev/null; then
|
|
132
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] cleanup_harness_tabs: SKIPPED (reddit-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
133
|
-
return 0
|
|
134
|
-
fi
|
|
135
|
-
fi
|
|
136
|
-
BH_CLEANUP_PORT=9557 python3 "$_BH_REPO_DIR/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
123
|
+
hc_cleanup_tabs 9557 reddit-harness
|
|
137
124
|
}
|
|
138
125
|
|
|
139
|
-
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
143
|
-
|
|
126
|
+
_reddit_defer_if_posting() {
|
|
127
|
+
# Posting-active defer (2026-07-14, mirrors twitter's posting-active flag):
|
|
128
|
+
# a poster mid-drain owns the ONE shared harness tab; a pipeline that
|
|
129
|
+
# grabs it mid-post navigates the tab out from under the poster and the
|
|
130
|
+
# comment-form check false-positives as account_blocked_in_sub. When the
|
|
131
|
+
# flag is fresh (heartbeat < 120s; post_reddit.py re-stamps per row), skip
|
|
132
|
+
# this fire via the reserved code 78 — the launchd job re-fires on its
|
|
133
|
+
# next cadence. A stale flag never blocks (a killed poster must not wedge
|
|
134
|
+
# every reddit pipeline).
|
|
135
|
+
local _f="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}/reddit-posting-active.json"
|
|
136
|
+
[ -f "$_f" ] || return 0
|
|
137
|
+
local _age
|
|
138
|
+
_age=$(( $(date +%s) - $(stat -f %m "$_f" 2>/dev/null || stat -c %Y "$_f" 2>/dev/null || echo 0) ))
|
|
139
|
+
if [ "$_age" -lt 120 ]; then
|
|
140
|
+
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] reddit posting-active (age ${_age}s); skipping this fire (rc=78) so the poster keeps the tab" >&2
|
|
141
|
+
return 78
|
|
144
142
|
fi
|
|
145
|
-
|
|
146
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
147
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
148
|
-
"/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
|
|
149
|
-
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
|
|
150
|
-
do
|
|
151
|
-
if [ -x "$_p" ]; then echo "$_p"; return 0; fi
|
|
152
|
-
done
|
|
153
|
-
for _n in google-chrome google-chrome-stable chromium chromium-browser; do
|
|
154
|
-
_which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
|
|
155
|
-
done
|
|
156
|
-
echo ""; return 1
|
|
143
|
+
return 0
|
|
157
144
|
}
|
|
158
145
|
|
|
159
146
|
ensure_reddit_browser_for_backend() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
fi
|
|
171
|
-
# Probe + launch harness Chrome on port 9557 if needed.
|
|
172
|
-
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
173
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome down on port 9557, launching..." >&2
|
|
174
|
-
local _chrome_bin
|
|
175
|
-
_chrome_bin=$(_resolve_chrome_bin)
|
|
176
|
-
if [ -z "$_chrome_bin" ]; then
|
|
177
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
178
|
-
return 1
|
|
179
|
-
fi
|
|
180
|
-
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
181
|
-
local _extra=()
|
|
182
|
-
case "$(uname -s)" in
|
|
183
|
-
Linux)
|
|
184
|
-
_extra+=(--no-sandbox --disable-dev-shm-usage)
|
|
185
|
-
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
|
186
|
-
_extra+=(--headless=new --disable-gpu)
|
|
187
|
-
fi
|
|
188
|
-
;;
|
|
189
|
-
Darwin)
|
|
190
|
-
# Default position = the Reddit browser's current off-screen
|
|
191
|
-
# spot (captured 2026-05-29); overridable via BH_REDDIT_WINDOW_POS.
|
|
192
|
-
_extra+=(--window-position="${BH_REDDIT_WINDOW_POS:-2131,-1032}")
|
|
193
|
-
_extra+=(--window-size="${BH_REDDIT_WINDOW_SIZE:-911,1016}")
|
|
194
|
-
;;
|
|
195
|
-
esac
|
|
196
|
-
# Self-heal (2026-06-03): reap any stale Chrome holding THIS profile dir
|
|
197
|
-
# but not answering CDP on our port, else the relaunch hands off via the
|
|
198
|
-
# SingletonLock and loops "failed to start within 12s". Exact-dir match
|
|
199
|
-
# (trailing space) keeps this scoped to reddit-harness only. See
|
|
200
|
-
# twitter-backend.sh for the regression that motivated this.
|
|
201
|
-
local _prof_dir="$HOME/.claude/browser-profiles/reddit-harness"
|
|
202
|
-
local _stale_pids
|
|
203
|
-
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
204
|
-
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
205
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
206
|
-
kill $_stale_pids 2>/dev/null || true
|
|
207
|
-
sleep 2
|
|
208
|
-
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
209
|
-
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
210
|
-
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
211
|
-
fi
|
|
212
|
-
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
|
|
213
|
-
# exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
|
|
214
|
-
# steal, out of this job's pgroup), setsid exec fallback elsewhere.
|
|
215
|
-
# This backend previously used a bare exec and had NONE of those
|
|
216
|
-
# protections (2026-07-13 consolidation).
|
|
217
|
-
# shellcheck disable=SC1091
|
|
218
|
-
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
219
|
-
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
220
|
-
--remote-debugging-port=9557 \
|
|
221
|
-
--user-data-dir="$HOME/.claude/browser-profiles/reddit-harness" \
|
|
222
|
-
--no-first-run --no-default-browser-check \
|
|
223
|
-
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
|
|
224
|
-
--disable-backgrounding-occluded-windows \
|
|
225
|
-
"${_extra[@]}" \
|
|
226
|
-
about:blank
|
|
227
|
-
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
228
|
-
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null && break
|
|
229
|
-
sleep 1
|
|
230
|
-
done
|
|
231
|
-
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
232
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] ERROR: Reddit harness Chrome failed to start within 12s" >&2
|
|
233
|
-
return 1
|
|
234
|
-
fi
|
|
235
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Reddit harness Chrome up on port 9557" >&2
|
|
236
|
-
fi
|
|
237
|
-
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
238
|
-
# "reddit-browser" serialization.
|
|
239
|
-
cleanup_harness_tabs
|
|
147
|
+
HC_PLATFORM=reddit \
|
|
148
|
+
HC_PORT=9557 \
|
|
149
|
+
HC_PROFILE_DIR="$HOME/.claude/browser-profiles/reddit-harness" \
|
|
150
|
+
HC_DEFAULT_URL="$_BH_REDDIT_DEFAULT_URL" \
|
|
151
|
+
HC_CDP_URL="${REDDIT_CDP_URL:-$_BH_REDDIT_DEFAULT_URL}" \
|
|
152
|
+
HC_LAUNCH_URL="about:blank" \
|
|
153
|
+
HC_WINDOW_POS="${BH_REDDIT_WINDOW_POS:-2131,-1032}" \
|
|
154
|
+
HC_WINDOW_SIZE="${BH_REDDIT_WINDOW_SIZE:-911,1016}" \
|
|
155
|
+
HC_PRE_LAUNCH_HOOK=_reddit_defer_if_posting \
|
|
156
|
+
hc_ensure_browser
|
|
240
157
|
}
|
|
241
158
|
|
|
242
159
|
defer_if_foreign_for_backend() {
|
|
243
|
-
|
|
244
|
-
# reddit-harness profile, so a foreign MCP wrapper cannot cause
|
|
245
|
-
# SingletonLock contention. Always return 1 (do not defer).
|
|
246
|
-
return 1
|
|
160
|
+
hc_defer_if_foreign
|
|
247
161
|
}
|