@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,180 @@
1
+ #!/usr/bin/env python3
2
+ """Return top-performing search_topic seeds per project + platform.
3
+
4
+ This is the Reddit + GitHub feedback feed; the Twitter analog lives in
5
+ `scripts/top_twitter_queries.py`. As of 2026-05-10 the Reddit path is at
6
+ parity with the Twitter feed: it reads `reddit_candidates` instead of
7
+ `posts`, surfaces the full conversion funnel (posted/skipped sample
8
+ sizes, posted/skipped delta_score split, upvotes/comments/clicks),
9
+ and ranks by clicks first.
10
+
11
+ What changed (2026-05-10): the previous version scored only
12
+ `comments_count*3 + upvotes` from the `posts` table, missed clicks
13
+ entirely, had no posted-vs-skipped split, and could not tell the model
14
+ "this query keeps surfacing viral threads we keep skipping" — i.e. a
15
+ mismatch signal. Twitter has had that signal since the Phase 0 batch
16
+ salvage rebuild; Reddit was flying blind on which subreddits/queries
17
+ actually convert to clicks.
18
+
19
+ Reddit path (platform='reddit'):
20
+ Source = reddit_candidates (one row per discovered thread, status in
21
+ pending/posted/skipped/expired/failed). Joins posts via post_id for
22
+ upvotes/comments_count and post_links via post_id for real_clicks
23
+ (clicks are only meaningful on posted rows; the FILTER clauses gate
24
+ to status='posted' inside the SUM).
25
+
26
+ Fields surfaced per (search_topic, project):
27
+ posts — distinct posted candidates
28
+ posted_n — count(*) FILTER (status='posted')
29
+ skipped_n — count(*) FILTER (status IN ('skipped','expired','failed'))
30
+ avg_delta_posted — avg reddit_candidates.delta_score for posted rows
31
+ avg_delta_skipped — avg reddit_candidates.delta_score for skipped/expired/failed rows
32
+ upvotes_total — sum upvotes on our replies (posted only)
33
+ comments_total — sum comments_count on our replies (posted only)
34
+ clicks_total — sum post_links.real_clicks on our replies (posted only)
35
+ composite_score — clicks*100 + comments + upvotes (clicks dominate)
36
+
37
+ delta_score is reddit's velocity proxy (Δup + 4*Δcomments computed
38
+ during the T1 ripen step in ripen_reddit_plan.py). It is set on every
39
+ ripened row regardless of eventual status, which is what lets us
40
+ split the average by posted vs skipped — same diagnostic shape as
41
+ Twitter's avg_virality_posted / avg_virality_skipped:
42
+ high avg_delta_posted + many posts → keep this query, mimic style
43
+ high avg_delta_skipped + few posts → on-rank but off-topic, reword
44
+ low avg_delta_skipped + few posts → dead supply, drop the seed
45
+
46
+ Non-reddit path (platform='github' or unset):
47
+ Source = posts (search_topic stamped at INSERT time). Joins
48
+ post_links via posts.id for clicks_total. Same composite + clicks-DESC
49
+ ordering as the reddit path. Reddit-style status splits are not
50
+ available here because GitHub posts directly without a candidates
51
+ table.
52
+
53
+ Usage:
54
+ python3 scripts/top_search_topics.py --project "fazm" --platform reddit
55
+ python3 scripts/top_search_topics.py --project "fazm" --platform github
56
+ python3 scripts/top_search_topics.py --project "fazm" --platform reddit --json
57
+ """
58
+ import argparse
59
+ import json
60
+ import os
61
+ import sys
62
+
63
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
64
+ from http_api import api_get # noqa: E402
65
+
66
+
67
+ def query(project=None, platform=None, window_days=30, limit=10):
68
+ """Top-performing search_topic seeds per (project, platform).
69
+
70
+ Migrated 2026-05-30 off direct DB (db.get_conn) onto the HTTP lane:
71
+ GET /api/v1/search-topics/ranked?platform=&project=&window_days=&limit=.
72
+ The route mirrors the four legacy `_query_*` SQL paths one-for-one
73
+ (reddit / twitter / linkedin / posts-fallback), including the 2026-05-29
74
+ cross-route guard on the twitter posted-conversion aggregates, and returns
75
+ rows already shaped as the dicts this function used to build, so we read
76
+ `data.rows` verbatim. There is intentionally NO direct-DB fallback.
77
+ """
78
+ q = {"window_days": int(window_days), "limit": int(limit)}
79
+ if platform:
80
+ q["platform"] = platform
81
+ if project:
82
+ q["project"] = project
83
+ resp = api_get("/api/v1/search-topics/ranked", q)
84
+ data = (resp or {}).get("data") or {}
85
+ return list(data.get("rows") or [])
86
+
87
+
88
+ def format_text(results, project=None, platform=None, window_days=30):
89
+ plat = (platform or "").lower()
90
+ is_reddit = plat == "reddit"
91
+ is_twitter = plat == "twitter"
92
+ is_linkedin = plat == "linkedin"
93
+ if not results:
94
+ return (
95
+ f"(no search_topic data yet in the last {window_days}d"
96
+ + (f" for {project}" if project else "")
97
+ + (f" on {platform}" if platform else "")
98
+ + ")"
99
+ )
100
+ header = f"Top search_topic seeds (last {window_days}d"
101
+ if project:
102
+ header += f", project={project}"
103
+ if platform:
104
+ header += f", platform={platform}"
105
+ if is_reddit:
106
+ header += ", ranked by clicks_total DESC then composite (clicks×100 + comments + upvotes))"
107
+ elif is_twitter:
108
+ header += ", ranked by clicks_total DESC then composite (clicks×100 + likes + views×0.001))"
109
+ elif is_linkedin:
110
+ header += ", ranked by clicks_total DESC then composite (clicks×100 + likes + views×0.001 + velocity))"
111
+ else:
112
+ header += ", ranked by clicks_total DESC then composite (clicks×100 + comments×3 + upvotes))"
113
+ lines = [header]
114
+ if is_reddit:
115
+ lines.append(
116
+ f" {'clicks':>6} {'comm':>5} {'upv':>5} "
117
+ f"{'posts':>5} {'pN':>3} {'sN':>3} "
118
+ f"{'Δpost':>6} {'Δskip':>6} topic"
119
+ )
120
+ for r in results:
121
+ lines.append(
122
+ f" {r['clicks_total']:>6} {r['comments_total']:>5} {r['upvotes_total']:>5} "
123
+ f"{r['posts']:>5} {r['posted_n']:>3} {r['skipped_n']:>3} "
124
+ f"{r['avg_delta_posted']:>6.1f} {r['avg_delta_skipped']:>6.1f} {r['search_topic']}"
125
+ )
126
+ lines.append(
127
+ " (Δpost = avg ripen delta_score on posted rows; "
128
+ "Δskip = avg ripen delta_score on skipped/expired/failed rows. "
129
+ "High Δskip + few posts = query is on-rank but off-topic — reword. "
130
+ "Low Δskip + few posts = dead supply, drop the seed.)"
131
+ )
132
+ elif is_twitter or is_linkedin:
133
+ lines.append(
134
+ f" {'clicks':>6} {'views':>7} {'likes':>5} "
135
+ f"{'posts':>5} {'pN':>3} {'sN':>3} "
136
+ f"{'Vpost':>6} {'Vskip':>6} topic"
137
+ )
138
+ for r in results:
139
+ lines.append(
140
+ f" {r['clicks_total']:>6} {r['views_total']:>7} {r['likes_total']:>5} "
141
+ f"{r['posts']:>5} {r['posted_n']:>3} {r['skipped_n']:>3} "
142
+ f"{r['avg_virality_posted']:>6.1f} {r['avg_virality_skipped']:>6.1f} {r['search_topic']}"
143
+ )
144
+ lines.append(
145
+ " (Vpost = avg virality_score on posted rows; "
146
+ "Vskip = avg virality_score on skipped/expired/failed rows. "
147
+ "High Vskip + few posts = topic finds viral noise we keep skipping - reword. "
148
+ "Low Vskip + few posts = dead supply, drop the seed.)"
149
+ )
150
+ else:
151
+ lines.append(
152
+ f" {'clicks':>6} {'comm':>5} {'upv':>5} {'posts':>5} topic"
153
+ )
154
+ for r in results:
155
+ lines.append(
156
+ f" {r['clicks_total']:>6} {r['comments_total']:>5} {r['upvotes_total']:>5} "
157
+ f"{r['posts']:>5} {r['search_topic']}"
158
+ )
159
+ return "\n".join(lines)
160
+
161
+
162
+ def main():
163
+ ap = argparse.ArgumentParser()
164
+ ap.add_argument("--project", default=None)
165
+ ap.add_argument("--platform", default=None)
166
+ ap.add_argument("--window-days", type=int, default=30)
167
+ ap.add_argument("--limit", type=int, default=10)
168
+ ap.add_argument("--json", action="store_true", help="Output JSON instead of text")
169
+ args = ap.parse_args()
170
+
171
+ results = query(args.project, args.platform, args.window_days, args.limit)
172
+ if args.json:
173
+ json.dump(results, sys.stdout)
174
+ sys.stdout.write("\n")
175
+ else:
176
+ print(format_text(results, args.project, args.platform, args.window_days))
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_twitter_queries.py
4
+
5
+ Returns top-performing historical search queries scored by a composite of
6
+ clicks, likes, views, posts produced, AND raw supply (tweets_found per
7
+ attempt). Used as STYLE inspiration for the LLM that drafts new queries.
8
+
9
+ Per-query fields, structured so the model can see the FULL conversion
10
+ funnel AND distinguish "queries that find threads worth posting to" from
11
+ "queries that find viral noise we keep skipping":
12
+
13
+ query , the literal X search string (with min_faves:N etc.)
14
+ project , project the query was drafted for (matched_project)
15
+ tweets_found_avg , SUPPLY: avg tweets X returned per attempt
16
+ posted_n , count of candidates with status='posted'
17
+ skipped_n , count of candidates with status IN ('skipped','expired')
18
+ post_rate , posted_n / (posted_n + skipped_n); draft-gate acceptance ratio
19
+ avg_virality_posted , avg source-thread virality_score for posted candidates
20
+ avg_virality_skipped , avg source-thread virality_score for skipped/expired
21
+ views_total , sum of views on OUR replies (downstream surface)
22
+ likes_total , sum of likes on OUR replies
23
+ clicks_total , sum of real_clicks attributed to our replies (CTA tracking)
24
+ composite_score , clicks*100 + likes + views*0.001 (clicks dominate)
25
+
26
+ The two virality fields together let the model diagnose query failure
27
+ modes that pure conversion data misses:
28
+ - high avg_virality_posted + many posts → keep / mimic this query style
29
+ - high avg_virality_skipped + few posts → reword: query is on-rank but
30
+ semantically off-topic (e.g. studyly catching unrelated viral student
31
+ drama because keywords overlap with study-related slang)
32
+ - low avg_virality_skipped + few posts → query is just dead supply,
33
+ drop the keyword cluster entirely
34
+
35
+ Source-thread virality_score is computed by score_twitter_candidates.py
36
+ (engagement velocity + retweet ratio + reply weight + author followers,
37
+ with 6h half-life decay). It's set on EVERY candidate at discovery time
38
+ regardless of posted/skipped/expired status, which is why we can split
39
+ the average by status group.
40
+
41
+ Usage:
42
+
43
+ python3 scripts/top_twitter_queries.py [--limit 20] [--window-days 14] [--project NAME]
44
+
45
+ The optional --project filter is what enables per-project surfacing in the
46
+ Phase 1 scanner prompt: each cycle, the scanner can fetch the top queries
47
+ specifically for the project it's currently drafting for.
48
+
49
+ Migrated 2026-05-18: reads now go through /api/v1/twitter-search-attempts/
50
+ top-queries via scripts/http_api.py instead of a direct psycopg2 query.
51
+ The SQL composite-score join (cand_agg + supply_agg, click_total tiebreaker)
52
+ runs server-side; this script just shapes the response into the legacy JSON.
53
+ """
54
+ import argparse
55
+ import json
56
+ import os
57
+ import sys
58
+
59
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
60
+ from http_api import api_get # noqa: E402
61
+
62
+
63
+ def main():
64
+ p = argparse.ArgumentParser()
65
+ p.add_argument("--limit", type=int, default=20)
66
+ p.add_argument("--window-days", type=int, default=14)
67
+ p.add_argument("--project", default=None,
68
+ help="If set, only return top queries for this project (matched_project).")
69
+ args = p.parse_args()
70
+
71
+ query = {
72
+ "limit": args.limit,
73
+ "window_days": args.window_days,
74
+ }
75
+ if args.project:
76
+ query["project"] = args.project
77
+
78
+ resp = api_get("/api/v1/twitter-search-attempts/top-queries", query=query)
79
+ rows = (resp.get("data") or {}).get("rows") or []
80
+
81
+ # Pass-through shape, but float-coerce so the legacy JSON consumers
82
+ # (run-twitter-cycle.sh's Phase 1 prompt) see the same types as before.
83
+ # Derived field: post_rate = posted_n / (posted_n + skipped_n), the draft-gate
84
+ # acceptance ratio. Lets the model see whether a query that LOOKS productive
85
+ # by raw posts count actually clears the skip filter, or whether it surfaces
86
+ # 100 candidates and we reject 95 of them. Safe-divide on empty denominator.
87
+ # Dropped: 'posts' field (was identical to 'posted_n' in every observed row).
88
+ def _post_rate(posted: int, skipped: int) -> float:
89
+ denom = posted + skipped
90
+ if denom <= 0:
91
+ return 0.0
92
+ return round(posted / denom, 3)
93
+
94
+ # Derived field: posts_per_attempt = tweets_found_avg * post_rate.
95
+ # Algebraically equivalent to posted_n / attempts_n (the raw API doesn't
96
+ # expose attempts_n directly, but tweets_found_avg is tweets/attempt and
97
+ # post_rate is posted/(posted+skipped) so the product is posts/attempt).
98
+ # This is the headline efficiency number: how many posts does this query
99
+ # actually yield per Phase 1 search invocation? <0.1 means most attempts
100
+ # don't even produce one survivor.
101
+ def _posts_per_attempt(tweets_found_avg: float, post_rate: float) -> float:
102
+ return round(tweets_found_avg * post_rate, 3)
103
+
104
+ # Two-axis bucketing (2026-05-28). Drafter prompt uses these labels to
105
+ # decide whether to mimic, narrow, or broaden a past query's structure.
106
+ # Raw numbers stay in the payload; buckets are derived helpers that give
107
+ # the model a categorical hint instead of forcing it to threshold floats
108
+ # in-context every time.
109
+ #
110
+ # supply_bucket: raw tweet supply from X for this query phrasing.
111
+ # low = <1 tweets/attempt (query is dying or freshness window too tight)
112
+ # medium = 1-5 tweets/attempt (healthy)
113
+ # high = >5 tweets/attempt (lots of supply, often noisy)
114
+ #
115
+ # conversion_bucket: how often a found tweet survives the draft gate.
116
+ # low = <0.2 post_rate (gate keeps rejecting; query is on-rank
117
+ # but semantically off-target)
118
+ # medium = 0.2-0.6 post_rate (normal)
119
+ # high = >=0.6 post_rate (high-fit query, mimic the structure)
120
+ def _supply_bucket(tweets_found_avg: float) -> str:
121
+ if tweets_found_avg < 1:
122
+ return "low"
123
+ if tweets_found_avg <= 5:
124
+ return "medium"
125
+ return "high"
126
+
127
+ def _conversion_bucket(post_rate: float) -> str:
128
+ if post_rate < 0.2:
129
+ return "low"
130
+ if post_rate < 0.6:
131
+ return "medium"
132
+ return "high"
133
+
134
+ # guidance: what should the drafter DO with this query as a reference?
135
+ #
136
+ # BROADEN — supply is dying. Shorten to 1-2 keywords, drop OR groups,
137
+ # step min_faves down a tier. Past query's OPERATORS are
138
+ # dead weight; only the topic-keyword is signal.
139
+ # NARROW — supply is abundant but conversion is bad. Past query
140
+ # fishes in a noisy pond. Add specificity (more OR terms,
141
+ # stricter min_faves, -term excludes) so the freshness
142
+ # window surfaces fewer, higher-fit candidates.
143
+ # KEEP_STYLE — middle of the road. Use the operator skeleton, swap
144
+ # keywords for the picker-assigned topic.
145
+ # MIMIC — gold tier. Reuse the full operator pattern verbatim,
146
+ # only swap the topic-keyword. This is what works.
147
+ def _guidance(supply_b: str, conv_b: str) -> str:
148
+ if supply_b == "low":
149
+ return "BROADEN"
150
+ if supply_b == "high" and conv_b == "low":
151
+ return "NARROW"
152
+ if conv_b == "high":
153
+ return "MIMIC"
154
+ if supply_b == "medium" and conv_b == "medium":
155
+ return "KEEP_STYLE"
156
+ # high supply + medium conversion, medium supply + low conversion
157
+ return "NARROW" if conv_b == "low" else "KEEP_STYLE"
158
+
159
+ out = []
160
+ for r in rows:
161
+ posted_n = int(r.get("posted_n") or 0)
162
+ skipped_n = int(r.get("skipped_n") or 0)
163
+ tweets_found_avg = float(r.get("tweets_found_avg") or 0)
164
+ post_rate = _post_rate(posted_n, skipped_n)
165
+ supply_b = _supply_bucket(tweets_found_avg)
166
+ conv_b = _conversion_bucket(post_rate)
167
+ out.append({
168
+ "query": r.get("query"),
169
+ "project": r.get("project") or "",
170
+ "posted_n": posted_n,
171
+ "skipped_n": skipped_n,
172
+ "post_rate": post_rate,
173
+ "posts_per_attempt": _posts_per_attempt(tweets_found_avg, post_rate),
174
+ "supply_bucket": supply_b,
175
+ "conversion_bucket": conv_b,
176
+ "guidance": _guidance(supply_b, conv_b),
177
+ "avg_virality_posted": round(float(r.get("avg_virality_posted") or 0), 2),
178
+ "avg_virality_skipped": round(float(r.get("avg_virality_skipped") or 0), 2),
179
+ "views_total": int(r.get("views_total") or 0),
180
+ "likes_total": int(r.get("likes_total") or 0),
181
+ "clicks_total": int(r.get("clicks_total") or 0),
182
+ "tweets_found_avg": tweets_found_avg,
183
+ "composite_score": round(float(r.get("composite_score") or 0), 2),
184
+ })
185
+ json.dump(out, sys.stdout)
186
+ print("", file=sys.stdout)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()