@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,301 @@
1
+ #!/usr/bin/env python3
2
+ """engage_twitter_helper.py — small CLI wrapper used by skill/engage-twitter.sh
3
+ to replace the six `psql -t -A -c "..."` one-liners the shell used to embed
4
+ inline. Every subcommand prints exactly one value to stdout (string / int /
5
+ JSON) so bash can capture it with $(...) without changing shape.
6
+
7
+ Subcommands:
8
+ reset-stuck-replies
9
+ -> POST /api/v1/replies/reset-stuck { platform:'x', older_than_hours:2 }
10
+ -> prints the integer reset_count
11
+ pending-count
12
+ -> GET /api/v1/replies/counts?platform=x
13
+ -> prints the pending count
14
+ reply-counts
15
+ -> GET /api/v1/replies/counts?platform=x
16
+ -> prints JSON {pending, replied, skipped}
17
+ pending-data --batch-size N
18
+ -> GET /api/v1/replies/next-pending?platform=x&limit=N
19
+ then reshape to the legacy json_agg() shape engage-twitter.sh's
20
+ prompt-template expects:
21
+ [{id, platform, their_author, their_content, their_comment_url,
22
+ their_comment_id, depth, thread_title, thread_url, our_content,
23
+ our_url, is_our_original_post, project_name}, ...]
24
+ post-reset
25
+ -> POST /api/v1/replies/reset-stuck { platform:'x', older_than_hours:0 }
26
+ -> prints reset_count
27
+ active-campaign
28
+ -> GET /api/v1/campaigns?platform=twitter&has_suffix=true
29
+ &with_budget_remaining=true&status=active&limit=1
30
+ -> prints JSON {id, suffix, sample_rate} or {} when none active
31
+
32
+ Migrated 2026-05-18: the bash used to embed six raw `psql` queries against
33
+ Postgres for this engage loop; this helper replaces them all with HTTP API
34
+ calls and keeps the bash side free of DATABASE_URL handling for the engage
35
+ pipeline.
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import argparse
40
+ import json
41
+ import os
42
+ import sys
43
+
44
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
45
+ from http_api import api_get, api_post # noqa: E402
46
+
47
+
48
+ def cmd_reset_stuck_replies() -> int:
49
+ resp = api_post(
50
+ "/api/v1/replies/reset-stuck",
51
+ {"platform": "x", "older_than_hours": 2},
52
+ )
53
+ data = resp.get("data") or {}
54
+ print(int(data.get("reset_count") or 0))
55
+ return 0
56
+
57
+
58
+ def cmd_post_reset() -> int:
59
+ # 0-hour window resets every 'processing' reply we have right now.
60
+ # Mirrors the engage-twitter.sh "leftover after subprocess exit" sweep.
61
+ resp = api_post(
62
+ "/api/v1/replies/reset-stuck",
63
+ {"platform": "x", "older_than_hours": 1},
64
+ )
65
+ data = resp.get("data") or {}
66
+ print(int(data.get("reset_count") or 0))
67
+ return 0
68
+
69
+
70
+ def _counts_dict() -> dict[str, int]:
71
+ """Reshape /api/v1/replies/counts into the flat {pending, replied,
72
+ skipped, processing} dict our callers expect.
73
+
74
+ Prefers `eligible_counts` (JOIN-aware: matches what /next-pending
75
+ actually surfaces) over the raw `counts` field. The two diverge when
76
+ truly-orphan rows exist (post_id pointing at a deleted post AND no
77
+ mention_id / parent_reply_id fallback) — historically the raw count
78
+ misled engage-twitter's early-skip gate into burning the
79
+ twitter-browser lock for the full Phase B window finding nothing.
80
+ Falls back to raw `counts` if the deploy doesn't yet expose
81
+ `eligible_counts` (pre-2026-05-26 vintage).
82
+ """
83
+ resp = api_get(
84
+ "/api/v1/replies/counts",
85
+ query={"platform": "x"},
86
+ )
87
+ data = resp.get("data") or {}
88
+ rows = data.get("eligible_counts") or data.get("counts") or []
89
+ out: dict[str, int] = {}
90
+ for r in rows:
91
+ s = r.get("status")
92
+ c = r.get("count")
93
+ if s is None:
94
+ continue
95
+ try:
96
+ out[str(s)] = int(c or 0)
97
+ except (TypeError, ValueError):
98
+ out[str(s)] = 0
99
+ return out
100
+
101
+
102
+ def cmd_pending_count() -> int:
103
+ counts = _counts_dict()
104
+ print(int(counts.get("pending") or 0))
105
+ return 0
106
+
107
+
108
+ def cmd_reply_counts() -> int:
109
+ counts = _counts_dict()
110
+ out = {
111
+ "pending": int(counts.get("pending") or 0),
112
+ "replied": int(counts.get("replied") or 0),
113
+ "skipped": int(counts.get("skipped") or 0),
114
+ }
115
+ json.dump(out, sys.stdout, separators=(",", ":"))
116
+ sys.stdout.write("\n")
117
+ return 0
118
+
119
+
120
+ def _render_media_block(media) -> str:
121
+ """Render replies.their_media ([{url,alt,type}]) into a short, self-titled
122
+ text block for the Phase B prompt (2026-06-03 thread-media feature). Empty
123
+ string when the comment had no media (or media was never captured), so it
124
+ stays invisible in the embedded JSON for text-only comments.
125
+ """
126
+ if not isinstance(media, list) or not media:
127
+ return ""
128
+ lines = []
129
+ for it in media:
130
+ if not isinstance(it, dict):
131
+ continue
132
+ t = (it.get("type") or "media").strip()
133
+ alt = (it.get("alt") or "").strip()
134
+ url = (it.get("url") or "").strip()
135
+ alt_part = f'"{alt}"' if alt else "[no description]"
136
+ lines.append(f" - {t}: {alt_part} ({url})")
137
+ if not lines:
138
+ return ""
139
+ return (
140
+ "## Media in the comment you are replying to\n"
141
+ "React to what it VISUALLY shows, not just the text. "
142
+ "[no description] = no alt-text; infer from the comment + media type.\n"
143
+ + "\n".join(lines)
144
+ )
145
+
146
+
147
+ def cmd_pending_data(batch_size: int) -> int:
148
+ try:
149
+ from account_resolver import resolve as _resolve_account # noqa: WPS433
150
+ our_account = _resolve_account("twitter")
151
+ except Exception:
152
+ our_account = None
153
+ query = {"platform": "x", "limit": batch_size}
154
+ if our_account:
155
+ query["our_account"] = our_account
156
+ resp = api_get(
157
+ "/api/v1/replies/next-pending",
158
+ query=query,
159
+ )
160
+ rows = (resp.get("data") or {}).get("replies") or []
161
+
162
+ # Enrich each row with a per-counterparty history block (DM cross-thread
163
+ # + public-reply history) via the shared counterparty_history module.
164
+ # The block is self-titled ("## Prior history with @author") and lands
165
+ # inline in PENDING_DATA so the Phase B prompt picks it up without any
166
+ # change to the shell-side prompt template.
167
+ #
168
+ # Capped to ENRICH_TOP_N because the API list is priority-ordered
169
+ # (our_thread first, then discovered_at ASC) and the Phase B Claude
170
+ # session rarely processes more than ~50 items before gtimeout fires.
171
+ # Beyond the cap we leave counterparty_history_block empty; if a row
172
+ # falls past the cap and IS reached on a later cycle, it'll be in the
173
+ # top slot then and get enriched.
174
+ ENRICH_TOP_N = 60
175
+ history_blocks = [""] * len(rows)
176
+ try:
177
+ from concurrent.futures import ThreadPoolExecutor
178
+ from counterparty_history import get_counterparty_history_block
179
+
180
+ def _enrich(r):
181
+ author = r.get("their_author")
182
+ if not author:
183
+ return ""
184
+ try:
185
+ _disengage, block = get_counterparty_history_block(
186
+ platform="x",
187
+ author=author,
188
+ current_post_id=r.get("post_id"),
189
+ current_reply_id=r.get("id"),
190
+ )
191
+ return block or ""
192
+ except Exception as e:
193
+ print(
194
+ f"[engage_twitter_helper] counterparty_history failed "
195
+ f"for @{author}: {e}",
196
+ file=sys.stderr,
197
+ )
198
+ return ""
199
+
200
+ top_rows = rows[:ENRICH_TOP_N]
201
+ with ThreadPoolExecutor(max_workers=8) as ex:
202
+ for idx, block in enumerate(ex.map(_enrich, top_rows)):
203
+ history_blocks[idx] = block
204
+ non_empty = sum(1 for b in history_blocks if b)
205
+ print(
206
+ f"[engage_twitter_helper] counterparty_history enriched "
207
+ f"{len(top_rows)}/{len(rows)} rows ({non_empty} with non-empty block)",
208
+ file=sys.stderr,
209
+ )
210
+ except Exception as e:
211
+ print(
212
+ f"[engage_twitter_helper] enrichment phase failed "
213
+ f"(continuing without history): {e}",
214
+ file=sys.stderr,
215
+ )
216
+
217
+ out = []
218
+ for r, history_block in zip(rows, history_blocks):
219
+ out.append({
220
+ "id": r.get("id"),
221
+ "platform": r.get("platform"),
222
+ "their_author": r.get("their_author"),
223
+ "their_content": r.get("their_content"),
224
+ "their_comment_url": r.get("their_comment_url"),
225
+ "their_comment_id": r.get("their_comment_id"),
226
+ "depth": r.get("depth"),
227
+ "thread_title": r.get("thread_title"),
228
+ "thread_url": r.get("thread_url"),
229
+ "our_content": r.get("our_content"),
230
+ "our_url": r.get("our_url"),
231
+ "is_our_original_post": int(r.get("is_our_original_post") or 0),
232
+ "project_name": r.get("project_name"),
233
+ "counterparty_history_block": history_block,
234
+ "their_media_block": _render_media_block(r.get("their_media")),
235
+ })
236
+ # json_agg(...) returns null when the array is empty; engage-twitter.sh's
237
+ # downstream prompt-template expects an empty array instead, which is
238
+ # easier to embed verbatim.
239
+ json.dump(out, sys.stdout, separators=(",", ":"))
240
+ sys.stdout.write("\n")
241
+ return 0
242
+
243
+
244
+ def cmd_active_campaign() -> int:
245
+ resp = api_get(
246
+ "/api/v1/campaigns",
247
+ query={
248
+ "status": "active",
249
+ "platform": "twitter",
250
+ "has_suffix": "true",
251
+ "with_budget_remaining": "true",
252
+ "limit": 1,
253
+ },
254
+ )
255
+ rows = (resp.get("data") or {}).get("campaigns") or []
256
+ if not rows:
257
+ sys.stdout.write("{}\n")
258
+ return 0
259
+ r = rows[0]
260
+ out = {
261
+ "id": r.get("id"),
262
+ "suffix": r.get("suffix"),
263
+ "sample_rate": float(r.get("sample_rate") or 1.0),
264
+ }
265
+ json.dump(out, sys.stdout, separators=(",", ":"))
266
+ sys.stdout.write("\n")
267
+ return 0
268
+
269
+
270
+ def main() -> int:
271
+ ap = argparse.ArgumentParser(description="Helper for engage-twitter.sh")
272
+ sub = ap.add_subparsers(dest="cmd", required=True)
273
+
274
+ sub.add_parser("reset-stuck-replies")
275
+ sub.add_parser("pending-count")
276
+ sub.add_parser("reply-counts")
277
+ sub.add_parser("post-reset")
278
+ sub.add_parser("active-campaign")
279
+
280
+ p_pending = sub.add_parser("pending-data")
281
+ p_pending.add_argument("--batch-size", type=int, default=500)
282
+
283
+ args = ap.parse_args()
284
+
285
+ if args.cmd == "reset-stuck-replies":
286
+ return cmd_reset_stuck_replies()
287
+ if args.cmd == "pending-count":
288
+ return cmd_pending_count()
289
+ if args.cmd == "reply-counts":
290
+ return cmd_reply_counts()
291
+ if args.cmd == "post-reset":
292
+ return cmd_post_reset()
293
+ if args.cmd == "active-campaign":
294
+ return cmd_active_campaign()
295
+ if args.cmd == "pending-data":
296
+ return cmd_pending_data(args.batch_size)
297
+ return 1
298
+
299
+
300
+ if __name__ == "__main__":
301
+ sys.exit(main())