@m13v/s4l 1.6.197-rc.7
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/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1314 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +497 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# linkedin-backend.sh - LinkedIn pipeline browser bootstrap (linkedin-harness,
|
|
3
|
+
# mirrors twitter-backend.sh post the 2026-05-19 Twitter harness migration).
|
|
4
|
+
#
|
|
5
|
+
# Source this AFTER lock.sh, BEFORE any acquire_lock / browser pre-flight /
|
|
6
|
+
# claude -p subprocess calls. Sets these for the caller:
|
|
7
|
+
#
|
|
8
|
+
# MCP_CONFIG_FILE - claude -p --mcp-config path (linkedin-harness MCP)
|
|
9
|
+
# BROWSER_INSTRUCTIONS - prompt block describing the harness backend +
|
|
10
|
+
# its bh_run tool surface (inject at the TOP of any
|
|
11
|
+
# prompt that mentions browser_* tools)
|
|
12
|
+
#
|
|
13
|
+
# And exports (so Python subprocesses like linkedin_browser.py inherit them):
|
|
14
|
+
#
|
|
15
|
+
# LINKEDIN_CDP_URL - http://127.0.0.1:9556 (forces direct CDP attach,
|
|
16
|
+
# skipping ps-based agent-profile discovery)
|
|
17
|
+
#
|
|
18
|
+
# Provides these functions (names mirror twitter-backend for back-compat with
|
|
19
|
+
# the existing call shape used in run-linkedin.sh, stats-linkedin.sh,
|
|
20
|
+
# scan-linkedin-mentions.sh, dm-outreach-linkedin.sh, etc.):
|
|
21
|
+
#
|
|
22
|
+
# ensure_linkedin_browser_for_backend
|
|
23
|
+
# Call AFTER acquire_lock "linkedin-browser". Probes harness Chrome on
|
|
24
|
+
# port 9556 and launches it idempotently if down, then cleans leftover
|
|
25
|
+
# tabs from prior runs.
|
|
26
|
+
#
|
|
27
|
+
# defer_if_foreign_for_backend [log_file]
|
|
28
|
+
# No-op. Harness CDP supports multiple concurrent clients on the same
|
|
29
|
+
# Chrome (no SingletonLock fight), so foreign MCP wrappers never block
|
|
30
|
+
# us. Kept as a function only so callers don't have to change.
|
|
31
|
+
#
|
|
32
|
+
# IMPORTANT — LinkedIn anti-bot considerations (per CLAUDE.md):
|
|
33
|
+
# The 2026-04-17 ban was caused by Voyager API calls + permalink scrape loops
|
|
34
|
+
# (behavioral fingerprinting), NOT by the CDP-attach mechanism itself. The
|
|
35
|
+
# existing discover_linkedin_candidates.py and scrape_linkedin_comment_stats.py
|
|
36
|
+
# already CDP-attach without triggering bans, so the harness substrate is safe.
|
|
37
|
+
# What MUST stay forbidden inside any bh_run script targeting LinkedIn:
|
|
38
|
+
# - /voyager/api/* calls (Python, fetch(), page.evaluate())
|
|
39
|
+
# - Loops that open each post permalink to scrape reactions/comments
|
|
40
|
+
# - scrollBy combined with "Show more comments" / "Load earlier replies" clicks
|
|
41
|
+
# - Programmatic login flows (passive checks only; on checkpoint return early)
|
|
42
|
+
|
|
43
|
+
MCP_CONFIG_FILE="$HOME/.claude/browser-agent-configs/linkedin-harness-mcp.json"
|
|
44
|
+
|
|
45
|
+
# Per-host env override (written by bin/cli.js when installing on an AppMaker
|
|
46
|
+
# VM). On a Mac dev box this file does not exist, so the default below kicks in.
|
|
47
|
+
if [ -f "$HOME/.social-autoposter-env" ]; then
|
|
48
|
+
# shellcheck disable=SC1091
|
|
49
|
+
. "$HOME/.social-autoposter-env"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Tell linkedin_browser.py (and any other Python helper that honors this env
|
|
53
|
+
# var) to skip ps-based discovery and connect directly to the configured CDP
|
|
54
|
+
# endpoint. Default 9556 (Mac harness Chrome, separate port from Twitter's 9555).
|
|
55
|
+
export LINKEDIN_CDP_URL="${LINKEDIN_CDP_URL:-http://127.0.0.1:9556}"
|
|
56
|
+
|
|
57
|
+
# Resolve a Playwright-capable Python for the browser-path SERP search
|
|
58
|
+
# (discover_linkedin_candidates.py CDP-attaches to the harness Chrome via
|
|
59
|
+
# playwright.sync_api). The agent's bare `python3` resolves to whatever is
|
|
60
|
+
# first on PATH, which on this Mac is /opt/homebrew/bin/python3 (3.14) where
|
|
61
|
+
# Playwright is NOT installed -> ModuleNotFoundError. Playwright lives under
|
|
62
|
+
# /opt/homebrew/bin/python3.11 and /usr/bin/python3 (3.9). Pick the first
|
|
63
|
+
# interpreter that can actually import playwright.sync_api and export it so the
|
|
64
|
+
# Phase A browser prompt can shell out via "$LINKEDIN_DISCOVER_PYTHON" instead
|
|
65
|
+
# of the ambiguous bare "python3". Only the browser backend needs this; the
|
|
66
|
+
# unipile path uses the REST API and never imports Playwright.
|
|
67
|
+
if [ -z "${LINKEDIN_DISCOVER_PYTHON:-}" ]; then
|
|
68
|
+
for _li_py in /opt/homebrew/bin/python3.11 /usr/bin/python3 /opt/homebrew/bin/python3 python3; do
|
|
69
|
+
if command -v "$_li_py" >/dev/null 2>&1 && \
|
|
70
|
+
"$_li_py" -c 'from playwright.sync_api import sync_playwright' >/dev/null 2>&1; then
|
|
71
|
+
export LINKEDIN_DISCOVER_PYTHON="$_li_py"
|
|
72
|
+
break
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
# Fallback: if none resolved, keep bare python3 so the failure is loud and
|
|
76
|
+
# obvious in the run log rather than silently substituting a wrong path.
|
|
77
|
+
export LINKEDIN_DISCOVER_PYTHON="${LINKEDIN_DISCOVER_PYTHON:-python3}"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Default harness URL - used by ensure_linkedin_browser_for_backend +
|
|
81
|
+
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
82
|
+
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
83
|
+
_BH_LINKEDIN_DEFAULT_URL="http://127.0.0.1:9556"
|
|
84
|
+
|
|
85
|
+
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
86
|
+
BROWSER BACKEND: linkedin-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
87
|
+
port 9556, profile ~/.claude/browser-profiles/browser-harness-linkedin). The Chrome is
|
|
88
|
+
already logged in as Matthew Diakonov (i@m13v.com); cookies persist on disk.
|
|
89
|
+
|
|
90
|
+
You have ONE tool: mcp__linkedin-harness__bh_run(script). It runs arbitrary Python with
|
|
91
|
+
these helpers pre-imported:
|
|
92
|
+
new_tab(url), goto_url(url), wait_for_load(), page_info(),
|
|
93
|
+
capture_screenshot(), # returns path to PNG; Read it to see the page
|
|
94
|
+
click_at_xy(x, y), # coordinate click (viewport pixels)
|
|
95
|
+
js(expression), # page.evaluate-style; returns the result
|
|
96
|
+
type_text(text), # types into currently-focused element
|
|
97
|
+
press_key(key), # e.g. "Enter", "Tab", "Escape"
|
|
98
|
+
scroll(direction, amount), cdp(method, **params)
|
|
99
|
+
|
|
100
|
+
TAB HYGIENE (IMPORTANT): A placeholder tab ALWAYS already exists when you start
|
|
101
|
+
(pre-flight leaves exactly one tab open). REUSE IT: use goto_url() for your VERY FIRST
|
|
102
|
+
navigation as well as every subsequent one, so the existing tab is navigated in place.
|
|
103
|
+
Call new_tab() ONLY as a fallback when no usable tab exists (goto_url errors because
|
|
104
|
+
there is no active page) OR when you genuinely need a second tab open in parallel.
|
|
105
|
+
Opening a fresh tab on first navigation orphans the placeholder and leaks a tab every
|
|
106
|
+
cycle, which exhausts per-process Chrome resources.
|
|
107
|
+
|
|
108
|
+
LINKEDIN SAFETY (HARD RULES):
|
|
109
|
+
- NEVER call /voyager/api/* endpoints (Python, fetch(), js()). That is the internal
|
|
110
|
+
web-client backend and tripped the 2026-04-17 restriction.
|
|
111
|
+
- NEVER loop opening individual post permalinks to scrape reactions/comments.
|
|
112
|
+
- NEVER combine scrollBy() with clicks on "Show more comments" or "Load earlier replies".
|
|
113
|
+
- If a checkpoint / login / verify-you-are-human page appears, return SESSION_INVALID
|
|
114
|
+
immediately and stop. Do not attempt programmatic login.
|
|
115
|
+
|
|
116
|
+
TRANSLATION TABLE - wherever this prompt mentions a Playwright-style tool, do the
|
|
117
|
+
following with bh_run instead:
|
|
118
|
+
|
|
119
|
+
browser_navigate(url) -> Reuse the existing tab (default, incl. first nav):
|
|
120
|
+
bh_run('goto_url("URL"); wait_for_load()')
|
|
121
|
+
Fallback only if no tab exists / parallel tab needed:
|
|
122
|
+
bh_run('new_tab("URL"); wait_for_load()')
|
|
123
|
+
browser_snapshot -> bh_run('print(js("""..."""))') to read DOM as structured data,
|
|
124
|
+
OR bh_run('print(capture_screenshot())') + Read the PNG
|
|
125
|
+
browser_run_code(js) -> bh_run('print(js("""<the JS expression>"""))')
|
|
126
|
+
browser_click(ref=...) -> Find the element via selector, compute center coords from
|
|
127
|
+
getBoundingClientRect, then bh_run('click_at_xy(X, Y)')
|
|
128
|
+
browser_type(ref=..., text=...) -> Click the textbox first (click_at_xy), then bh_run('type_text("TEXT")')
|
|
129
|
+
browser_take_screenshot -> bh_run('print(capture_screenshot())') then Read the path
|
|
130
|
+
browser_press_key("Enter") -> bh_run('press_key("Enter")')
|
|
131
|
+
|
|
132
|
+
EXAMPLE - read recent activity comment count:
|
|
133
|
+
bh_run('''
|
|
134
|
+
goto_url("https://www.linkedin.com/in/me/recent-activity/comments/")
|
|
135
|
+
wait_for_load()
|
|
136
|
+
count = js("""
|
|
137
|
+
return document.querySelectorAll('[data-id^="urn:li:comment:"]').length;
|
|
138
|
+
""")
|
|
139
|
+
print(count)
|
|
140
|
+
''')
|
|
141
|
+
|
|
142
|
+
VERIFY AFTER EVERY MUTATION by capturing a screenshot and reading the PNG, coordinate
|
|
143
|
+
clicks can miss; visual verification is the only reliable confirmation that the action took.
|
|
144
|
+
BROWSER_HARNESS_EOF
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
cleanup_harness_tabs() {
|
|
148
|
+
# Close every CDP "page" tab except one. Same pattern as twitter-backend,
|
|
149
|
+
# but scoped to the LinkedIn harness Chrome on port 9556.
|
|
150
|
+
#
|
|
151
|
+
# Health-check gate: 10s timeout + ONE retry; log skips so they are not silent.
|
|
152
|
+
local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:9556/json/version"
|
|
153
|
+
if ! $_probe 2>/dev/null; then
|
|
154
|
+
sleep 1
|
|
155
|
+
if ! $_probe 2>/dev/null; then
|
|
156
|
+
echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (linkedin-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
fi
|
|
160
|
+
# Reuse the same cleanup script as Twitter; it just iterates /json on the
|
|
161
|
+
# default port. Pass the port via env so a single script can serve both.
|
|
162
|
+
BH_CLEANUP_PORT=9556 python3 "$HOME/social-autoposter/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ===== Cross-pipeline whole-run lock (2026-05-30) =====
|
|
166
|
+
# Only ONE LinkedIn browser pipeline may drive the single linkedin-harness
|
|
167
|
+
# Chrome (port 9556) at a time: run-linkedin, engage-linkedin,
|
|
168
|
+
# dm-outreach-linkedin, audit-linkedin, engage-dm-replies-linkedin,
|
|
169
|
+
# stats-linkedin. Without this, two launchd-fired pipelines interleave (each
|
|
170
|
+
# releases the per-phase `linkedin-browser` FIFO lock between phases), so e.g.
|
|
171
|
+
# run-linkedin Phase B posts a comment while engage drives a SERP, yanking the
|
|
172
|
+
# same window back and forth and leaking tabs between reactive sweeps.
|
|
173
|
+
#
|
|
174
|
+
# Every browser pipeline funnels through ensure_linkedin_browser_for_backend
|
|
175
|
+
# before it touches Chrome, so acquiring here covers ALL of them without
|
|
176
|
+
# editing the (chflags-locked) top-level scripts. Semantics mirror
|
|
177
|
+
# run-linkedin.sh's existing singleton guard:
|
|
178
|
+
# - try once (mkdir), reclaim if the holder PID is dead
|
|
179
|
+
# - if a DIFFERENT live pipeline holds it -> exit 0 (skip this fire; the
|
|
180
|
+
# launchd job retries on its next cadence). No indefinite wait, so the
|
|
181
|
+
# ordering vs the per-phase FIFO `linkedin-browser` lock can't deadlock.
|
|
182
|
+
# - idempotent within a process via _LI_PIPELINE_LOCK_HELD so the SECOND
|
|
183
|
+
# phase-call (e.g. run-linkedin Phase B) does not block on a lock this
|
|
184
|
+
# same process already owns.
|
|
185
|
+
# No release trap on purpose: a finished pipeline's lock dir is reclaimed by
|
|
186
|
+
# the next pipeline's dead-PID check, exactly like the singleton guard. This
|
|
187
|
+
# avoids clobbering the parent scripts' EXIT/INT/TERM/HUP run_monitor traps.
|
|
188
|
+
_LI_PIPELINE_LOCK_DIR="/tmp/saps-linkedin-pipeline.lock"
|
|
189
|
+
_acquire_linkedin_pipeline_lock() {
|
|
190
|
+
# Already held by THIS process (re-entry across phases) -> proceed.
|
|
191
|
+
if [ "${_LI_PIPELINE_LOCK_HELD:-0}" = "1" ]; then
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
local _who="${S4L_PIPELINE_NAME:-$(basename "${0:-linkedin-pipeline}")}"
|
|
195
|
+
# BAIL, don't wait. Reverted 2026-06-05 to the original behavior: if another
|
|
196
|
+
# LinkedIn pipeline already drives the 9556 Chrome, this fire exits 0 and
|
|
197
|
+
# launchd re-fires on its next cadence. The 2026-06-04/05 "wait + drop/retake
|
|
198
|
+
# browser lock" experiment starved the 15-min run-linkedin comment poster by
|
|
199
|
+
# making it queue (and risk a lock-ordering deadlock) behind stats/dm jobs.
|
|
200
|
+
# No indefinite wait, so the per-phase FIFO linkedin-browser lock can't
|
|
201
|
+
# deadlock against this coarse one-driver-per-Chrome lock.
|
|
202
|
+
while : ; do
|
|
203
|
+
if mkdir "$_LI_PIPELINE_LOCK_DIR" 2>/dev/null; then
|
|
204
|
+
echo "$$" > "$_LI_PIPELINE_LOCK_DIR/pid"
|
|
205
|
+
echo "$_who" > "$_LI_PIPELINE_LOCK_DIR/holder"
|
|
206
|
+
export _LI_PIPELINE_LOCK_HELD=1
|
|
207
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock ACQUIRED by $_who (pid $$)" >&2
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
local _h_pid _h_who
|
|
211
|
+
_h_pid="$(cat "$_LI_PIPELINE_LOCK_DIR/pid" 2>/dev/null || echo "")"
|
|
212
|
+
_h_who="$(cat "$_LI_PIPELINE_LOCK_DIR/holder" 2>/dev/null || echo "?")"
|
|
213
|
+
if [ -z "$_h_pid" ] || ! kill -0 "$_h_pid" 2>/dev/null; then
|
|
214
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: reclaiming stale lock (dead holder ${_h_who} pid ${_h_pid:-unknown})" >&2
|
|
215
|
+
rm -rf "$_LI_PIPELINE_LOCK_DIR"
|
|
216
|
+
continue
|
|
217
|
+
fi
|
|
218
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: held by ${_h_who} (pid ${_h_pid}); ${_who} exiting this fire to avoid two drivers on the 9556 Chrome" >&2
|
|
219
|
+
exit 0
|
|
220
|
+
done
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_resolve_chrome_bin() {
|
|
224
|
+
# Auto-detect Chrome/Chromium so the same script launches the harness on
|
|
225
|
+
# macOS dev boxes AND Linux VMs. Override with BH_CHROME_BIN.
|
|
226
|
+
if [ -n "${BH_CHROME_BIN:-}" ] && [ -x "$BH_CHROME_BIN" ]; then
|
|
227
|
+
echo "$BH_CHROME_BIN"; return 0
|
|
228
|
+
fi
|
|
229
|
+
for _p in \
|
|
230
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
231
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
232
|
+
"/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
|
|
233
|
+
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
|
|
234
|
+
do
|
|
235
|
+
if [ -x "$_p" ]; then echo "$_p"; return 0; fi
|
|
236
|
+
done
|
|
237
|
+
for _n in google-chrome google-chrome-stable chromium chromium-browser; do
|
|
238
|
+
_which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
|
|
239
|
+
done
|
|
240
|
+
echo ""; return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
ensure_linkedin_browser_for_backend() {
|
|
244
|
+
# AppMaker / BYO Chrome: LINKEDIN_CDP_URL points at something other than our
|
|
245
|
+
# default harness URL. Don't touch that browser; just probe it and bail.
|
|
246
|
+
if [ "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" != "$_BH_LINKEDIN_DEFAULT_URL" ]; then
|
|
247
|
+
local _ext_url="${LINKEDIN_CDP_URL}"
|
|
248
|
+
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
249
|
+
echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
250
|
+
return 0
|
|
251
|
+
fi
|
|
252
|
+
echo "[$(date +%H:%M:%S)] ERROR: LINKEDIN_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
|
|
253
|
+
return 1
|
|
254
|
+
fi
|
|
255
|
+
# Cross-pipeline whole-run lock: only one LinkedIn browser pipeline drives
|
|
256
|
+
# the 9556 harness Chrome at a time. Acquired here (the single chokepoint
|
|
257
|
+
# every browser pipeline calls) so it covers run/engage/dm/audit/stats
|
|
258
|
+
# without editing the locked top-level scripts. Skipped above for
|
|
259
|
+
# externally-managed (AppMaker/BYO) Chrome, which is not ours to serialize.
|
|
260
|
+
_acquire_linkedin_pipeline_lock
|
|
261
|
+
# Probe + launch harness Chrome on port 9556 if needed.
|
|
262
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
263
|
+
echo "[$(date +%H:%M:%S)] LinkedIn harness Chrome down on port 9556, launching..." >&2
|
|
264
|
+
local _chrome_bin
|
|
265
|
+
_chrome_bin=$(_resolve_chrome_bin)
|
|
266
|
+
if [ -z "$_chrome_bin" ]; then
|
|
267
|
+
echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
268
|
+
return 1
|
|
269
|
+
fi
|
|
270
|
+
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
271
|
+
local _extra=()
|
|
272
|
+
case "$(uname -s)" in
|
|
273
|
+
Linux)
|
|
274
|
+
_extra+=(--no-sandbox --disable-dev-shm-usage)
|
|
275
|
+
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
|
276
|
+
_extra+=(--headless=new --disable-gpu)
|
|
277
|
+
fi
|
|
278
|
+
;;
|
|
279
|
+
Darwin)
|
|
280
|
+
# Default position captured 2026-05-26 from the user's
|
|
281
|
+
# secondary monitor; overridable via BH_LINKEDIN_WINDOW_POS.
|
|
282
|
+
_extra+=(--window-position="${BH_LINKEDIN_WINDOW_POS:-3814,-1050}")
|
|
283
|
+
_extra+=(--window-size="${BH_LINKEDIN_WINDOW_SIZE:-1024,1013}")
|
|
284
|
+
;;
|
|
285
|
+
esac
|
|
286
|
+
# Self-heal (2026-06-03): reap any stale Chrome holding THIS profile dir
|
|
287
|
+
# but not answering CDP on our port, else the relaunch hands off via the
|
|
288
|
+
# SingletonLock and loops "failed to start within 12s". Exact-dir match
|
|
289
|
+
# (trailing space) so this never touches the twitter browser-harness
|
|
290
|
+
# profile. See twitter-backend.sh for the regression that motivated this.
|
|
291
|
+
local _prof_dir="$HOME/.claude/browser-profiles/browser-harness-linkedin"
|
|
292
|
+
local _stale_pids
|
|
293
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
294
|
+
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
295
|
+
echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
296
|
+
kill $_stale_pids 2>/dev/null || true
|
|
297
|
+
sleep 2
|
|
298
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
299
|
+
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
300
|
+
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
301
|
+
fi
|
|
302
|
+
"$_chrome_bin" \
|
|
303
|
+
--remote-debugging-port=9556 \
|
|
304
|
+
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
|
|
305
|
+
--no-first-run --no-default-browser-check \
|
|
306
|
+
--disable-features=ChromeWhatsNewUI \
|
|
307
|
+
"${_extra[@]}" \
|
|
308
|
+
about:blank >/dev/null 2>&1 &
|
|
309
|
+
disown
|
|
310
|
+
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
311
|
+
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null && break
|
|
312
|
+
sleep 1
|
|
313
|
+
done
|
|
314
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null; then
|
|
315
|
+
echo "[$(date +%H:%M:%S)] ERROR: LinkedIn harness Chrome failed to start within 12s" >&2
|
|
316
|
+
return 1
|
|
317
|
+
fi
|
|
318
|
+
echo "[$(date +%H:%M:%S)] LinkedIn harness Chrome up on port 9556" >&2
|
|
319
|
+
fi
|
|
320
|
+
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
321
|
+
# "linkedin-browser" serialization.
|
|
322
|
+
cleanup_harness_tabs
|
|
323
|
+
|
|
324
|
+
# Per-run logout detection (2026-06-03). Every browser pipeline funnels
|
|
325
|
+
# through here before it touches LinkedIn, so this single call makes ANY
|
|
326
|
+
# pipeline trip the killswitch on its natural next fire if the harness
|
|
327
|
+
# Chrome has been logged out (999 / authwall / checkpoint), without editing
|
|
328
|
+
# the chflags-locked top-level scripts. detect-gate is a no-op when the
|
|
329
|
+
# killswitch is already active, and only ENGAGES on a CONCLUSIVE /feed/
|
|
330
|
+
# redirect to auth (infra hiccups -> proceed, so a flaky render never
|
|
331
|
+
# strands the pipeline). On a confirmed logout it engages the flag (which
|
|
332
|
+
# pauses every pipeline on its next fire + starts the 24h recovery clock)
|
|
333
|
+
# and returns 2, so we abort this fire instead of burning a Claude session
|
|
334
|
+
# on a dead session.
|
|
335
|
+
_linkedin_session_detect_gate
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Once-per-process guard mirrors _LI_PIPELINE_LOCK_HELD: run-linkedin.sh calls
|
|
339
|
+
# ensure_linkedin_browser_for_backend in both Phase A and Phase B, and we do not
|
|
340
|
+
# want two /feed/ probes per fire.
|
|
341
|
+
_linkedin_session_detect_gate() {
|
|
342
|
+
if [ "${_LI_SESSION_PROBED:-0}" = "1" ]; then
|
|
343
|
+
return 0
|
|
344
|
+
fi
|
|
345
|
+
export _LI_SESSION_PROBED=1
|
|
346
|
+
local _py="${LINKEDIN_DISCOVER_PYTHON:-python3}"
|
|
347
|
+
# `|| _rc=$?` so a nonzero exit (e.g. 2 = logged out) is "handled" and does
|
|
348
|
+
# not trip a caller's `set -e` before we inspect the code ourselves.
|
|
349
|
+
local _rc=0
|
|
350
|
+
"$_py" "$HOME/social-autoposter/scripts/linkedin_killswitch.py" detect-gate \
|
|
351
|
+
--cdp-url "${LINKEDIN_CDP_URL:-$_BH_LINKEDIN_DEFAULT_URL}" >&2 || _rc=$?
|
|
352
|
+
if [ "$_rc" = "2" ]; then
|
|
353
|
+
echo "[$(date +%H:%M:%S)] detect-gate tripped the LinkedIn killswitch; aborting this fire" >&2
|
|
354
|
+
return 1
|
|
355
|
+
fi
|
|
356
|
+
return 0
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
defer_if_foreign_for_backend() {
|
|
360
|
+
# Harness Chrome accepts multiple concurrent CDP clients on the same
|
|
361
|
+
# browser-harness-linkedin profile, so a foreign MCP wrapper cannot cause
|
|
362
|
+
# SingletonLock contention. Always return 1 (do not defer).
|
|
363
|
+
return 1
|
|
364
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Portable platform detection for social-autoposter shell scripts.
|
|
3
|
+
# Source this file, then use: $PLATFORM, stat_mtime <path>, platform_notify <title> <msg>.
|
|
4
|
+
|
|
5
|
+
if [ -z "${PLATFORM:-}" ]; then
|
|
6
|
+
case "$(uname -s)" in
|
|
7
|
+
Darwin) PLATFORM=darwin ;;
|
|
8
|
+
Linux) PLATFORM=linux ;;
|
|
9
|
+
*) PLATFORM=unknown ;;
|
|
10
|
+
esac
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
stat_mtime() {
|
|
14
|
+
local f="$1"
|
|
15
|
+
case "$PLATFORM" in
|
|
16
|
+
darwin) stat -f %m "$f" 2>/dev/null || echo 0 ;;
|
|
17
|
+
linux) stat -c %Y "$f" 2>/dev/null || echo 0 ;;
|
|
18
|
+
*) echo 0 ;;
|
|
19
|
+
esac
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
platform_notify() {
|
|
23
|
+
local title="$1"
|
|
24
|
+
local msg="$2"
|
|
25
|
+
case "$PLATFORM" in
|
|
26
|
+
darwin)
|
|
27
|
+
osascript -e "display notification \"$msg\" with title \"$title\" sound name \"Glass\"" 2>/dev/null || true
|
|
28
|
+
;;
|
|
29
|
+
linux)
|
|
30
|
+
if command -v notify-send >/dev/null 2>&1; then
|
|
31
|
+
notify-send "$title" "$msg" 2>/dev/null || true
|
|
32
|
+
fi
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Portable `gtimeout`: on macOS with Homebrew coreutils the binary is named
|
|
38
|
+
# gtimeout; on Linux GNU coreutils ships it as `timeout`. Define a function
|
|
39
|
+
# so every script can call `gtimeout <secs> <cmd>` regardless of platform.
|
|
40
|
+
if ! command -v gtimeout >/dev/null 2>&1; then
|
|
41
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
42
|
+
gtimeout() { timeout "$@"; }
|
|
43
|
+
else
|
|
44
|
+
gtimeout() { "$@"; } # last-resort no-op wrapper
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
export PLATFORM
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# reddit-backend.sh - Reddit pipeline browser bootstrap (reddit-harness,
|
|
3
|
+
# mirrors twitter-backend.sh / linkedin-backend.sh).
|
|
4
|
+
#
|
|
5
|
+
# 2026-05-29 migration: Reddit's discovery path (reddit_tools.py) fetched
|
|
6
|
+
# Reddit's *.json via Python urllib, which Reddit began 403ing from residential
|
|
7
|
+
# IPs on 2026-05-28 (TLS-fingerprint + no-cookies block). Fetching the same
|
|
8
|
+
# JSON from inside a logged-in real-Chrome page returns 200. So the entire
|
|
9
|
+
# Reddit pipeline (discovery + posting) now rides a dedicated browser-harness
|
|
10
|
+
# Chrome on port 9557, profile ~/.claude/browser-profiles/reddit-harness
|
|
11
|
+
# (seeded from the existing logged-in ~/.claude/browser-profiles/reddit).
|
|
12
|
+
#
|
|
13
|
+
# Source this AFTER lock.sh, BEFORE any acquire_lock / browser pre-flight /
|
|
14
|
+
# claude -p subprocess calls. Sets these for the caller:
|
|
15
|
+
#
|
|
16
|
+
# MCP_CONFIG_FILE - claude -p --mcp-config path (reddit-harness MCP)
|
|
17
|
+
# BROWSER_INSTRUCTIONS - prompt block describing the harness backend +
|
|
18
|
+
# its bh_run tool surface (inject at the TOP of any
|
|
19
|
+
# prompt that mentions browser_* tools)
|
|
20
|
+
#
|
|
21
|
+
# And exports (so Python subprocesses like reddit_browser.py / reddit_tools.py
|
|
22
|
+
# inherit them):
|
|
23
|
+
#
|
|
24
|
+
# REDDIT_CDP_URL - http://127.0.0.1:9557 (forces direct CDP attach,
|
|
25
|
+
# skipping ps-based agent-profile discovery; also
|
|
26
|
+
# tells reddit_tools.py to fetch JSON via the browser)
|
|
27
|
+
#
|
|
28
|
+
# Provides these functions (names mirror twitter/linkedin-backend for the
|
|
29
|
+
# existing call shape in run-reddit-search.sh, run-reddit-threads.sh,
|
|
30
|
+
# engage-reddit.sh, dm-outreach-reddit.sh, link-edit-reddit.sh, etc.):
|
|
31
|
+
#
|
|
32
|
+
# ensure_reddit_browser_for_backend
|
|
33
|
+
# Call AFTER acquire_lock "reddit-browser". Probes harness Chrome on
|
|
34
|
+
# port 9557 and launches it idempotently if down, then cleans leftover
|
|
35
|
+
# tabs from prior runs.
|
|
36
|
+
#
|
|
37
|
+
# defer_if_foreign_for_backend [log_file]
|
|
38
|
+
# No-op. Harness CDP supports multiple concurrent clients on the same
|
|
39
|
+
# Chrome (no SingletonLock fight), so foreign MCP wrappers never block us.
|
|
40
|
+
|
|
41
|
+
MCP_CONFIG_FILE="$HOME/.claude/browser-agent-configs/reddit-harness-mcp.json"
|
|
42
|
+
|
|
43
|
+
# Per-host env override (written by bin/cli.js when installing on an AppMaker
|
|
44
|
+
# VM). On a Mac dev box this file does not exist, so the default below kicks in.
|
|
45
|
+
if [ -f "$HOME/.social-autoposter-env" ]; then
|
|
46
|
+
# shellcheck disable=SC1091
|
|
47
|
+
. "$HOME/.social-autoposter-env"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Tell reddit_browser.py + reddit_tools.py (and any other Python helper that
|
|
51
|
+
# honors this env var) to skip ps-based discovery and connect directly to the
|
|
52
|
+
# configured CDP endpoint. Default 9557 (Mac harness Chrome, separate port from
|
|
53
|
+
# Twitter's 9555 and LinkedIn's 9556).
|
|
54
|
+
export REDDIT_CDP_URL="${REDDIT_CDP_URL:-http://127.0.0.1:9557}"
|
|
55
|
+
|
|
56
|
+
# Default harness URL - used by ensure_reddit_browser_for_backend +
|
|
57
|
+
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
58
|
+
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
59
|
+
_BH_REDDIT_DEFAULT_URL="http://127.0.0.1:9557"
|
|
60
|
+
|
|
61
|
+
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
62
|
+
BROWSER BACKEND: reddit-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
63
|
+
port 9557, profile ~/.claude/browser-profiles/reddit-harness). The Chrome is already
|
|
64
|
+
logged in to Reddit; cookies persist on disk.
|
|
65
|
+
|
|
66
|
+
You have ONE tool: mcp__reddit-harness__bh_run(script). It runs arbitrary Python with
|
|
67
|
+
these helpers pre-imported:
|
|
68
|
+
new_tab(url), goto_url(url), wait_for_load(), page_info(),
|
|
69
|
+
capture_screenshot(), # returns path to PNG; Read it to see the page
|
|
70
|
+
click_at_xy(x, y), # coordinate click (viewport pixels)
|
|
71
|
+
js(expression), # page.evaluate-style; returns the result
|
|
72
|
+
type_text(text), # types into currently-focused element
|
|
73
|
+
press_key(key), # e.g. "Enter", "Tab", "Escape"
|
|
74
|
+
scroll(direction, amount), cdp(method, **params)
|
|
75
|
+
|
|
76
|
+
TAB HYGIENE (IMPORTANT): Reuse the SAME tab for sequential same-domain navigation.
|
|
77
|
+
Use new_tab() ONLY for the very first navigation OR when you need to keep an old tab
|
|
78
|
+
open in parallel. For each subsequent query / page / scan, use goto_url() so the
|
|
79
|
+
existing tab is reused. Opening a fresh tab for every query leaks tabs over time and
|
|
80
|
+
exhausts per-process Chrome resources.
|
|
81
|
+
|
|
82
|
+
REDDIT JSON FETCH (the whole point of this backend): Reddit 403s urllib/curl on
|
|
83
|
+
*.json from this IP, but same-origin fetch() from inside a logged-in reddit.com page
|
|
84
|
+
returns 200. To read any Reddit JSON endpoint:
|
|
85
|
+
bh_run('''
|
|
86
|
+
goto_url("https://www.reddit.com/")
|
|
87
|
+
wait_for_load()
|
|
88
|
+
body = js("""
|
|
89
|
+
return (async () => {
|
|
90
|
+
const r = await fetch("https://www.reddit.com/search.json?q=...&limit=25",
|
|
91
|
+
{credentials:"include", headers:{"Accept":"application/json"}});
|
|
92
|
+
return JSON.stringify({status:r.status, body: await r.text()});
|
|
93
|
+
})();
|
|
94
|
+
""")
|
|
95
|
+
print(body)
|
|
96
|
+
''')
|
|
97
|
+
|
|
98
|
+
TRANSLATION TABLE - wherever this prompt mentions a Playwright-style tool, do the
|
|
99
|
+
following with bh_run instead:
|
|
100
|
+
|
|
101
|
+
browser_navigate(url) -> First navigation: bh_run('new_tab("URL"); wait_for_load()')
|
|
102
|
+
Subsequent navigations (same session): bh_run('goto_url("URL"); wait_for_load()')
|
|
103
|
+
browser_snapshot -> bh_run('print(js("""..."""))') to read DOM as structured data,
|
|
104
|
+
OR bh_run('print(capture_screenshot())') + Read the PNG
|
|
105
|
+
browser_run_code(js) -> bh_run('print(js("""<the JS expression>"""))')
|
|
106
|
+
browser_click(ref=...) -> Find the element via selector, compute center coords from
|
|
107
|
+
getBoundingClientRect, then bh_run('click_at_xy(X, Y)')
|
|
108
|
+
browser_type(ref=..., text=...) -> Click the textbox first (click_at_xy), then bh_run('type_text("TEXT")')
|
|
109
|
+
browser_take_screenshot -> bh_run('print(capture_screenshot())') then Read the path
|
|
110
|
+
browser_press_key("Enter") -> bh_run('press_key("Enter")')
|
|
111
|
+
|
|
112
|
+
VERIFY AFTER EVERY MUTATION by capturing a screenshot and reading the PNG, coordinate
|
|
113
|
+
clicks can miss; visual verification is the only reliable confirmation that the action took.
|
|
114
|
+
BROWSER_HARNESS_EOF
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
cleanup_harness_tabs() {
|
|
118
|
+
# Close every CDP "page" tab except one. Same pattern as twitter/linkedin
|
|
119
|
+
# backend, scoped to the Reddit harness Chrome on port 9557.
|
|
120
|
+
#
|
|
121
|
+
# Health-check gate: 10s timeout + ONE retry; log skips so they are not silent.
|
|
122
|
+
local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:9557/json/version"
|
|
123
|
+
if ! $_probe 2>/dev/null; then
|
|
124
|
+
sleep 1
|
|
125
|
+
if ! $_probe 2>/dev/null; then
|
|
126
|
+
echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (reddit-harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
BH_CLEANUP_PORT=9557 python3 "$HOME/social-autoposter/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_resolve_chrome_bin() {
|
|
134
|
+
# Auto-detect Chrome/Chromium so the same script launches the harness on
|
|
135
|
+
# macOS dev boxes AND Linux VMs. Override with BH_CHROME_BIN.
|
|
136
|
+
if [ -n "${BH_CHROME_BIN:-}" ] && [ -x "$BH_CHROME_BIN" ]; then
|
|
137
|
+
echo "$BH_CHROME_BIN"; return 0
|
|
138
|
+
fi
|
|
139
|
+
for _p in \
|
|
140
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
141
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
142
|
+
"/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
|
|
143
|
+
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
|
|
144
|
+
do
|
|
145
|
+
if [ -x "$_p" ]; then echo "$_p"; return 0; fi
|
|
146
|
+
done
|
|
147
|
+
for _n in google-chrome google-chrome-stable chromium chromium-browser; do
|
|
148
|
+
_which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
|
|
149
|
+
done
|
|
150
|
+
echo ""; return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ensure_reddit_browser_for_backend() {
|
|
154
|
+
# AppMaker / BYO Chrome: REDDIT_CDP_URL points at something other than our
|
|
155
|
+
# default harness URL. Don't touch that browser; just probe it and bail.
|
|
156
|
+
if [ "${REDDIT_CDP_URL:-$_BH_REDDIT_DEFAULT_URL}" != "$_BH_REDDIT_DEFAULT_URL" ]; then
|
|
157
|
+
local _ext_url="${REDDIT_CDP_URL}"
|
|
158
|
+
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
159
|
+
echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
160
|
+
return 0
|
|
161
|
+
fi
|
|
162
|
+
echo "[$(date +%H:%M:%S)] ERROR: REDDIT_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host." >&2
|
|
163
|
+
return 1
|
|
164
|
+
fi
|
|
165
|
+
# Probe + launch harness Chrome on port 9557 if needed.
|
|
166
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
167
|
+
echo "[$(date +%H:%M:%S)] Reddit harness Chrome down on port 9557, launching..." >&2
|
|
168
|
+
local _chrome_bin
|
|
169
|
+
_chrome_bin=$(_resolve_chrome_bin)
|
|
170
|
+
if [ -z "$_chrome_bin" ]; then
|
|
171
|
+
echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
172
|
+
return 1
|
|
173
|
+
fi
|
|
174
|
+
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
175
|
+
local _extra=()
|
|
176
|
+
case "$(uname -s)" in
|
|
177
|
+
Linux)
|
|
178
|
+
_extra+=(--no-sandbox --disable-dev-shm-usage)
|
|
179
|
+
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
|
180
|
+
_extra+=(--headless=new --disable-gpu)
|
|
181
|
+
fi
|
|
182
|
+
;;
|
|
183
|
+
Darwin)
|
|
184
|
+
# Default position = the Reddit browser's current off-screen
|
|
185
|
+
# spot (captured 2026-05-29); overridable via BH_REDDIT_WINDOW_POS.
|
|
186
|
+
_extra+=(--window-position="${BH_REDDIT_WINDOW_POS:-2131,-1032}")
|
|
187
|
+
_extra+=(--window-size="${BH_REDDIT_WINDOW_SIZE:-911,1016}")
|
|
188
|
+
;;
|
|
189
|
+
esac
|
|
190
|
+
# Self-heal (2026-06-03): reap any stale Chrome holding THIS profile dir
|
|
191
|
+
# but not answering CDP on our port, else the relaunch hands off via the
|
|
192
|
+
# SingletonLock and loops "failed to start within 12s". Exact-dir match
|
|
193
|
+
# (trailing space) keeps this scoped to reddit-harness only. See
|
|
194
|
+
# twitter-backend.sh for the regression that motivated this.
|
|
195
|
+
local _prof_dir="$HOME/.claude/browser-profiles/reddit-harness"
|
|
196
|
+
local _stale_pids
|
|
197
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
198
|
+
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
199
|
+
echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
200
|
+
kill $_stale_pids 2>/dev/null || true
|
|
201
|
+
sleep 2
|
|
202
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
203
|
+
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
204
|
+
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
205
|
+
fi
|
|
206
|
+
"$_chrome_bin" \
|
|
207
|
+
--remote-debugging-port=9557 \
|
|
208
|
+
--user-data-dir="$HOME/.claude/browser-profiles/reddit-harness" \
|
|
209
|
+
--no-first-run --no-default-browser-check \
|
|
210
|
+
--disable-features=ChromeWhatsNewUI \
|
|
211
|
+
"${_extra[@]}" \
|
|
212
|
+
about:blank >/dev/null 2>&1 &
|
|
213
|
+
disown
|
|
214
|
+
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
215
|
+
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null && break
|
|
216
|
+
sleep 1
|
|
217
|
+
done
|
|
218
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null; then
|
|
219
|
+
echo "[$(date +%H:%M:%S)] ERROR: Reddit harness Chrome failed to start within 12s" >&2
|
|
220
|
+
return 1
|
|
221
|
+
fi
|
|
222
|
+
echo "[$(date +%H:%M:%S)] Reddit harness Chrome up on port 9557" >&2
|
|
223
|
+
fi
|
|
224
|
+
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
225
|
+
# "reddit-browser" serialization.
|
|
226
|
+
cleanup_harness_tabs
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
defer_if_foreign_for_backend() {
|
|
230
|
+
# Harness Chrome accepts multiple concurrent CDP clients on the same
|
|
231
|
+
# reddit-harness profile, so a foreign MCP wrapper cannot cause
|
|
232
|
+
# SingletonLock contention. Always return 1 (do not defer).
|
|
233
|
+
return 1
|
|
234
|
+
}
|