@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,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dm-outreach-linkedin.sh — Outbound LinkedIn DM outreach.
|
|
3
|
+
# Scans for DM candidates (users who engaged on our posts), then sends LinkedIn
|
|
4
|
+
# messages to continue the conversation. Inbound DM replies are handled separately
|
|
5
|
+
# by engage-dm-replies-linkedin.sh.
|
|
6
|
+
# Called by launchd (com.m13v.social-dm-outreach-linkedin) every 6 hours.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# LinkedIn killswitch (2026-05-27): refuse to run if a prior fire detected
|
|
11
|
+
# session compromise (http_999, authwall, throttle, li_at cleared).
|
|
12
|
+
# State: ~/.claude/social-autoposter/linkedin.killswitch
|
|
13
|
+
# Clear: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear
|
|
14
|
+
if [ -f "$HOME/.claude/social-autoposter/linkedin.killswitch" ]; then
|
|
15
|
+
echo "[$(date +%H:%M:%S)] LINKEDIN_KILLSWITCH active. Aborting LinkedIn pipeline."
|
|
16
|
+
echo " Re-auth LinkedIn in harness Chrome, then: python3 ~/social-autoposter/scripts/linkedin_killswitch.py clear"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Cycle ID for cross-cycle cost accounting (see run-linkedin.sh / engage-linkedin.sh
|
|
21
|
+
# for the same pattern). Stamps claude_sessions.cycle_id via env inheritance.
|
|
22
|
+
BATCH_ID="${BATCH_ID:-dmli-$(date +%Y%m%d-%H%M%S)}"
|
|
23
|
+
export BATCH_ID
|
|
24
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
25
|
+
|
|
26
|
+
# Browser-profile lock first (shared with other linkedin pipelines), then pipeline lock.
|
|
27
|
+
source "$(dirname "$0")/lock.sh"
|
|
28
|
+
# Browser backend bootstrap (linkedin-harness). Sets MCP_CONFIG_FILE,
|
|
29
|
+
# BROWSER_INSTRUCTIONS, exports LINKEDIN_CDP_URL, and provides
|
|
30
|
+
# ensure_linkedin_browser_for_backend. Migrated off the deprecated
|
|
31
|
+
# mcp__linkedin-agent Playwright MCP to the CDP-driven harness Chrome (port 9556).
|
|
32
|
+
source "$(dirname "$0")/lib/linkedin-backend.sh"
|
|
33
|
+
acquire_lock "linkedin-browser" 3600
|
|
34
|
+
ensure_linkedin_browser_for_backend
|
|
35
|
+
acquire_lock "dm-outreach-linkedin" 2700
|
|
36
|
+
|
|
37
|
+
# Load secrets
|
|
38
|
+
# shellcheck source=/dev/null
|
|
39
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
40
|
+
|
|
41
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
42
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
43
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
44
|
+
|
|
45
|
+
mkdir -p "$LOG_DIR"
|
|
46
|
+
LOG_FILE="$LOG_DIR/dm-outreach-linkedin-$(date +%Y-%m-%d_%H%M%S).log"
|
|
47
|
+
|
|
48
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
49
|
+
|
|
50
|
+
RUN_START=$(date +%s)
|
|
51
|
+
log "=== LinkedIn DM Outreach Run: $(date) ==="
|
|
52
|
+
|
|
53
|
+
# DB-free since 2026-06-01: all DM state goes through the s4l.ai HTTP API
|
|
54
|
+
# (X-Installation auth). No DATABASE_URL needed.
|
|
55
|
+
PY_BIN="$(command -v python3 || echo /usr/bin/python3)"
|
|
56
|
+
|
|
57
|
+
# dm_count <status> -> integer count of linkedin dms in that status.
|
|
58
|
+
# Backed by GET /api/v1/dms/counts (same shape as /api/v1/replies/counts).
|
|
59
|
+
dm_count() {
|
|
60
|
+
"$PY_BIN" -c "
|
|
61
|
+
import sys; sys.path.insert(0, '$REPO_DIR/scripts')
|
|
62
|
+
from http_api import api_get
|
|
63
|
+
resp = api_get('/api/v1/dms/counts', {'platform': 'linkedin'})
|
|
64
|
+
counts = ((resp or {}).get('data') or {}).get('counts') or []
|
|
65
|
+
want = '$1'
|
|
66
|
+
print(next((int(r.get('count', 0)) for r in counts if r.get('status') == want), 0))
|
|
67
|
+
" 2>/dev/null || echo 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Scan for new DM candidates first (cheap Python, writes to dms table)
|
|
71
|
+
log "Scanning for DM candidates (all platforms)..."
|
|
72
|
+
(PYTHONUNBUFFERED=1 python3 "$REPO_DIR/scripts/scan_dm_candidates.py" 2>&1 || true) | tee -a "$LOG_FILE"
|
|
73
|
+
|
|
74
|
+
DM_PENDING=$(dm_count pending)
|
|
75
|
+
|
|
76
|
+
if [ "$DM_PENDING" -eq 0 ]; then
|
|
77
|
+
log "No pending LinkedIn DMs"
|
|
78
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_linkedin" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
log "LinkedIn: $DM_PENDING DMs to send"
|
|
83
|
+
|
|
84
|
+
# Pull the pending DM batch + 60-day cross-thread engagement via the
|
|
85
|
+
# outreach-queue endpoint (no DATABASE_URL). The route mirrors the old
|
|
86
|
+
# json_agg join exactly and returns {rows:[...]}. We extract the array so
|
|
87
|
+
# DM_DATA keeps the same JSON shape the prompt consumed before the migration.
|
|
88
|
+
DM_DATA=$("$PY_BIN" -c "
|
|
89
|
+
import json, sys
|
|
90
|
+
sys.path.insert(0, '$REPO_DIR/scripts')
|
|
91
|
+
from http_api import api_get
|
|
92
|
+
resp = api_get('/api/v1/dms/outreach-queue', {'platform': 'linkedin', 'status': 'pending', 'limit': 200})
|
|
93
|
+
rows = (resp.get('data') or {}).get('rows') or []
|
|
94
|
+
print(json.dumps(rows))
|
|
95
|
+
" 2>/dev/null || echo "[]")
|
|
96
|
+
|
|
97
|
+
# Per-project qualification context for ICP pre-check
|
|
98
|
+
PROJECTS_QUALIFICATION=$(python3 -c "
|
|
99
|
+
import json
|
|
100
|
+
c = json.load(open('$REPO_DIR/config.json'))
|
|
101
|
+
for p in c.get('projects', []):
|
|
102
|
+
q = p.get('qualification') or {}
|
|
103
|
+
if not q:
|
|
104
|
+
continue
|
|
105
|
+
print(f\"- {p['name']}:\")
|
|
106
|
+
if q.get('must_have'):
|
|
107
|
+
print(f\" must_have: {' ; '.join(q['must_have'])}\")
|
|
108
|
+
if q.get('disqualify'):
|
|
109
|
+
print(f\" disqualify: {' ; '.join(q['disqualify'])}\")
|
|
110
|
+
" 2>/dev/null || echo "")
|
|
111
|
+
|
|
112
|
+
export CLAUDE_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
|
|
113
|
+
|
|
114
|
+
PROMPT_FILE=$(mktemp)
|
|
115
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
116
|
+
You are the Social Autoposter LinkedIn DM outreach bot.
|
|
117
|
+
|
|
118
|
+
$BROWSER_INSTRUCTIONS
|
|
119
|
+
|
|
120
|
+
Read $SKILL_FILE for content rules (tone, anti-AI detection, no em dashes).
|
|
121
|
+
|
|
122
|
+
## Task: Send LinkedIn messages to continue comment conversations
|
|
123
|
+
|
|
124
|
+
These users engaged with our LinkedIn posts/comments. We already replied publicly. Now send a short, casual message to continue the conversation.
|
|
125
|
+
|
|
126
|
+
CRITICAL RULES:
|
|
127
|
+
1. Messages must feel like a natural continuation of the comment discussion, NOT a cold outreach or sales pitch
|
|
128
|
+
2. Reference the specific conversation topic, not generic "hey I saw your comment"
|
|
129
|
+
3. Keep it short: 1-2 sentences max, like a text message
|
|
130
|
+
4. No links in the first DM; earn the conversation first
|
|
131
|
+
5. No em dashes. Write casually, like texting a coworker.
|
|
132
|
+
|
|
133
|
+
## COMMITMENT GUARDRAILS (violating any of these is a critical failure)
|
|
134
|
+
- **NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.** Keep everything in the DM thread.
|
|
135
|
+
- **NEVER agree to podcast appearances, X Spaces, interviews, or live events.**
|
|
136
|
+
- **NEVER offer to move the conversation to another platform** (Telegram, Discord, email, etc.). Stay in the current DM thread.
|
|
137
|
+
- **NEVER promise to share specific links, files, or resources you don't have right now.** If you don't have it in config.json projects, don't promise it.
|
|
138
|
+
- **NEVER make time-bound commitments** ("this week", "tomorrow", "Thursday"). Don't commit the human to any schedule.
|
|
139
|
+
- **NEVER say "I'm in [city]"** or share location/personal details not in config.json.
|
|
140
|
+
- If someone asks for any of the above, respond naturally but deflect: keep the conversation going in the DM without making promises. Example: "honestly easier to hash it out here, what specifically are you trying to set up?"
|
|
141
|
+
|
|
142
|
+
DM EXAMPLES (good):
|
|
143
|
+
- "yo your point about token costs scaling with agent count hit home, we're dealing with the exact same thing. what's your setup look like?"
|
|
144
|
+
- "that workaround you mentioned for the accessibility API crash is clever, did it hold up in production?"
|
|
145
|
+
- "curious how you ended up going with that approach for the MCP server, we tried something similar"
|
|
146
|
+
|
|
147
|
+
DM EXAMPLES (bad):
|
|
148
|
+
- "Hey! I noticed your comment on LinkedIn. I'm building something you might find interesting..." (cold pitch)
|
|
149
|
+
- "Great point! I'd love to connect and share what we're working on." (generic)
|
|
150
|
+
- "Hi there, I saw your insightful comment about AI agents..." (too formal)
|
|
151
|
+
|
|
152
|
+
## Users to DM:
|
|
153
|
+
$DM_DATA
|
|
154
|
+
|
|
155
|
+
## Cross-thread engagement awareness
|
|
156
|
+
Each row may include an \`other_engagement\` array: this user's other recent (60-day) interactions with our posts on the same platform. Each entry has thread_title, their_content snippet, our_reply_content snippet, depth (>1 = public follow-up to our reply in a thread), status, replied_at.
|
|
157
|
+
|
|
158
|
+
Use it as context for the DM:
|
|
159
|
+
- If the most recent other_engagement entry is on the SAME thread with depth>1 and replied_at < 6 hours ago, they're actively continuing the public conversation. Prefer a lighter-touch DM, or open with an acknowledgment of the ongoing thread instead of introducing a new angle.
|
|
160
|
+
- If they've engaged on multiple other threads, it signals genuine interest. The DM can be slightly more direct without feeling cold.
|
|
161
|
+
- Do NOT quote their other comments back at them or enumerate their history. It's context, not content.
|
|
162
|
+
|
|
163
|
+
## Per-project ICP criteria (used for the pre-check step, NOT to skip sending):
|
|
164
|
+
$PROJECTS_QUALIFICATION
|
|
165
|
+
|
|
166
|
+
## Pre-send profile fetch + ICP pre-check (MANDATORY per DM, no filter)
|
|
167
|
+
|
|
168
|
+
For each DM row, BEFORE you compose or send, do this in order. USE the bh_run tool from the BROWSER BACKEND block ONLY (follow its translation table for any Playwright-style step below); NEVER call /voyager/api/; NEVER run Python CDP scripts against LinkedIn.
|
|
169
|
+
|
|
170
|
+
1. Look at the row's \`target_project\`. If it's NULL, set icp_precheck=unknown with notes="no_target_project" and proceed to step 4 — but still try to capture profile basics.
|
|
171
|
+
|
|
172
|
+
2. Fetch the prospect's LinkedIn profile:
|
|
173
|
+
- From the original comment thread (r.their_comment_url), click into THEIR_AUTHOR's profile link, OR
|
|
174
|
+
- Search LinkedIn for THEIR_AUTHOR from the messaging UI once you have them open.
|
|
175
|
+
- Read their profile header DOM (bh_run with js(...) per the translation table, or capture_screenshot + Read the PNG). Extract: headline, current company, current role, a short summary of their About/experience top section, and (if visible) 1-2 recent posts/activity items.
|
|
176
|
+
- If you hit a login/checkpoint, STOP and print SESSION_INVALID; do NOT attempt to log in.
|
|
177
|
+
- If the profile is private or shows only a minimal header, record what you can and note "profile_limited".
|
|
178
|
+
|
|
179
|
+
3. Persist the profile fields:
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
python3 $REPO_DIR/scripts/fetch_prospect_profile.py upsert \\
|
|
182
|
+
--platform linkedin --author "THEIR_AUTHOR" \\
|
|
183
|
+
--profile-url "PROFILE_URL" \\
|
|
184
|
+
--headline "HEADLINE_FROM_PROFILE" \\
|
|
185
|
+
--company "CURRENT_COMPANY" \\
|
|
186
|
+
--role "CURRENT_ROLE" \\
|
|
187
|
+
--bio "SHORT_ABOUT_OR_SUMMARY" \\
|
|
188
|
+
--recent-activity "1-2 LINE RECENT ACTIVITY SUMMARY" \\
|
|
189
|
+
--notes "ANY_SIGNAL_WORTH_REMEMBERING" \\
|
|
190
|
+
--link-dm DM_ID
|
|
191
|
+
\`\`\`
|
|
192
|
+
Omit any flag whose value is empty or unknown. \`--link-dm\` also wires dms.prospect_id.
|
|
193
|
+
|
|
194
|
+
4. Evaluate ICP match against EVERY project listed in "Per-project ICP criteria" above (not only target_project). For each project compare the profile + their_content + comment_context against its must_have (satisfy at least one) and disqualify (trigger ANY = fail), and pick one label: icp_match, icp_miss, disqualified, or unknown. Upsert one entry per project:
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
python3 $REPO_DIR/scripts/dm_conversation.py set-icp-precheck \\
|
|
197
|
+
--dm-id DM_ID --project PROJECT_NAME --label LABEL --notes "SHORT_RATIONALE"
|
|
198
|
+
\`\`\`
|
|
199
|
+
Run this once per project from the list. Each call upserts one entry in dms.icp_matches (JSONB array) keyed by project.
|
|
200
|
+
|
|
201
|
+
5. If ANY entry in icp_matches has label=disqualified, skip the send: run \`python3 scripts/dm_conversation.py mark-skipped --dm-id DM_ID --reason "disqualified: PROJECT - SHORT_NOTES"\` and move on. \`icp_miss\` alone does NOT gate; send when every project scored miss. Only explicit \`disqualified\` blocks the opener.
|
|
202
|
+
|
|
203
|
+
## How to send messages on LinkedIn (use the bh_run tool):
|
|
204
|
+
1. Navigate to https://www.linkedin.com/messaging/ (bh_run: new_tab/goto_url + wait_for_load)
|
|
205
|
+
2. Start new message to THEIR_AUTHOR
|
|
206
|
+
3. Type and send the message. Click the message box (click_at_xy) then type_text; click the Send button via click_at_xy. Do NOT press Enter (Enter inserts a newline in LinkedIn's contenteditable).
|
|
207
|
+
|
|
208
|
+
## After each DM:
|
|
209
|
+
|
|
210
|
+
Inspect the send result (capture_screenshot + Read the PNG to confirm the message appeared in the thread). There are exactly three outcomes:
|
|
211
|
+
|
|
212
|
+
(A) The message was actually delivered (you saw it appear in the thread, no error toast) -> mark sent via the verified gateway:
|
|
213
|
+
CLAUDE_SESSION_ID=$CLAUDE_SESSION_ID python3 $REPO_DIR/scripts/dm_send_log.py \\
|
|
214
|
+
--dm-id DM_ID --message "DM_TEXT" --verified
|
|
215
|
+
|
|
216
|
+
Do NOT issue a raw "UPDATE dms SET status='sent'" psql command and do NOT call
|
|
217
|
+
dm_conversation.py log-outbound directly. dm_send_log.py is the ONLY path that
|
|
218
|
+
may flip status to 'sent'; it requires --verified, and refuses without it. It
|
|
219
|
+
also forwards to log-outbound internally with --verified, so dm_messages stays
|
|
220
|
+
in sync. This is intentional: prior phantom-DM bugs (April 2026 LinkedIn Haiku
|
|
221
|
+
cycle inserted 5 phantom outbound rows because the prompt let the LLM call
|
|
222
|
+
log-outbound without a verified send) came from bypassing this gateway.
|
|
223
|
+
|
|
224
|
+
(B) The send did not land (no toast, message did not appear in thread) -> mark error:
|
|
225
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status error --skip-reason send_unverified --claude-session-id "$CLAUDE_SESSION_ID"
|
|
226
|
+
|
|
227
|
+
(C) DMs disabled / chat blocked -> mark skipped:
|
|
228
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status skipped --skip-reason chat_disabled --claude-session-id "$CLAUDE_SESSION_ID"
|
|
229
|
+
|
|
230
|
+
(D) Rate limit, account checkpoint, or any other thrown exception -> mark error and STOP the run:
|
|
231
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status error --skip-reason "REASON" --claude-session-id "$CLAUDE_SESSION_ID"
|
|
232
|
+
|
|
233
|
+
CRITICAL: ALL browser calls MUST use the mcp__linkedin-harness__bh_run tool (the BROWSER BACKEND block above). NEVER use generic mcp__playwright-extension__*, mcp__isolated-browser__*, or mcp__macos-use__* tools. If a bh_run call is blocked or times out, wait 30 seconds and retry (up to 3 times). Do NOT fall back to any other browser tool.
|
|
234
|
+
PROMPT_EOF
|
|
235
|
+
|
|
236
|
+
ensure_linkedin_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
237
|
+
gtimeout 2700 "$REPO_DIR/scripts/run_claude.sh" "dm-outreach-linkedin" --strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" --output-format stream-json --verbose -p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: LinkedIn DM outreach claude exited with code $?"
|
|
238
|
+
rm -f "$PROMPT_FILE"
|
|
239
|
+
|
|
240
|
+
SENT=$(dm_count sent)
|
|
241
|
+
STILL_PENDING=$(dm_count pending)
|
|
242
|
+
log "LinkedIn DM outreach summary: sent (all-time)=$SENT, still_pending=$STILL_PENDING"
|
|
243
|
+
|
|
244
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
245
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "dm-outreach-linkedin" 2>/dev/null || echo "0.0000")
|
|
246
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_linkedin" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
247
|
+
|
|
248
|
+
find "$LOG_DIR" -name "dm-outreach-linkedin-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
249
|
+
|
|
250
|
+
log "=== LinkedIn DM outreach complete: $(date) ==="
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dm-outreach-reddit.sh — Outbound Reddit DM outreach.
|
|
3
|
+
# Scans for DM candidates (users who engaged on our posts), then sends Reddit DMs
|
|
4
|
+
# to continue the conversation. Inbound DM replies are handled separately by
|
|
5
|
+
# engage-dm-replies-reddit.sh.
|
|
6
|
+
# Called by launchd (com.m13v.social-dm-outreach-reddit) every 6 hours.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# Cycle ID for cross-cycle cost accounting (matches the pattern in
|
|
11
|
+
# run-reddit-search.sh, engage-reddit.sh, etc.). Every claude session spawned
|
|
12
|
+
# in this script inherits SA_CYCLE_ID via env so log_claude_session.py stamps
|
|
13
|
+
# claude_sessions.cycle_id. Lets get_run_cost.py --cycle-id report THIS run's
|
|
14
|
+
# spend instead of bleeding into overlapping outreach cycles.
|
|
15
|
+
BATCH_ID="${BATCH_ID:-dmrd-$(date +%Y%m%d-%H%M%S)}"
|
|
16
|
+
export BATCH_ID
|
|
17
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
18
|
+
|
|
19
|
+
# Pipeline lock at top (only-one-of-us guard). We DO NOT acquire
|
|
20
|
+
# reddit-browser at the bash level anymore — claude itself acquires it
|
|
21
|
+
# per-DM via scripts/reddit_browser_lock.py, only around the actual MCP
|
|
22
|
+
# browser operations (profile fetch + compose DM, ~30-90s per DM). This
|
|
23
|
+
# unblocks peer reddit pipelines (engage-reddit, dm-replies-reddit,
|
|
24
|
+
# link-edit-reddit, post-reddit) during the DB scan, prompt build, and
|
|
25
|
+
# HTTP PATCH update phases of each DM row.
|
|
26
|
+
source "$(dirname "$0")/lock.sh"
|
|
27
|
+
# reddit-harness backend (2026-05-29). Sets MCP_CONFIG_FILE (reddit-harness MCP),
|
|
28
|
+
# BROWSER_INSTRUCTIONS (bh_run tool surface + translation table), exports
|
|
29
|
+
# REDDIT_CDP_URL=:9557, and provides ensure_reddit_browser_for_backend.
|
|
30
|
+
# Source after lock.sh, before acquire_lock / claude -p.
|
|
31
|
+
source "$(dirname "$0")/lib/reddit-backend.sh"
|
|
32
|
+
acquire_lock "dm-outreach-reddit" 2700
|
|
33
|
+
|
|
34
|
+
# Load secrets
|
|
35
|
+
# shellcheck source=/dev/null
|
|
36
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
37
|
+
|
|
38
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
39
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
40
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
41
|
+
|
|
42
|
+
# 2026-05-12 migration: bash-level DB access moved off psql / DATABASE_URL
|
|
43
|
+
# onto HTTP routes (/api/v1/dms*, /api/v1/dms/outreach-queue). The shell no
|
|
44
|
+
# longer needs Postgres credentials; everything flows through
|
|
45
|
+
# scripts/dm_outreach_helper.py which calls the website.
|
|
46
|
+
# DATABASE_URL may still be defined in .env for other tooling; we don't
|
|
47
|
+
# require it here. The Python http_api layer expects AUTOPOSTER_API_BASE
|
|
48
|
+
# (defaults to https://s4l.ai) and an installation identity header.
|
|
49
|
+
|
|
50
|
+
mkdir -p "$LOG_DIR"
|
|
51
|
+
LOG_FILE="$LOG_DIR/dm-outreach-reddit-$(date +%Y-%m-%d_%H%M%S).log"
|
|
52
|
+
|
|
53
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
54
|
+
|
|
55
|
+
RUN_START=$(date +%s)
|
|
56
|
+
log "=== Reddit DM Outreach Run: $(date) ==="
|
|
57
|
+
|
|
58
|
+
# Scan for new DM candidates first (cheap Python, writes to dms table)
|
|
59
|
+
log "Scanning for DM candidates (all platforms)..."
|
|
60
|
+
(PYTHONUNBUFFERED=1 python3 "$REPO_DIR/scripts/scan_dm_candidates.py" 2>&1 || true) | tee -a "$LOG_FILE"
|
|
61
|
+
|
|
62
|
+
DM_PENDING=$(python3 "$REPO_DIR/scripts/dm_outreach_helper.py" count --platform reddit --status pending 2>/dev/null || echo "0")
|
|
63
|
+
|
|
64
|
+
if [ "$DM_PENDING" -eq 0 ]; then
|
|
65
|
+
log "No pending Reddit DMs"
|
|
66
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_reddit" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
log "Reddit: $DM_PENDING DMs to send"
|
|
71
|
+
|
|
72
|
+
DM_DATA=$(python3 "$REPO_DIR/scripts/dm_outreach_helper.py" outreach-queue \
|
|
73
|
+
--platform reddit --status pending --limit 200 \
|
|
74
|
+
--other-engagement-days 60 2>/dev/null || echo "[]")
|
|
75
|
+
|
|
76
|
+
# Per-project qualification context for ICP pre-check
|
|
77
|
+
PROJECTS_QUALIFICATION=$(python3 -c "
|
|
78
|
+
import json
|
|
79
|
+
c = json.load(open('$REPO_DIR/config.json'))
|
|
80
|
+
for p in c.get('projects', []):
|
|
81
|
+
q = p.get('qualification') or {}
|
|
82
|
+
if not q:
|
|
83
|
+
continue
|
|
84
|
+
print(f\"- {p['name']}:\")
|
|
85
|
+
if q.get('must_have'):
|
|
86
|
+
print(f\" must_have: {' ; '.join(q['must_have'])}\")
|
|
87
|
+
if q.get('disqualify'):
|
|
88
|
+
print(f\" disqualify: {' ; '.join(q['disqualify'])}\")
|
|
89
|
+
" 2>/dev/null || echo "")
|
|
90
|
+
|
|
91
|
+
export CLAUDE_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
|
|
92
|
+
|
|
93
|
+
PROMPT_FILE=$(mktemp)
|
|
94
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
95
|
+
You are the Social Autoposter Reddit DM outreach bot.
|
|
96
|
+
|
|
97
|
+
Read $SKILL_FILE for content rules (tone, anti-AI detection, no em dashes).
|
|
98
|
+
|
|
99
|
+
$BROWSER_INSTRUCTIONS
|
|
100
|
+
|
|
101
|
+
## Task: Send Reddit DMs to continue comment conversations
|
|
102
|
+
|
|
103
|
+
These users engaged with our Reddit posts/comments. We already replied publicly. Now send a short, casual DM (Reddit Chat) to continue the conversation.
|
|
104
|
+
|
|
105
|
+
CRITICAL RULES:
|
|
106
|
+
1. DMs must feel like a natural continuation of the comment discussion, NOT a cold outreach or sales pitch
|
|
107
|
+
2. Reference the specific conversation topic, not generic "hey I saw your comment"
|
|
108
|
+
3. Keep it short: 1-2 sentences max, like a text message
|
|
109
|
+
4. No links in the first DM; earn the conversation first
|
|
110
|
+
5. No em dashes. Write casually, like texting a coworker.
|
|
111
|
+
|
|
112
|
+
EXECUTION MODEL — STRICT SEQUENTIAL, NO BATCHING (read this twice):
|
|
113
|
+
- Process DMs ONE AT A TIME. Run the FULL chain (profile fetch → ICP pre-check → compose → send → log) end-to-end for DM N before reading DM N+1.
|
|
114
|
+
- The reddit-browser lock is NOT held by the parent shell. You acquire/release it explicitly per DM (steps 1.5 and 7.5 below), so peer reddit pipelines can use the browser during your DB queries, ICP scoring, and dm_outreach_helper.py PATCH calls.
|
|
115
|
+
- The lock has a 90s lease that auto-renews on every reddit-agent MCP call (PreToolUse / PostToolUse hooks), so you do NOT need to manually heartbeat. Just acquire before the first browser call for the DM, release after the last one.
|
|
116
|
+
|
|
117
|
+
## COMMITMENT GUARDRAILS (violating any of these is a critical failure)
|
|
118
|
+
- **NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.** Keep everything in the DM thread.
|
|
119
|
+
- **NEVER agree to podcast appearances, X Spaces, interviews, or live events.**
|
|
120
|
+
- **NEVER offer to move the conversation to another platform** (Telegram, Discord, email, etc.). Stay in the current DM thread.
|
|
121
|
+
- **NEVER promise to share specific links, files, or resources you don't have right now.** If you don't have it in config.json projects, don't promise it.
|
|
122
|
+
- **NEVER make time-bound commitments** ("this week", "tomorrow", "Thursday"). Don't commit the human to any schedule.
|
|
123
|
+
- **NEVER say "I'm in [city]"** or share location/personal details not in config.json.
|
|
124
|
+
- If someone asks for any of the above, respond naturally but deflect: keep the conversation going in the DM without making promises. Example: "honestly easier to hash it out here, what specifically are you trying to set up?"
|
|
125
|
+
|
|
126
|
+
DM EXAMPLES (good):
|
|
127
|
+
- "yo your point about token costs scaling with agent count hit home, we're dealing with the exact same thing. what's your setup look like?"
|
|
128
|
+
- "that workaround you mentioned for the accessibility API crash is clever, did it hold up in production?"
|
|
129
|
+
- "curious how you ended up going with that approach for the MCP server, we tried something similar"
|
|
130
|
+
|
|
131
|
+
DM EXAMPLES (bad):
|
|
132
|
+
- "Hey! I noticed your comment on Reddit. I'm building something you might find interesting..." (cold pitch)
|
|
133
|
+
- "Great point! I'd love to connect and share what we're working on." (generic)
|
|
134
|
+
- "Hi there - I saw your insightful comment about AI agents..." (too formal)
|
|
135
|
+
|
|
136
|
+
## Users to DM:
|
|
137
|
+
$DM_DATA
|
|
138
|
+
|
|
139
|
+
## Cross-thread engagement awareness
|
|
140
|
+
Each row may include an \`other_engagement\` array: this user's other recent (60-day) interactions with our posts on the same platform. Each entry has thread_title, their_content snippet, our_reply_content snippet, depth (>1 = public follow-up to our reply in a thread), status, replied_at.
|
|
141
|
+
|
|
142
|
+
Use it as context for the DM:
|
|
143
|
+
- If the most recent other_engagement entry is on the SAME thread with depth>1 and replied_at < 6 hours ago, they're actively continuing the public conversation. Prefer a lighter-touch DM, or open with an acknowledgment of the ongoing thread instead of introducing a new angle.
|
|
144
|
+
- If they've engaged on multiple other threads, it signals genuine interest. The DM can be slightly more direct without feeling cold.
|
|
145
|
+
- Do NOT quote their other comments back at them or enumerate their history. It's context, not content.
|
|
146
|
+
|
|
147
|
+
## Per-project ICP criteria (used for the pre-check step, NOT to skip sending):
|
|
148
|
+
$PROJECTS_QUALIFICATION
|
|
149
|
+
|
|
150
|
+
## Pre-send profile fetch + ICP pre-check (MANDATORY per DM, no filter)
|
|
151
|
+
|
|
152
|
+
For each DM row, BEFORE you compose or send, do this in order:
|
|
153
|
+
|
|
154
|
+
1. Look at the row's \`target_project\`. If it's NULL, skip the ICP evaluation (set icp_precheck=unknown with notes="no_target_project") and move to step 4 — but still fetch the profile if it's cheap.
|
|
155
|
+
|
|
156
|
+
1.5. ACQUIRE the reddit-browser lock NOW (just before any reddit-agent browser call for this DM). This is the ONLY moment you may touch the browser for this DM:
|
|
157
|
+
\`\`\`bash
|
|
158
|
+
LOCK_OUT=\$(python3 $REPO_DIR/scripts/reddit_browser_lock.py acquire --timeout 600 2>&1)
|
|
159
|
+
\`\`\`
|
|
160
|
+
- If stdout starts with "OK", proceed to step 2.
|
|
161
|
+
- If "BUSY", a peer reddit pipeline owns the browser and didn't release within 10 min. Mark this DM error/transient and move on to the NEXT DM:
|
|
162
|
+
\`python3 $REPO_DIR/scripts/dm_outreach_helper.py patch --id DM_ID --status error --skip-reason reddit_browser_busy --claude-session-id $CLAUDE_SESSION_ID\`
|
|
163
|
+
- If "ERROR", same handling as BUSY: mark error, move on. Do NOT call browser tools without the lock — collisions on the same chrome profile crash both runs.
|
|
164
|
+
|
|
165
|
+
2. Fetch the prospect's Reddit profile with the browser backend (mcp__reddit-harness__bh_run, see BROWSER BACKEND block above):
|
|
166
|
+
- Navigate to https://www.reddit.com/user/THEIR_AUTHOR/
|
|
167
|
+
- Read the page (snapshot / capture_screenshot per the translation table). Pull:
|
|
168
|
+
- the profile bio/tagline (text under their name)
|
|
169
|
+
- karma numbers (post + comment karma)
|
|
170
|
+
- a 1-2 line summary of their most recent 3-5 posts/comments (titles + subreddits)
|
|
171
|
+
- If the profile page is a login wall, deleted, or suspended user, record notes="profile_inaccessible" and proceed with icp_precheck=unknown.
|
|
172
|
+
|
|
173
|
+
3. Persist the profile fields via:
|
|
174
|
+
\`\`\`bash
|
|
175
|
+
python3 $REPO_DIR/scripts/fetch_prospect_profile.py upsert \\
|
|
176
|
+
--platform reddit --author "THEIR_AUTHOR" \\
|
|
177
|
+
--profile-url "https://www.reddit.com/user/THEIR_AUTHOR/" \\
|
|
178
|
+
--headline "SHORT_TAGLINE_OR_BIO_FIRST_LINE" \\
|
|
179
|
+
--bio "FULL_BIO_TEXT" \\
|
|
180
|
+
--recent-activity "SHORT_3-5_ITEM_SUMMARY" \\
|
|
181
|
+
--notes "ANY_SIGNAL_WORTH_REMEMBERING" \\
|
|
182
|
+
--link-dm DM_ID
|
|
183
|
+
\`\`\`
|
|
184
|
+
Omit any flag whose value is empty or unknown. \`--link-dm\` also wires dms.prospect_id.
|
|
185
|
+
|
|
186
|
+
4. Evaluate ICP match against EVERY project listed in "Per-project ICP criteria" above (not only target_project). For each project compare the profile + their_content + comment_context against its must_have (satisfy at least one) and disqualify (trigger ANY = fail), and pick one label: icp_match, icp_miss, disqualified, or unknown. Upsert one entry per project:
|
|
187
|
+
\`\`\`bash
|
|
188
|
+
python3 $REPO_DIR/scripts/dm_conversation.py set-icp-precheck \\
|
|
189
|
+
--dm-id DM_ID --project PROJECT_NAME --label LABEL --notes "SHORT_RATIONALE"
|
|
190
|
+
\`\`\`
|
|
191
|
+
Run this once per project from the list. Each call upserts one entry in dms.icp_matches (JSONB array) keyed by project.
|
|
192
|
+
|
|
193
|
+
5. If ANY entry in icp_matches has label=disqualified, skip the send: run \`python3 scripts/dm_conversation.py mark-skipped --dm-id DM_ID --reason "disqualified: PROJECT - SHORT_NOTES"\` and move on. \`icp_miss\` alone does NOT gate; send when every project scored miss. Only explicit \`disqualified\` blocks the opener.
|
|
194
|
+
|
|
195
|
+
## How to send DMs on Reddit (use the browser backend, mcp__reddit-harness__bh_run):
|
|
196
|
+
1. Navigate to https://www.reddit.com/message/compose/?to=THEIR_AUTHOR
|
|
197
|
+
2. Reddit uses Chat now. Fill in subject (2-4 casual words) and body.
|
|
198
|
+
3. Submit. The send_dm / compose_dm tool returns a JSON object with an
|
|
199
|
+
"ok" field and a "verified" field. The send only counts if BOTH are true.
|
|
200
|
+
|
|
201
|
+
## After each DM:
|
|
202
|
+
|
|
203
|
+
Inspect the tool's return value. There are exactly three outcomes:
|
|
204
|
+
|
|
205
|
+
(A) ok=true AND verified=true -> success, mark sent:
|
|
206
|
+
CLAUDE_SESSION_ID=$CLAUDE_SESSION_ID python3 $REPO_DIR/scripts/dm_send_log.py \\
|
|
207
|
+
--dm-id DM_ID --message "DM_TEXT" --verified
|
|
208
|
+
|
|
209
|
+
Do NOT use dm_outreach_helper.py to set status='sent'. The helper
|
|
210
|
+
refuses, and dm_send_log.py is the only path that may flip status to
|
|
211
|
+
'sent'; it requires --verified, and refuses without it. This is
|
|
212
|
+
intentional: prior phantom-DM bugs (~700 rows in 4/2026) came from
|
|
213
|
+
prose-driven status flips that ignored the verification result.
|
|
214
|
+
|
|
215
|
+
(B) ok=false OR verified=false -> send did not land, mark error:
|
|
216
|
+
python3 $REPO_DIR/scripts/dm_outreach_helper.py patch --id DM_ID --status error --skip-reason send_unverified --claude-session-id $CLAUDE_SESSION_ID
|
|
217
|
+
|
|
218
|
+
(C) Rate limit, account blocked, or any other thrown exception:
|
|
219
|
+
python3 $REPO_DIR/scripts/dm_outreach_helper.py patch --id DM_ID --status error --skip-reason REASON --claude-session-id $CLAUDE_SESSION_ID
|
|
220
|
+
|
|
221
|
+
DMs/Chat disabled (recipient setting, not a send failure):
|
|
222
|
+
python3 $REPO_DIR/scripts/dm_outreach_helper.py patch --id DM_ID --status skipped --skip-reason chat_disabled --claude-session-id $CLAUDE_SESSION_ID
|
|
223
|
+
|
|
224
|
+
7.5. RELEASE the reddit-browser lock IMMEDIATELY after the DM result is logged (success, error, or skip). This is mandatory — failing to release blocks every other reddit pipeline:
|
|
225
|
+
\`\`\`bash
|
|
226
|
+
python3 $REPO_DIR/scripts/reddit_browser_lock.py release
|
|
227
|
+
\`\`\`
|
|
228
|
+
Run this even if step 2 (profile fetch) raised, the send threw, or you're skipping the DM. Wrap the per-DM browser block (steps 2 → 7) in a way that step 7.5 ALWAYS executes (mental try/finally). The release is idempotent and safe to call multiple times. Move to the NEXT DM only after the release.
|
|
229
|
+
|
|
230
|
+
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 30 seconds and retry (up to 3 times). Do NOT fall back to any other browser tool.
|
|
231
|
+
PROMPT_EOF
|
|
232
|
+
|
|
233
|
+
# NOTE: We do NOT acquire reddit-browser at the bash level. Claude itself
|
|
234
|
+
# acquires/releases it per DM via scripts/reddit_browser_lock.py
|
|
235
|
+
# (steps 1.5 and 7.5 in the prompt). This keeps the lock held only during
|
|
236
|
+
# the actual ~30-90s reddit-agent browser ops per DM (profile fetch +
|
|
237
|
+
# compose), not the full ~45-min run. Peer pipelines (engage-reddit,
|
|
238
|
+
# dm-replies-reddit, link-edit-reddit, post-reddit) can use the profile
|
|
239
|
+
# during our DB queries, ICP scoring, and HTTP PATCH update phases.
|
|
240
|
+
#
|
|
241
|
+
# Pre-flight: ensure the profile isn't wedged by a prior crashed run.
|
|
242
|
+
# Unified lock (2026-05-10): brief Python acquire+release so the orphan-Chrome
|
|
243
|
+
# sweep happens once before claude starts. Python acquire honors expires_at,
|
|
244
|
+
# so a TTL-stale-but-PID-alive holder gets reclaimed automatically instead of
|
|
245
|
+
# blocking us for the full bash timeout.
|
|
246
|
+
log "Pre-flight: sweep orphan reddit-agent Chrome / playwright-mcp before handing off to claude..."
|
|
247
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 60 --ttl 30 2>&1 | tee -a "$LOG_FILE" || \
|
|
248
|
+
log "WARNING: reddit_browser_lock.py pre-flight acquire failed; proceeding (claude will retry per-DM)."
|
|
249
|
+
if ! ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE"; then
|
|
250
|
+
log "WARNING: reddit-harness bootstrap failed; falling back to ensure_browser_healthy reddit"
|
|
251
|
+
ensure_browser_healthy "reddit"
|
|
252
|
+
fi
|
|
253
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
254
|
+
|
|
255
|
+
gtimeout 2700 "$REPO_DIR/scripts/run_claude.sh" "dm-outreach-reddit" --strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" --output-format stream-json --verbose -p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: Reddit DM outreach claude exited with code $?"
|
|
256
|
+
rm -f "$PROMPT_FILE"
|
|
257
|
+
|
|
258
|
+
# Belt-and-suspenders: if claude exited without releasing the lock (e.g.
|
|
259
|
+
# crashed mid-DM before reaching step 7.5), free it now so peer
|
|
260
|
+
# pipelines aren't stuck behind a phantom holder. release_lock checks
|
|
261
|
+
# the lock_dir and rm-rf's it; safe even if claude already released.
|
|
262
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
263
|
+
|
|
264
|
+
SENT=$(python3 "$REPO_DIR/scripts/dm_outreach_helper.py" count --platform reddit --status sent 2>/dev/null || echo "0")
|
|
265
|
+
STILL_PENDING=$(python3 "$REPO_DIR/scripts/dm_outreach_helper.py" count --platform reddit --status pending 2>/dev/null || echo "0")
|
|
266
|
+
log "Reddit DM outreach summary: sent (all-time)=$SENT, still_pending=$STILL_PENDING"
|
|
267
|
+
|
|
268
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
269
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "dm-outreach-reddit" 2>/dev/null || echo "0.0000")
|
|
270
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_reddit" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
271
|
+
|
|
272
|
+
find "$LOG_DIR" -name "dm-outreach-reddit-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
273
|
+
|
|
274
|
+
log "=== Reddit DM outreach complete: $(date) ==="
|