@m13v/s4l 1.6.197-rc.10
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 +1336 -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 +513 -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,201 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# link-edit-reddit.sh — Edit high-performing Reddit comments to append a project link.
|
|
3
|
+
# Splits out from the legacy engage.sh Phase D so a single platform failure
|
|
4
|
+
# (e.g. LinkedIn hang) no longer blocks Reddit.
|
|
5
|
+
# Called by launchd (com.m13v.social-link-edit-reddit) every 6 hours.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Cycle ID for cross-cycle cost accounting (see run-reddit-search.sh / engage-reddit.sh
|
|
10
|
+
# for the same pattern). Stamps claude_sessions.cycle_id via env inheritance.
|
|
11
|
+
BATCH_ID="${BATCH_ID:-lerd-$(date +%Y%m%d-%H%M%S)}"
|
|
12
|
+
export BATCH_ID
|
|
13
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
14
|
+
|
|
15
|
+
# Pipeline lock at top. We DO NOT acquire reddit-browser at the bash level
|
|
16
|
+
# anymore — claude itself acquires it per-post via
|
|
17
|
+
# scripts/reddit_browser_lock.py, only around the actual MCP browser
|
|
18
|
+
# operations (~15-60s per post). This unblocks peer reddit pipelines
|
|
19
|
+
# (engage-reddit, dm-replies-reddit, post-reddit) during the long
|
|
20
|
+
# generate_page.py / WebFetch / DB phases of each post.
|
|
21
|
+
source "$(dirname "$0")/lock.sh"
|
|
22
|
+
# reddit-harness backend (2026-05-29). Sets MCP_CONFIG_FILE, BROWSER_INSTRUCTIONS,
|
|
23
|
+
# exports REDDIT_CDP_URL=:9557, provides ensure_reddit_browser_for_backend.
|
|
24
|
+
source "$(dirname "$0")/lib/reddit-backend.sh"
|
|
25
|
+
acquire_lock "link-edit-reddit" 5400
|
|
26
|
+
|
|
27
|
+
# Load secrets
|
|
28
|
+
# shellcheck source=/dev/null
|
|
29
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
30
|
+
|
|
31
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
32
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
33
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
34
|
+
# HTTP-only lane (2026-06-01): all reads/writes go through the s4l.ai API via
|
|
35
|
+
# scripts/link_edit_helper.py. No DATABASE_URL, no psql, no fallback.
|
|
36
|
+
LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
37
|
+
|
|
38
|
+
mkdir -p "$LOG_DIR"
|
|
39
|
+
LOG_FILE="$LOG_DIR/link-edit-reddit-$(date +%Y-%m-%d_%H%M%S).log"
|
|
40
|
+
|
|
41
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
42
|
+
|
|
43
|
+
RUN_START=$(date +%s)
|
|
44
|
+
log "=== Reddit Link Edit Run: $(date) ==="
|
|
45
|
+
|
|
46
|
+
# A/B gate: per-post deterministic coin flip for the page-gen lane. Mirrors
|
|
47
|
+
# scripts/twitter_gen_links.py's TWITTER_PAGE_GEN_RATE behavior. 0.30 means
|
|
48
|
+
# ~30% of eligible posts hit the full seo/generate_page.py pipeline; the
|
|
49
|
+
# other ~70% fall through to the project's homepage with
|
|
50
|
+
# link_source='plain_url_ab_skip'. Per-post hash via Postgres hashtext() so
|
|
51
|
+
# the same post stays in the same lane across cron retries — without that
|
|
52
|
+
# we'd risk shipping two different lanes for the same post on consecutive
|
|
53
|
+
# runs and burn budget. Tunable via env var so cadence sweeps don't need
|
|
54
|
+
# code changes. 0.0 disables page-gen; 1.0 restores 100% page-gen.
|
|
55
|
+
# DEFAULT 0.0: Reddit no longer generates custom SEO pages — every eligible
|
|
56
|
+
# post goes through the wrap-an-existing-link route (homepage + /r/<code>).
|
|
57
|
+
LINK_EDIT_REDDIT_PAGE_GEN_RATE="${LINK_EDIT_REDDIT_PAGE_GEN_RATE:-0.0}"
|
|
58
|
+
PAGE_GEN_RATE_PCT=$(python3 -c "v=float('$LINK_EDIT_REDDIT_PAGE_GEN_RATE'); v=max(0.0,min(1.0,v)); print(int(round(v*100)))")
|
|
59
|
+
log "A/B gate: LINK_EDIT_REDDIT_PAGE_GEN_RATE=$LINK_EDIT_REDDIT_PAGE_GEN_RATE (page_gen_lane='page_gen' on ~${PAGE_GEN_RATE_PCT}% of eligible posts; rest go to plain_url_ab_skip)"
|
|
60
|
+
|
|
61
|
+
EDITABLE=$(python3 "$LE_HELPER" eligible --platform reddit --min-upvotes-exclusive 1 --page-gen-rate-pct "$PAGE_GEN_RATE_PCT" --order upvotes 2>/dev/null || echo "")
|
|
62
|
+
|
|
63
|
+
if [ "$EDITABLE" = "null" ] || [ -z "$EDITABLE" ]; then
|
|
64
|
+
log "No Reddit posts eligible for link edit"
|
|
65
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_reddit" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
EDITABLE_COUNT=$(echo "$EDITABLE" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
70
|
+
log "Reddit: $EDITABLE_COUNT posts eligible for link edit"
|
|
71
|
+
|
|
72
|
+
PROMPT_FILE=$(mktemp)
|
|
73
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
74
|
+
You are the Social Autoposter Reddit link-edit bot.
|
|
75
|
+
|
|
76
|
+
Read $SKILL_FILE for the full workflow. Execute the Reddit link-edit phase only.
|
|
77
|
+
|
|
78
|
+
$BROWSER_INSTRUCTIONS
|
|
79
|
+
|
|
80
|
+
CRITICAL - Browser agent rule: ONLY use the browser tool described in the BROWSER BACKEND block above (mcp__reddit-harness__bh_run). NEVER use generic mcp__playwright-extension__*, mcp__isolated-browser__*, mcp__reddit-agent__*, or mcp__macos-use__* tools. If a bh_run call is blocked or times out, wait 30s and retry (up to 3 times). If still blocked, skip that post.
|
|
81
|
+
|
|
82
|
+
CRITICAL: This is a single-shot run. NEVER call ScheduleWakeup, CronCreate, CronDelete, CronList, EnterPlanMode, EnterWorktree, or any deferred-execution / scheduling tool. You MUST complete or skip every post in this one run; do not defer work to "a future run". If you hit a hard block, mark the post SKIPPED via step 9 and move on to the next post.
|
|
83
|
+
|
|
84
|
+
EXECUTION MODEL — STRICT SEQUENTIAL, NO BATCHING (read this twice):
|
|
85
|
+
- Process posts ONE AT A TIME. Run the FULL chain (steps 1 → 8) end-to-end for post N before reading post N+1.
|
|
86
|
+
- NEVER batch generate_page.py calls in parallel. NEVER spawn page-gen subprocesses with double-fork (\`nohup ... &\`, \`disown\`, \`setsid\`, \`os.fork\`). NEVER write a polling loop that watches /tmp/seo_*.json. Each generate_page.py is a foreground call that you wait for synchronously.
|
|
87
|
+
- Step 8's DB UPDATE for post N MUST complete before you start step 1 for post N+1. This protects interim work: if the run is killed mid-stream, the first N-1 posts are fully shipped and only ONE post is in flight at most.
|
|
88
|
+
- The reddit-browser lock is NOT held by the parent shell. You acquire/release it explicitly per post (steps 6.5 and 7.5), so peer reddit pipelines can use the browser during your generate_page.py / WebFetch / DB phases.
|
|
89
|
+
|
|
90
|
+
Reddit posts eligible for editing:
|
|
91
|
+
$EDITABLE
|
|
92
|
+
|
|
93
|
+
Process ALL of them SEQUENTIALLY (one at a time, full chain per post). For each post:
|
|
94
|
+
1. Read ~/social-autoposter/config.json to get the projects list.
|
|
95
|
+
2. Pick the project whose topics are the CLOSEST match to thread_title + our_content.
|
|
96
|
+
a. First check the project_name column. If it is set AND its topics/description fit the thread, use it.
|
|
97
|
+
b. If project_name is set but CLEARLY does not fit the thread (e.g. Cyrano tagged to a law firm billing thread), treat it as a bad upstream tag and scan config.json for a project that DOES fit. If you find one, use that project instead and also run: python3 ~/social-autoposter/scripts/link_edit_helper.py set-project --post-id POST_ID --project "BETTER_PROJECT" so the correction is persisted.
|
|
98
|
+
c. If project_name is NOT set, match by topics. Be generous: if the thread touches agents, automation, desktop, memory, or anything related to the project descriptions, it's a match.
|
|
99
|
+
d. ONLY if no project in config.json fits at all, mark it skipped (see step 9) and move on. Frame it as recommending a cool tool you've come across, NOT as something you built.
|
|
100
|
+
3. PAGE-GEN LANE GATE — read the post's \`page_gen_lane\` field (set deterministically by the pipeline; do NOT override).
|
|
101
|
+
- If \`page_gen_lane == "ab_skip"\`: SKIP the full SEO page generation entirely. Set LINK_URL = the matched project's homepage from config.json (the \`website\` field) and LINK_SOURCE="plain_url_ab_skip". Continue to step 4. The /r/<code> short-link wrap in step 5 still mints attribution on the project's own domain, so we get click data for this lane to compare against seo_page lane CTR.
|
|
102
|
+
- If \`page_gen_lane == "page_gen"\` AND the matched project has a landing_pages config: continue to step 3a below.
|
|
103
|
+
- If \`page_gen_lane == "page_gen"\` BUT the matched project has NO landing_pages config: skip page-gen, set LINK_URL = project homepage, LINK_SOURCE="plain_url_no_lp", continue to step 4.
|
|
104
|
+
|
|
105
|
+
3a. If the matched project has a landing_pages config (with repo, base_url), generate a fresh SEO page for this thread by delegating to the unified generator:
|
|
106
|
+
a. Decide a SHORT keyword phrase (3-6 words) that captures what page would help this thread's audience. Think SEO intent, not headline copy. Examples: "local ai agent", "macos accessibility automation", "self hosted llm inference".
|
|
107
|
+
b. Derive a URL slug from the keyword: lowercase, kebab-case, alphanumeric and hyphens only, max 50 chars. Examples: "local-ai-agent", "macos-accessibility-automation".
|
|
108
|
+
c. Run the unified SEO page generator (it loads the @m13v/seo-components palette, picks content type, builds the page, commits, pushes, verifies the live URL, and writes the seo_keywords row that surfaces in the dashboard activity feed). The generator's prompt has its own model-driven Step 0 reuse-or-redirect decision: if an existing page on the site already serves this keyword's intent, the generator will consolidate (308 redirect this slug to the existing page) instead of building a duplicate. Trust that decision; do not pre-filter for reuse here. Use the Bash tool:
|
|
109
|
+
python3 ~/social-autoposter/seo/generate_page.py --product PROJECT_NAME --keyword "KEYWORD_PHRASE" --slug "url-slug" --trigger reddit
|
|
110
|
+
This call can take 10-40 minutes per page (Cloud Run staging-then-tag deploys on mk0r are the slow end). The final stdout is a JSON object; parse it. On success it contains "success": true and "page_url": "https://...". On failure it contains "success": false and "error": "...".
|
|
111
|
+
d. If success, set LINK_URL = the \`page_url\` from the JSON output and LINK_SOURCE="seo_page".
|
|
112
|
+
e. If failure (success: false in the JSON), fall back GRACEFULLY (mirrors the Twitter pipeline behavior in scripts/twitter_gen_links.py): set LINK_URL = the project's homepage from config.json (the \`website\` field for the matched project) and set LINK_SOURCE="plain_url_fallback:<reason>" where <reason> is a SHORT snake_case tag derived from the JSON error string (preferred values: timeout, no_page_url, deploy_failed, build_failed, push_failed; otherwise pick a sensible 1-3 word snake_case summary). Do NOT skip the post; continue to step 4. The short-link wrap in step 5 will still mint a /r/<code> on the project's own domain, so click attribution works on the homepage URL too.
|
|
113
|
+
4. Write the link sentence as a CONTEXTUAL BRIDGE, not a citation or footnote. This is a structured 4-step sub-task; do NOT shortcut it.
|
|
114
|
+
a. Re-read our_content (the comment we already left on this Reddit thread). Identify the SINGLE strongest claim, mechanism, or specific number we said in that comment (examples: "auto-rephrasing on revisit", "the 81 number on the rubric", "scoring distractor quality", "200ms p95 latency", "structured output across nested tool calls"). Pick ONE concrete thing, not a category.
|
|
115
|
+
b. Read the landing page at LINK_URL (use WebFetch on LINK_URL). Identify the SPECIFIC mechanism on the page that delivers the thing from step 4a (one feature, one capability, named in the page's own language).
|
|
116
|
+
c. Write 1-2 sentences (max ~35 words) that explicitly bridge: comment-claim → product mechanism → URL. The sentence MUST name at least ONE concrete capability inline (verb + noun, e.g. "scores against a rubric and rephrases the same fact on revisit"); NOT a category ("a tool for studying", "something for this"). The URL is the last token; nothing after it.
|
|
117
|
+
d. Voice depends on the matched project's \`voice_relationship\` field in config.json (read it before drafting):
|
|
118
|
+
- voice_relationship == "first_party": Frame as our own creation. Bridge form: "fwiw the [specific-thing-from-4a] is from [PROJECT_NAME], a thing I built that [specific-mechanism-from-4b], LINK_URL". Concrete first_party examples (do NOT copy verbatim; rewrite to match each post's actual claim):
|
|
119
|
+
* "fwiw the 81 number is from a rubric we trained on med-school decks, scores factual correctness, distractor quality, and rephrases on revisit so you can't pattern-match the stem, https://studyly.io/t/anki-card-generator-for-medical-school"
|
|
120
|
+
* "we shipped this for the rephrase-on-revisit problem specifically, it scores against a 4-axis rubric and re-asks the same fact in different stems, https://studyly.io/t/active-recall-question-generator"
|
|
121
|
+
- voice_relationship == "third_party": for accounts that are not the project's maker. Cite the project's mechanism directly. Bridge form: "fwiw [PROJECT_NAME] handles the [specific-thing-from-4a] via [specific-mechanism-from-4b], LINK_URL". Do NOT use "I built" / "we shipped" / "we made", and do NOT use "found this" / "came across this". Stay matter-of-fact and concrete.
|
|
122
|
+
e. BANNED phrasing (rejects the bridge structure, costs CTR):
|
|
123
|
+
- Bare citation with no mechanism: "fwiw the X number is from PROJECT, URL" with no named mechanism.
|
|
124
|
+
- Generic verbs with no object: "a tool for exactly this", "does this" (no concrete mechanism named).
|
|
125
|
+
- "Click here", "check it out", "give it a try" (Reddit downvotes pitchy CTAs).
|
|
126
|
+
- For voice_relationship=third_party only: "I built" / "we shipped" / "we made" (ownership claims on a project you don't own).
|
|
127
|
+
- For voice_relationship=first_party only: "I found this", "there's a tool", "came across this", "saw this manual", "found this guide" (pretends we're a neutral commenter pointing at someone else's project).
|
|
128
|
+
f. SELF-REVIEW before posting: read your sentence aloud. Does it (1) reference the comment's specific claim, (2) name a concrete product mechanism, and (3) end on the URL? If any of the three is missing, rewrite. If after one rewrite it still doesn't pass all three checks, write a fresh sentence from scratch.
|
|
129
|
+
5. URL-WRAP THE LINK TEXT for click attribution. This MUST run for every LINK_SOURCE (seo_page, plain_url_fallback:*, plain_url_no_lp). The wrap helper accepts homepage URLs and mints a /r/<code> on the project's own domain. Run:
|
|
130
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py wrap-post-text \\
|
|
131
|
+
--text "YOUR_LINK_SENTENCE_WITH_URL" \\
|
|
132
|
+
--platform reddit \\
|
|
133
|
+
--project PROJECT_NAME
|
|
134
|
+
PROJECT_NAME must be the EXACT \`name\` field from config.json (case-sensitive; e.g. "fazm" lowercase, "Cyrano", "WhatsApp MCP"). Parse the JSON output. Use \`text\` (URL replaced with /r/<code>) as the FINAL LINK_TEXT for steps 6 and 7. Keep \`minted_session\` for step 8. If wrap returns ok=false, log the error and skip this post (do NOT post a raw URL).
|
|
135
|
+
6. Append the wrapped LINK_TEXT to our_content with a blank line separator.
|
|
136
|
+
6.5. ACQUIRE the reddit-browser lock NOW (just before any reddit-agent browser call). This is the ONLY moment you may touch the browser:
|
|
137
|
+
LOCK_OUT=\$(python3 ~/social-autoposter/scripts/reddit_browser_lock.py acquire --timeout 600 2>&1)
|
|
138
|
+
- If stdout starts with "OK", proceed to step 7.
|
|
139
|
+
- If "BUSY", a peer reddit pipeline owns the browser and didn't release within 10 min. Treat as a TRANSIENT skip (step 10B): leave link_edited_at NULL, log the reason, move on to the NEXT post. Do NOT call step 7 without the lock — collisions on the same chrome profile crash both runs.
|
|
140
|
+
- If "ERROR", same handling as BUSY: TRANSIENT skip, leave link_edited_at NULL, move on.
|
|
141
|
+
7. Navigate to old.reddit.com comment permalink via the reddit-agent browser. Click "edit", append the wrapped link text to the existing content, save, verify.
|
|
142
|
+
7.5. RELEASE the reddit-browser lock IMMEDIATELY after the edit confirms or fails. This is mandatory — failing to release it blocks every other reddit pipeline:
|
|
143
|
+
python3 ~/social-autoposter/scripts/reddit_browser_lock.py release
|
|
144
|
+
Run this even if step 7 raised, errored, or you're skipping the post. Wrap step 7 in a way that step 7.5 ALWAYS executes (mental try/finally). The release is idempotent and safe to call multiple times.
|
|
145
|
+
8. After each successful edit, update the DB (via the HTTP API helper; pass link_source so we can A/B compare seo_page vs plain_url_fallback:* vs plain_url_no_lp click-through rates, same as Twitter does in scripts/twitter_gen_links.py) and backfill short-link attribution. THIS MUST RUN BEFORE YOU START THE NEXT POST:
|
|
146
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-edited --post-id POST_ID --content "LINK_TEXT" --source "LINK_SOURCE"
|
|
147
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py backfill-post --minted-session MINTED_SESSION --post-id POST_ID
|
|
148
|
+
9. COMMITMENT GUARDRAILS (never violate these):
|
|
149
|
+
- NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.
|
|
150
|
+
- NEVER promise to share links, files, or resources you don't have right now. Only share links from config.json projects (plus any new landing page you just deployed).
|
|
151
|
+
- NEVER offer to DM or send anything outside the comment.
|
|
152
|
+
- NEVER make time-bound promises.
|
|
153
|
+
10. SKIP HANDLING — two classes:
|
|
154
|
+
A. PERMANENT skips (no project match, comment not found, removed by moderation, bad URL, post deleted, project has no landing_pages and no website at all): mark so it won't be retried.
|
|
155
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-skipped --post-id POST_ID --reason "REASON"
|
|
156
|
+
B. TRANSIENT skips (single_run_capacity_exceeded, batch budget exhausted, you ran out of time, reddit-agent locked by peer, anything that would resolve on retry): DO NOT stamp link_edited_at. Leave both link_edited_at and link_edit_content NULL so the next 6h cron picks it up again.
|
|
157
|
+
Only annotate the reason in a comment / log line; never write to the DB for transient skips.
|
|
158
|
+
If unsure which class a skip falls into, treat it as TRANSIENT (default to retry, not to swallow). Stamping link_edited_at is permanent — once set, the post is excluded from future eligibility queries forever.
|
|
159
|
+
PROMPT_EOF
|
|
160
|
+
|
|
161
|
+
# NOTE: We do NOT acquire reddit-browser at the bash level. Claude itself
|
|
162
|
+
# acquires/releases it per post via scripts/reddit_browser_lock.py
|
|
163
|
+
# (steps 6.5 and 7.5 in the prompt). This keeps the lock held only during
|
|
164
|
+
# the actual ~15-60s reddit-agent browser ops per post, not the full
|
|
165
|
+
# 90-min run. Peer pipelines (engage-reddit, dm-replies-reddit,
|
|
166
|
+
# post-reddit) can use the profile during our generate_page.py / WebFetch
|
|
167
|
+
# / DB phases.
|
|
168
|
+
#
|
|
169
|
+
# Pre-flight: ensure the profile isn't wedged by a prior crashed run.
|
|
170
|
+
# Unified lock (2026-05-10): brief Python acquire+release so the orphan-Chrome
|
|
171
|
+
# sweep happens once before claude starts. Python acquire honors expires_at,
|
|
172
|
+
# so a TTL-stale-but-PID-alive holder gets reclaimed automatically instead of
|
|
173
|
+
# blocking us for the full bash timeout.
|
|
174
|
+
log "Pre-flight: sweep orphan reddit-agent Chrome / playwright-mcp before handing off to claude..."
|
|
175
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 60 --ttl 30 2>&1 | tee -a "$LOG_FILE" || \
|
|
176
|
+
log "WARNING: reddit_browser_lock.py pre-flight acquire failed; proceeding (claude will retry per-post)."
|
|
177
|
+
if ! ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE"; then
|
|
178
|
+
log "WARNING: reddit-harness bootstrap failed; falling back to ensure_browser_healthy reddit"
|
|
179
|
+
ensure_browser_healthy "reddit"
|
|
180
|
+
fi
|
|
181
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
182
|
+
|
|
183
|
+
gtimeout 5400 "$REPO_DIR/scripts/run_claude.sh" "link-edit-reddit" --strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" --disallowed-tools "ScheduleWakeup,CronCreate,CronDelete,CronList,EnterPlanMode,EnterWorktree" --output-format stream-json --verbose -p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: Reddit link-edit claude exited with code $?"
|
|
184
|
+
rm -f "$PROMPT_FILE"
|
|
185
|
+
|
|
186
|
+
# Belt-and-suspenders: if claude exited without releasing the lock (e.g.
|
|
187
|
+
# crashed mid-edit before reaching step 7.5), free it now so peer
|
|
188
|
+
# pipelines aren't stuck behind a phantom holder. release_lock checks
|
|
189
|
+
# the lock_dir and rm-rf's it; safe even if claude already released.
|
|
190
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
191
|
+
|
|
192
|
+
EDITED=$(python3 "$LE_HELPER" edited-count --platform reddit 2>/dev/null || echo "0")
|
|
193
|
+
log "Reddit link-edit complete. Total reddit posts edited (all-time): $EDITED"
|
|
194
|
+
|
|
195
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
196
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "link-edit-reddit" 2>/dev/null || echo "0.0000")
|
|
197
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_reddit" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
198
|
+
|
|
199
|
+
find "$LOG_DIR" -name "link-edit-reddit-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
200
|
+
|
|
201
|
+
log "=== Reddit link-edit complete: $(date) ==="
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# linkedin-presence.sh - read-only LinkedIn session presence pass.
|
|
3
|
+
#
|
|
4
|
+
# Purpose:
|
|
5
|
+
# Run a bounded, auditable browser pass in the real linkedin-harness Chrome.
|
|
6
|
+
# It only views first-party LinkedIn surfaces and performs small scroll passes.
|
|
7
|
+
# It does not like, follow, connect, message, comment, expand comments, or open
|
|
8
|
+
# post permalinks.
|
|
9
|
+
#
|
|
10
|
+
# This is intentionally a Claude/harness pipeline, not a Python CDP action
|
|
11
|
+
# helper, so it stays inside the same LinkedIn browser-action boundary as the
|
|
12
|
+
# rest of the repo.
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
17
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
18
|
+
mkdir -p "$LOG_DIR"
|
|
19
|
+
LOG_FILE="$LOG_DIR/linkedin-presence-$(date +%Y-%m-%d_%H%M%S).log"
|
|
20
|
+
RUN_START=$(date +%s)
|
|
21
|
+
BATCH_ID="lipres-$(date +%Y%m%d_%H%M%S)-$$"
|
|
22
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
23
|
+
export S4L_PIPELINE_NAME="linkedin-presence"
|
|
24
|
+
|
|
25
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
26
|
+
|
|
27
|
+
DRY_RUN="${LINKEDIN_PRESENCE_DRY_RUN:-0}"
|
|
28
|
+
|
|
29
|
+
if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ] && [ "$DRY_RUN" != "1" ]; then
|
|
30
|
+
log "LINKEDIN_KILLSWITCH active. Skipping LinkedIn presence pass."
|
|
31
|
+
exit 0
|
|
32
|
+
elif [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
|
|
33
|
+
log "DRY_RUN: ignoring active LINKEDIN_KILLSWITCH for validation."
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Optional local kill switch for operators who want the plist loaded but dormant.
|
|
37
|
+
if [ "${LINKEDIN_PRESENCE_ENABLED:-1}" = "0" ]; then
|
|
38
|
+
log "LINKEDIN_PRESENCE_ENABLED=0. Skipping."
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# The launchd timer is fixed; vary each actual pass inside the script. Skipped
|
|
43
|
+
# passes do not write run_monitor rows, so the dashboard history only shows real
|
|
44
|
+
# browser activity.
|
|
45
|
+
RUN_PCT="${LINKEDIN_PRESENCE_RUN_PCT:-65}"
|
|
46
|
+
if [ "$DRY_RUN" != "1" ]; then
|
|
47
|
+
ROLL=$(( RANDOM % 100 ))
|
|
48
|
+
if [ "$ROLL" -ge "$RUN_PCT" ]; then
|
|
49
|
+
log "Presence pass skipped by schedule jitter (roll=$ROLL threshold=$RUN_PCT)."
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
JITTER_MAX="${LINKEDIN_PRESENCE_JITTER_MAX_SEC:-900}"
|
|
53
|
+
if [ "$JITTER_MAX" -gt 0 ]; then
|
|
54
|
+
JITTER=$(( RANDOM % (JITTER_MAX + 1) ))
|
|
55
|
+
log "Sleeping ${JITTER}s before presence pass."
|
|
56
|
+
sleep "$JITTER"
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
MODE_ROLL=$(( RANDOM % 4 ))
|
|
61
|
+
case "$MODE_ROLL" in
|
|
62
|
+
0) MODE="feed"; TARGET_URL="https://www.linkedin.com/feed/" ;;
|
|
63
|
+
1) MODE="notifications"; TARGET_URL="https://www.linkedin.com/notifications/" ;;
|
|
64
|
+
2) MODE="messaging"; TARGET_URL="https://www.linkedin.com/messaging/" ;;
|
|
65
|
+
*) MODE="profile"; TARGET_URL="https://www.linkedin.com/in/me/" ;;
|
|
66
|
+
esac
|
|
67
|
+
|
|
68
|
+
SCROLLS=$(( 1 + (RANDOM % 3) ))
|
|
69
|
+
DWELL_A=$(( 2 + (RANDOM % 4) ))
|
|
70
|
+
DWELL_B=$(( 2 + (RANDOM % 4) ))
|
|
71
|
+
DWELL_C=$(( 2 + (RANDOM % 4) ))
|
|
72
|
+
SCROLL_A=$(( 420 + (RANDOM % 260) ))
|
|
73
|
+
SCROLL_B=$(( 420 + (RANDOM % 260) ))
|
|
74
|
+
SCROLL_C=$(( 420 + (RANDOM % 260) ))
|
|
75
|
+
|
|
76
|
+
log "=== LinkedIn Presence Run: $(date) (batch=$BATCH_ID mode=$MODE scrolls=$SCROLLS) ==="
|
|
77
|
+
|
|
78
|
+
# shellcheck source=/dev/null
|
|
79
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
80
|
+
|
|
81
|
+
source "$REPO_DIR/skill/lock.sh"
|
|
82
|
+
source "$REPO_DIR/skill/lib/linkedin-backend.sh"
|
|
83
|
+
|
|
84
|
+
PROMPT_FILE="$(mktemp -t saps-linkedin-presence.XXXXXX)"
|
|
85
|
+
cleanup() {
|
|
86
|
+
rm -f "$PROMPT_FILE" 2>/dev/null || true
|
|
87
|
+
rm -f "$HOME/.claude/linkedin-agent-lock.json" 2>/dev/null || true
|
|
88
|
+
if declare -f _sa_release_locks >/dev/null 2>&1; then
|
|
89
|
+
_sa_release_locks || true
|
|
90
|
+
fi
|
|
91
|
+
}
|
|
92
|
+
trap cleanup EXIT INT TERM HUP
|
|
93
|
+
|
|
94
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
95
|
+
You are running a read-only LinkedIn presence pass for Social Autoposter.
|
|
96
|
+
|
|
97
|
+
$BROWSER_INSTRUCTIONS
|
|
98
|
+
|
|
99
|
+
Task:
|
|
100
|
+
- Mode: $MODE
|
|
101
|
+
- URL: $TARGET_URL
|
|
102
|
+
- Scroll passes: $SCROLLS
|
|
103
|
+
- Scroll amounts, in order: $SCROLL_A, $SCROLL_B, $SCROLL_C
|
|
104
|
+
- Dwell seconds, in order: $DWELL_A, $DWELL_B, $DWELL_C
|
|
105
|
+
|
|
106
|
+
Hard rules:
|
|
107
|
+
- Use only mcp__linkedin-harness__bh_run.
|
|
108
|
+
- Do not post, comment, react, like, repost, follow, connect, send messages, or submit forms.
|
|
109
|
+
- Do not open individual post permalinks.
|
|
110
|
+
- Do not click "Show more comments", "Load earlier replies", "See more", or any comment-expansion control.
|
|
111
|
+
- Do not call /voyager/api/*, fetch(), XHR, or any internal LinkedIn endpoint.
|
|
112
|
+
- If a login, checkpoint, captcha, authwall, or verify-you-are-human page appears, print exactly SESSION_INVALID and stop. Do not try to log in.
|
|
113
|
+
- In messaging mode, stay on the messaging sidebar/list. Do not open a conversation and do not read private thread contents.
|
|
114
|
+
|
|
115
|
+
Workflow:
|
|
116
|
+
1. Reuse the existing tab and navigate to $TARGET_URL:
|
|
117
|
+
bh_run('goto_url("$TARGET_URL"); wait_for_load()')
|
|
118
|
+
2. Read the current URL and a small page text sample using js(). If the URL or page text indicates login/checkpoint/captcha/authwall, print SESSION_INVALID and stop.
|
|
119
|
+
3. Capture one screenshot and read it so you visually confirm the surface loaded.
|
|
120
|
+
4. Perform exactly $SCROLLS bounded scroll pass(es) on the loaded surface using the bh_run scroll(direction, amount) helper, with the amounts/dwells above. Do not use window.scrollBy. Do not click anything.
|
|
121
|
+
5. Capture one final screenshot.
|
|
122
|
+
6. Print exactly one summary line:
|
|
123
|
+
LINKEDIN_PRESENCE_SUMMARY: mode=$MODE pages=1 scrolls=$SCROLLS session=ok
|
|
124
|
+
|
|
125
|
+
Keep the run short and quiet. This is a read-only session maintenance pass, not discovery, scraping, or engagement.
|
|
126
|
+
PROMPT_EOF
|
|
127
|
+
|
|
128
|
+
if [ "$DRY_RUN" = "1" ]; then
|
|
129
|
+
log "DRY_RUN: would run mode=$MODE url=$TARGET_URL scrolls=$SCROLLS"
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
acquire_lock "linkedin-browser" 1800
|
|
134
|
+
ensure_linkedin_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
135
|
+
|
|
136
|
+
TIMEOUT_BIN="$(command -v gtimeout || command -v timeout || true)"
|
|
137
|
+
PRESENCE_RC=0
|
|
138
|
+
set +e
|
|
139
|
+
if [ -n "$TIMEOUT_BIN" ]; then
|
|
140
|
+
"$TIMEOUT_BIN" 900 "$REPO_DIR/scripts/run_claude.sh" "linkedin-presence" \
|
|
141
|
+
--strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" \
|
|
142
|
+
--output-format stream-json --verbose \
|
|
143
|
+
-p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE"
|
|
144
|
+
else
|
|
145
|
+
"$REPO_DIR/scripts/run_claude.sh" "linkedin-presence" \
|
|
146
|
+
--strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" \
|
|
147
|
+
--output-format stream-json --verbose \
|
|
148
|
+
-p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE"
|
|
149
|
+
fi
|
|
150
|
+
PRESENCE_RC=${PIPESTATUS[0]}
|
|
151
|
+
set -e
|
|
152
|
+
|
|
153
|
+
release_lock "linkedin-browser"
|
|
154
|
+
|
|
155
|
+
ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
156
|
+
COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" \
|
|
157
|
+
--since "$RUN_START" --scripts "linkedin-presence" 2>/dev/null || echo "0.0000")
|
|
158
|
+
|
|
159
|
+
FAILED=0
|
|
160
|
+
FAILURE_REASONS=""
|
|
161
|
+
if grep -q "SESSION_INVALID" "$LOG_FILE" 2>/dev/null; then
|
|
162
|
+
FAILED=1
|
|
163
|
+
FAILURE_REASONS="session_invalid:1"
|
|
164
|
+
elif [ "$PRESENCE_RC" -ne 0 ]; then
|
|
165
|
+
FAILED=1
|
|
166
|
+
if [ "$PRESENCE_RC" = "124" ]; then
|
|
167
|
+
FAILURE_REASONS="timeout:1"
|
|
168
|
+
else
|
|
169
|
+
FAILURE_REASONS="claude_or_browser_error:1"
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "presence_linkedin" \
|
|
174
|
+
--posted 0 --skipped 0 --failed "$FAILED" \
|
|
175
|
+
--cost "$COST" --elapsed "$ELAPSED" \
|
|
176
|
+
--scanned 1 --checked 1 \
|
|
177
|
+
--scan "pages=1,scrolls=$SCROLLS" \
|
|
178
|
+
${FAILURE_REASONS:+--failure-reasons "$FAILURE_REASONS"} \
|
|
179
|
+
2>/dev/null || true
|
|
180
|
+
|
|
181
|
+
find "$LOG_DIR" -name "linkedin-presence-*.log" -mtime +14 -delete 2>/dev/null || true
|
|
182
|
+
log "=== LinkedIn presence complete: $(date) rc=$PRESENCE_RC failed=$FAILED ==="
|