@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.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1336 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +513 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. 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) ==="
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # engage-dm-replies-linkedin.sh — LinkedIn DM replies only
3
+
4
+ exec "$(dirname "$0")/engage-dm-replies.sh" --platform linkedin
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # engage-dm-replies-reddit.sh — Reddit DM replies only
3
+
4
+ exec "$(dirname "$0")/engage-dm-replies.sh" --platform reddit
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # engage-dm-replies-twitter.sh — Twitter DM replies only
3
+
4
+ exec "$(dirname "$0")/engage-dm-replies.sh" --platform twitter