@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,265 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dm-outreach-twitter.sh — Outbound Twitter/X DM outreach.
|
|
3
|
+
# Scans for DM candidates (users who engaged on our posts), then sends Twitter DMs
|
|
4
|
+
# to continue the conversation. Inbound DM replies are handled separately
|
|
5
|
+
# by engage-dm-replies-twitter.sh.
|
|
6
|
+
# Called by launchd (com.m13v.social-dm-outreach-twitter) every 6 hours.
|
|
7
|
+
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# Cycle ID for cross-cycle cost accounting (see run-twitter-cycle.sh for the
|
|
11
|
+
# same pattern). Stamps claude_sessions.cycle_id so get_run_cost.py --cycle-id
|
|
12
|
+
# returns just this cycle's spend.
|
|
13
|
+
BATCH_ID="${BATCH_ID:-dmtw-$(date +%Y%m%d-%H%M%S)}"
|
|
14
|
+
export BATCH_ID
|
|
15
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
16
|
+
|
|
17
|
+
# Bootstrap log paths early so the singleton-cleanup output below gets captured
|
|
18
|
+
# in the same log file the rest of the run uses.
|
|
19
|
+
LOG_DIR="$HOME/social-autoposter/skill/logs"
|
|
20
|
+
mkdir -p "$LOG_DIR"
|
|
21
|
+
LOG_FILE="$LOG_DIR/dm-outreach-twitter-$(date +%Y-%m-%d_%H%M%S).log"
|
|
22
|
+
|
|
23
|
+
# Browser-profile lock first (shared with other twitter pipelines), then pipeline lock.
|
|
24
|
+
source "$(dirname "$0")/lock.sh"
|
|
25
|
+
# Harness-only browser bootstrap (twitter-agent path fully removed 2026-05-19).
|
|
26
|
+
# Sets MCP_CONFIG_FILE, BROWSER_INSTRUCTIONS, exports TWITTER_CDP_URL=9555.
|
|
27
|
+
source "$(dirname "$0")/lib/twitter-backend.sh"
|
|
28
|
+
|
|
29
|
+
acquire_lock "twitter-browser" 3600
|
|
30
|
+
ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
31
|
+
acquire_lock "dm-outreach-twitter" 2700
|
|
32
|
+
|
|
33
|
+
# Load secrets
|
|
34
|
+
# shellcheck source=/dev/null
|
|
35
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
36
|
+
|
|
37
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
38
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
39
|
+
|
|
40
|
+
# 2026-06-02: removed the vestigial DATABASE_URL gate. This rail talks to the
|
|
41
|
+
# central store exclusively through the S4L HTTP API (scan_dm_candidates.py,
|
|
42
|
+
# dm_outreach_twitter_helper.py, log_run.py all use scripts/http_api.py). No
|
|
43
|
+
# direct Postgres connection is opened here, matching dm-outreach-reddit.sh and
|
|
44
|
+
# dm-outreach-linkedin.sh (migrated 2026-05-12).
|
|
45
|
+
# (LOG_DIR/LOG_FILE bootstrapped at top of script.)
|
|
46
|
+
|
|
47
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
48
|
+
|
|
49
|
+
RUN_START=$(date +%s)
|
|
50
|
+
log "=== Twitter DM Outreach Run: $(date) ==="
|
|
51
|
+
|
|
52
|
+
# Scan for new DM candidates first (cheap Python, writes to dms table)
|
|
53
|
+
log "Scanning for DM candidates (all platforms)..."
|
|
54
|
+
(PYTHONUNBUFFERED=1 python3 "$REPO_DIR/scripts/scan_dm_candidates.py" 2>&1 || true) | tee -a "$LOG_FILE"
|
|
55
|
+
|
|
56
|
+
DM_PENDING=$(python3 "$REPO_DIR/scripts/dm_outreach_twitter_helper.py" pending-count 2>/dev/null || echo "0")
|
|
57
|
+
|
|
58
|
+
if [ "$DM_PENDING" -eq 0 ]; then
|
|
59
|
+
log "No pending Twitter DMs"
|
|
60
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_twitter" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
log "Twitter: $DM_PENDING DMs to send"
|
|
65
|
+
|
|
66
|
+
# Prompt-feed JSON now comes from /api/v1/dms/outreach-queue (same
|
|
67
|
+
# correlated other_engagement subquery, same 60-day window, same join
|
|
68
|
+
# graph). Helper canonicalises platform=twitter → 'x' to match the dms
|
|
69
|
+
# table's stored value.
|
|
70
|
+
DM_DATA=$(python3 "$REPO_DIR/scripts/dm_outreach_twitter_helper.py" outreach-queue 2>/dev/null || echo "[]")
|
|
71
|
+
|
|
72
|
+
# Per-project qualification context for ICP pre-check
|
|
73
|
+
PROJECTS_QUALIFICATION=$(python3 -c "
|
|
74
|
+
import json
|
|
75
|
+
c = json.load(open('$REPO_DIR/config.json'))
|
|
76
|
+
for p in c.get('projects', []):
|
|
77
|
+
q = p.get('qualification') or {}
|
|
78
|
+
if not q:
|
|
79
|
+
continue
|
|
80
|
+
print(f\"- {p['name']}:\")
|
|
81
|
+
if q.get('must_have'):
|
|
82
|
+
print(f\" must_have: {' ; '.join(q['must_have'])}\")
|
|
83
|
+
if q.get('disqualify'):
|
|
84
|
+
print(f\" disqualify: {' ; '.join(q['disqualify'])}\")
|
|
85
|
+
" 2>/dev/null || echo "")
|
|
86
|
+
|
|
87
|
+
export CLAUDE_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
|
|
88
|
+
|
|
89
|
+
PROMPT_FILE=$(mktemp)
|
|
90
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
91
|
+
You are the Social Autoposter Twitter/X DM outreach bot.
|
|
92
|
+
|
|
93
|
+
Read $SKILL_FILE for content rules (tone, anti-AI detection, no em dashes).
|
|
94
|
+
|
|
95
|
+
## Task: Send Twitter/X DMs to continue comment conversations
|
|
96
|
+
|
|
97
|
+
These users engaged with our Twitter/X posts/comments. We already replied publicly. Now send a short, casual DM to continue the conversation.
|
|
98
|
+
|
|
99
|
+
CRITICAL RULES:
|
|
100
|
+
1. DMs must feel like a natural continuation of the comment discussion, NOT a cold outreach or sales pitch
|
|
101
|
+
2. Reference the specific conversation topic, not generic "hey I saw your comment"
|
|
102
|
+
3. Keep it short: 1-2 sentences max, like a text message
|
|
103
|
+
4. No links in the first DM; earn the conversation first
|
|
104
|
+
5. No em dashes. Write casually, like texting a coworker.
|
|
105
|
+
|
|
106
|
+
## COMMITMENT GUARDRAILS (violating any of these is a critical failure)
|
|
107
|
+
- **NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.** Keep everything in the DM thread.
|
|
108
|
+
- **NEVER agree to podcast appearances, X Spaces, interviews, or live events.**
|
|
109
|
+
- **NEVER offer to move the conversation to another platform** (Telegram, Discord, email, etc.). Stay in the current DM thread.
|
|
110
|
+
- **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.
|
|
111
|
+
- **NEVER make time-bound commitments** ("this week", "tomorrow", "Thursday"). Don't commit the human to any schedule.
|
|
112
|
+
- **NEVER say "I'm in [city]"** or share location/personal details not in config.json.
|
|
113
|
+
- 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?"
|
|
114
|
+
|
|
115
|
+
DM EXAMPLES (good):
|
|
116
|
+
- "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?"
|
|
117
|
+
- "that workaround you mentioned for the accessibility API crash is clever, did it hold up in production?"
|
|
118
|
+
- "curious how you ended up going with that approach for the MCP server, we tried something similar"
|
|
119
|
+
|
|
120
|
+
DM EXAMPLES (bad):
|
|
121
|
+
- "Hey! I noticed your tweet. I'm building something you might find interesting..." (cold pitch)
|
|
122
|
+
- "Great point! I'd love to connect and share what we're working on." (generic)
|
|
123
|
+
- "Hi there, I saw your insightful tweet about AI agents..." (too formal)
|
|
124
|
+
|
|
125
|
+
## Users to DM:
|
|
126
|
+
$DM_DATA
|
|
127
|
+
|
|
128
|
+
## Cross-thread engagement awareness
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
Use it as context for the DM:
|
|
132
|
+
- 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.
|
|
133
|
+
- If they've engaged on multiple other threads, it signals genuine interest. The DM can be slightly more direct without feeling cold.
|
|
134
|
+
- Do NOT quote their other comments back at them or enumerate their history. It's context, not content.
|
|
135
|
+
|
|
136
|
+
## Per-project ICP criteria (used for the pre-check step, NOT to skip sending):
|
|
137
|
+
$PROJECTS_QUALIFICATION
|
|
138
|
+
|
|
139
|
+
## Pre-send profile fetch + ICP pre-check (MANDATORY per DM, no filter)
|
|
140
|
+
|
|
141
|
+
For each DM row, BEFORE you compose or send, do this in order:
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
2. Fetch the prospect's X/Twitter profile using the browser tools from the BROWSER BACKEND block above:
|
|
146
|
+
- Navigate to https://x.com/THEIR_AUTHOR (strip any leading @).
|
|
147
|
+
- Snapshot the page. Extract: display name, handle, bio text, follower count, pinned/top-of-feed recent tweet topic summary.
|
|
148
|
+
- If the profile is suspended, protected, or empty, capture what you can and note "profile_limited" or "profile_inaccessible".
|
|
149
|
+
|
|
150
|
+
3. Persist the profile fields:
|
|
151
|
+
\`\`\`bash
|
|
152
|
+
python3 $REPO_DIR/scripts/fetch_prospect_profile.py upsert \\
|
|
153
|
+
--platform twitter --author "THEIR_AUTHOR" \\
|
|
154
|
+
--profile-url "https://x.com/THEIR_AUTHOR" \\
|
|
155
|
+
--display-name "DISPLAY_NAME" \\
|
|
156
|
+
--headline "SHORT_BIO_FIRST_LINE" \\
|
|
157
|
+
--bio "FULL_BIO_TEXT" \\
|
|
158
|
+
--follower-count N \\
|
|
159
|
+
--recent-activity "SHORT_RECENT_TWEETS_SUMMARY" \\
|
|
160
|
+
--notes "ANY_SIGNAL_WORTH_REMEMBERING" \\
|
|
161
|
+
--link-dm DM_ID
|
|
162
|
+
\`\`\`
|
|
163
|
+
Omit any flag whose value is empty or unknown. \`--link-dm\` also wires dms.prospect_id.
|
|
164
|
+
|
|
165
|
+
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:
|
|
166
|
+
\`\`\`bash
|
|
167
|
+
python3 $REPO_DIR/scripts/dm_conversation.py set-icp-precheck \\
|
|
168
|
+
--dm-id DM_ID --project PROJECT_NAME --label LABEL --notes "SHORT_RATIONALE"
|
|
169
|
+
\`\`\`
|
|
170
|
+
Run this once per project from the list. Each call upserts one entry in dms.icp_matches (JSONB array) keyed by project.
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
174
|
+
## How to send Twitter/X DMs (use the browser tools from the BROWSER BACKEND block):
|
|
175
|
+
1. Navigate to https://x.com/messages
|
|
176
|
+
2. **ENCRYPTED DM PASSCODE**: Twitter may show an "Enter your passcode" or "encrypted_dm_passcode_required" dialog before you can access DMs. If you see this dialog:
|
|
177
|
+
a. Find the passcode input field in the snapshot
|
|
178
|
+
b. Type the passcode: $TWITTER_DM_PASSCODE
|
|
179
|
+
c. Click "Confirm" or press Enter
|
|
180
|
+
d. Wait for the DM inbox to load
|
|
181
|
+
The passcode is loaded from .env as TWITTER_DM_PASSCODE.
|
|
182
|
+
3. Start new message to THEIR_AUTHOR
|
|
183
|
+
4. Type and send the message.
|
|
184
|
+
|
|
185
|
+
## After each DM:
|
|
186
|
+
|
|
187
|
+
Inspect the send_dm tool's return value. There are exactly three outcomes:
|
|
188
|
+
|
|
189
|
+
(A) ok=true AND verified=true -> success, mark sent:
|
|
190
|
+
CLAUDE_SESSION_ID=$CLAUDE_SESSION_ID python3 $REPO_DIR/scripts/dm_send_log.py \\
|
|
191
|
+
--dm-id DM_ID --message "DM_TEXT" --verified
|
|
192
|
+
|
|
193
|
+
Do NOT issue a raw "UPDATE dms SET status='sent'" psql command. The
|
|
194
|
+
dm_send_log.py script is the only path that may flip status to 'sent';
|
|
195
|
+
it requires --verified, and refuses without it. This is intentional:
|
|
196
|
+
prior phantom-DM bugs (~700 rows in 4/2026) came from prose-driven
|
|
197
|
+
status flips that ignored the verification result.
|
|
198
|
+
|
|
199
|
+
After the DM lands, capture the current page URL by evaluating window.location.href in the browser (use the JS-eval tool from the BROWSER BACKEND block) and stamp it onto the DM row so the dashboard's "open chat" button works:
|
|
200
|
+
python3 $REPO_DIR/scripts/dm_conversation.py set-url --dm-id DM_ID --url "CHAT_URL"
|
|
201
|
+
The validator only accepts /i/chat/<id> or /messages/<id>; if the URL is something else (you got bounced to a profile or inbox), skip this step.
|
|
202
|
+
|
|
203
|
+
(B) ok=false OR verified=false -> send did not land, mark error via /api/v1/dms/DM_ID PATCH (DO NOT shell out to psql):
|
|
204
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status error --skip-reason send_unverified --claude-session-id "$CLAUDE_SESSION_ID"
|
|
205
|
+
|
|
206
|
+
(C) Rate limit, account blocked, or any other thrown exception:
|
|
207
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status error --skip-reason REASON --claude-session-id "$CLAUDE_SESSION_ID"
|
|
208
|
+
|
|
209
|
+
DMs disabled (recipient setting, not a send failure):
|
|
210
|
+
python3 $REPO_DIR/scripts/dm_db_update.py --dm-id DM_ID --status skipped --skip-reason chat_disabled --claude-session-id "$CLAUDE_SESSION_ID"
|
|
211
|
+
|
|
212
|
+
CRITICAL: ALL browser calls MUST use the tools listed in the BROWSER BACKEND block above (the Twitter-dedicated MCP for this run). NEVER use generic mcp__playwright-extension__*, mcp__isolated-browser__*, or mcp__macos-use__* tools. If a browser tool call is blocked or times out, wait 30 seconds and retry (up to 3 times). Do NOT fall back to any other browser tool.
|
|
213
|
+
|
|
214
|
+
## CRITICAL FAILURE MODE: Twitter browser tools not registered
|
|
215
|
+
If at the START of this run you cannot see ANY of the browser tools listed in the BROWSER BACKEND block above, OR every browser call fails with "MCP server not connected" / "no such tool" / similar, this is a transient infrastructure failure (Chrome profile collision, wedged MCP wrapper, lock acquired but profile still held by another process). It is NOT an error condition for the prospects in the queue.
|
|
216
|
+
|
|
217
|
+
Do EXACTLY this:
|
|
218
|
+
1. Make NO database changes. Do NOT mark any row as 'error', 'skipped', or anything else.
|
|
219
|
+
2. Print a single line to stdout: \`MCP_UNAVAILABLE: twitter browser tools not registered; aborting with rows left at status=pending\`
|
|
220
|
+
3. Exit cleanly. The launchd schedule will retry on the next cycle, by which point the wedged profile holder will likely have timed out.
|
|
221
|
+
|
|
222
|
+
Burning rows with skip_reason='twitter_agent_mcp_unavailable: ...' is a regression that on 2026-05-12 nuked 7 warm leads (efemjoba, gpuops, josesaezmerino, AIDailyGems, alkimiadev, RobertDMellish, kunaljeweller). The d.id IS NULL filter in scan_dm_candidates.py then permanently blocked them from re-discovery. Do not do this.
|
|
223
|
+
PROMPT_EOF
|
|
224
|
+
|
|
225
|
+
gtimeout 2700 "$REPO_DIR/scripts/run_claude.sh" "dm-outreach-twitter" --strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" --output-format stream-json --verbose -p "${BROWSER_INSTRUCTIONS}
|
|
226
|
+
|
|
227
|
+
$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: Twitter DM outreach claude exited with code $?"
|
|
228
|
+
rm -f "$PROMPT_FILE"
|
|
229
|
+
|
|
230
|
+
# Belt-and-suspenders: if Claude (despite the prompt instructions added 2026-05-13)
|
|
231
|
+
# marked any row with skip_reason='twitter_agent_mcp_unavailable: ...' or
|
|
232
|
+
# 'mcp_unavailable' or similar transient-MCP signals THIS run, revert it back
|
|
233
|
+
# to status='pending' so the next cycle can retry. Scoped to this run via
|
|
234
|
+
# claude_session_id to avoid clobbering legitimate older rows.
|
|
235
|
+
#
|
|
236
|
+
# The scan_dm_candidates.py discover query uses `LEFT JOIN dms ... WHERE d.id IS NULL`
|
|
237
|
+
# so once a reply has any dms row (even status='error'), it never re-appears as a
|
|
238
|
+
# candidate. That's how the 2026-05-12 incident permanently lost 7 warm leads.
|
|
239
|
+
# This revert closes the gap by ensuring transient-MCP failures don't lock rows
|
|
240
|
+
# into a status=error state that the scanner can't see past.
|
|
241
|
+
# MCP-failure recovery sweep now lives at /api/v1/dms/recover-mcp-failures
|
|
242
|
+
# (same UPDATE WHERE filter, same RETURNING shape). Helper prints the
|
|
243
|
+
# recovered_count integer so this $() capture is byte-equivalent.
|
|
244
|
+
RECOVERED=$(python3 "$REPO_DIR/scripts/dm_outreach_twitter_helper.py" \
|
|
245
|
+
recover-mcp --session-id "$CLAUDE_SESSION_ID" 2>/dev/null || echo "0")
|
|
246
|
+
if [ "$RECOVERED" -gt 0 ]; then
|
|
247
|
+
log "Reverted $RECOVERED row(s) from status='error' (transient MCP failure) back to pending"
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# Final summary counts: one HTTP roundtrip via /api/v1/dms/counts vs the
|
|
251
|
+
# two psql one-liners. Helper prints "<sent> <pending>" space-separated.
|
|
252
|
+
_DM_SUMMARY=$(python3 "$REPO_DIR/scripts/dm_outreach_twitter_helper.py" summary 2>/dev/null || echo "0 0")
|
|
253
|
+
SENT=$(echo "$_DM_SUMMARY" | awk '{print $1}')
|
|
254
|
+
STILL_PENDING=$(echo "$_DM_SUMMARY" | awk '{print $2}')
|
|
255
|
+
: "${SENT:=0}"
|
|
256
|
+
: "${STILL_PENDING:=0}"
|
|
257
|
+
log "Twitter DM outreach summary: sent (all-time)=$SENT, still_pending=$STILL_PENDING"
|
|
258
|
+
|
|
259
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
260
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "dm-outreach-twitter" 2>/dev/null || echo "0.0000")
|
|
261
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "dm_outreach_twitter" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
262
|
+
|
|
263
|
+
find "$LOG_DIR" -name "dm-outreach-twitter-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
264
|
+
|
|
265
|
+
log "=== Twitter DM outreach complete: $(date) ==="
|