@m13v/s4l 1.6.197-rc.7

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 +1314 -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 +497 -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,71 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_dud_linkedin_queries.py
4
+
5
+ Returns recent LinkedIn search queries that produced ZERO usable candidates
6
+ OR that returned candidates from a low-quality SERP (serp_quality_score < 4),
7
+ so the LLM scanner can be told "do not redraft these phrasings, they have
8
+ been flat or audience-wrong for the last week".
9
+
10
+ Why both signals (zero-result AND low-SERP-quality):
11
+ - Zero-result query: keyword too narrow, typos, or LinkedIn search index
12
+ rejects the phrasing. Standard dud.
13
+ - Low-quality SERP: query returns 30 hits but all from influencer-bait
14
+ accounts; technically not zero, but useless for our outbound posting.
15
+ Same dud-class for the LLM's purposes.
16
+
17
+ Pair with top_linkedin_queries.py (positive signal).
18
+
19
+ python3 scripts/top_dud_linkedin_queries.py [--project NAME] [--search-topic TOPIC] [--limit 30] [--window-days 7]
20
+
21
+ Output: JSON list of
22
+ {"query": ..., "project": ..., "search_topic": ..., "attempts": N,
23
+ "last_ran_h_ago": F, "reason": "zero_results"|"low_serp_quality"}
24
+
25
+ Window default 7 days (vs Twitter's 48h). LinkedIn cycle frequency is much
26
+ lower; need a wider window to gather enough samples.
27
+
28
+ Source: linkedin_search_attempts (one row per query per cycle, written by
29
+ run-linkedin.sh after Phase A scrape parses queries_used).
30
+
31
+ Migrated 2026-06-01 from direct db.py SELECTs to the s4l.ai HTTP API
32
+ (GET /api/v1/linkedin-search-attempts/duds). No DATABASE_URL needed.
33
+ """
34
+ import argparse
35
+ import json
36
+ import os
37
+ import sys
38
+
39
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40
+ from http_api import api_get
41
+
42
+
43
+ def main():
44
+ p = argparse.ArgumentParser()
45
+ p.add_argument("--limit", type=int, default=30)
46
+ p.add_argument("--window-days", type=int, default=7,
47
+ help="Look back this many days for dud queries.")
48
+ p.add_argument("--low-serp-threshold", type=float, default=4.0,
49
+ help="serp_quality_score below this counts as a dud.")
50
+ p.add_argument("--project", default=None)
51
+ p.add_argument("--search-topic", default=None)
52
+ args = p.parse_args()
53
+
54
+ query = {
55
+ "limit": args.limit,
56
+ "window_days": args.window_days,
57
+ "low_serp_threshold": args.low_serp_threshold,
58
+ }
59
+ if args.project:
60
+ query["project"] = args.project
61
+ if args.search_topic:
62
+ query["search_topic"] = args.search_topic
63
+
64
+ resp = api_get("/api/v1/linkedin-search-attempts/duds", query)
65
+ out = (resp.get("data") or {}).get("duds") or []
66
+ json.dump(out, sys.stdout)
67
+ print("", file=sys.stdout)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_dud_reddit_queries.py
4
+
5
+ Returns recent Reddit search queries that produced ZERO post-filter
6
+ candidates so post_reddit.py:build_prompt can tell the LLM scanner
7
+ "do not redraft these phrasings, they were flat in the last N hours".
8
+ Counterpart to top_search_topics.py (positive signal): this is the
9
+ negative-signal feed.
10
+
11
+ python3 scripts/top_dud_reddit_queries.py [--project NAME] [--limit 30] [--window-hours 168]
12
+
13
+ Output: JSON list of
14
+ {"query": ..., "subreddits": ..., "project": ..., "attempts": N, "last_ran_h_ago": F}
15
+ sorted by most-attempted dud first (so the most-wasteful repeats surface
16
+ at the top of the prompt anti-list).
17
+
18
+ Source: reddit_search_attempts (one row per (query, subreddits, project)
19
+ per cmd_search call, written by reddit_tools.py:cmd_search). Routed
20
+ through GET /api/v1/reddit-search-attempts/dud-queries on the website
21
+ to keep this script HTTP-only (no psycopg2 / no DATABASE_URL).
22
+ """
23
+ import argparse
24
+ import json
25
+ import os
26
+ import sys
27
+
28
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
+ from http_api import api_get
30
+
31
+
32
+ def main():
33
+ p = argparse.ArgumentParser()
34
+ p.add_argument("--project", default=None,
35
+ help="Filter to a single project (matches project_name).")
36
+ p.add_argument("--limit", type=int, default=30)
37
+ p.add_argument("--window-hours", type=int, default=168,
38
+ help="Look back this many hours for dud queries (default 7d).")
39
+ args = p.parse_args()
40
+
41
+ query = {
42
+ "limit": args.limit,
43
+ "window_hours": args.window_hours,
44
+ }
45
+ if args.project:
46
+ query["project"] = args.project
47
+
48
+ resp = api_get("/api/v1/reddit-search-attempts/dud-queries", query=query)
49
+ data = (resp or {}).get("data") or {}
50
+ rows = data.get("rows") or []
51
+
52
+ out = [
53
+ {
54
+ "query": r.get("query"),
55
+ "subreddits": r.get("subreddits") or None,
56
+ "project": r.get("project") or "",
57
+ "attempts": int(r.get("attempts") or 0),
58
+ "last_ran_h_ago": round(float(r.get("last_ran_h_ago") or 0), 1),
59
+ }
60
+ for r in rows
61
+ ]
62
+ json.dump(out, sys.stdout)
63
+ print("", file=sys.stdout)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_dud_twitter_queries.py
4
+
5
+ Returns recent Twitter search queries that produced ZERO tweets so the
6
+ LLM scanner can be told "do not redraft these phrasings — they were flat
7
+ in the last N hours". Counterpart to top_twitter_queries.py (positive
8
+ signal): this is the negative-signal feed.
9
+
10
+ python3 scripts/top_dud_twitter_queries.py [--limit 30] [--window-hours 48]
11
+
12
+ Output: JSON list of
13
+ {"query": ..., "project": ..., "min_faves": N | null,
14
+ "attempts": N, "last_ran_h_ago": F}
15
+ sorted by most-attempted dud first (so the most-wasteful repeats surface
16
+ at the top of the prompt anti-list).
17
+
18
+ The min_faves field is parsed from the query string (X operator
19
+ `min_faves:N`). Surfacing it lets the model correlate "every studyly dud
20
+ last 48h used min_faves:20" → drop the floor for that project.
21
+
22
+ Source: twitter_search_attempts (one row per query per cycle, written by
23
+ run-twitter-cycle.sh after the Phase 1 scan parses queries_used).
24
+
25
+ Migrated 2026-05-18: reads now go through /api/v1/twitter-search-attempts/
26
+ dud-queries via scripts/http_api.py instead of a direct psycopg2 query.
27
+ """
28
+ import argparse
29
+ import json
30
+ import os
31
+ import sys
32
+
33
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
34
+ from http_api import api_get # noqa: E402
35
+
36
+
37
+ def main():
38
+ p = argparse.ArgumentParser()
39
+ p.add_argument("--limit", type=int, default=30)
40
+ p.add_argument("--window-hours", type=int, default=48,
41
+ help="Look back this many hours for dud queries.")
42
+ p.add_argument("--project", default=None,
43
+ help="If set, only return duds for this project.")
44
+ args = p.parse_args()
45
+
46
+ query = {
47
+ "limit": args.limit,
48
+ "window_hours": args.window_hours,
49
+ }
50
+ if args.project:
51
+ query["project"] = args.project
52
+
53
+ resp = api_get("/api/v1/twitter-search-attempts/dud-queries", query=query)
54
+ rows = (resp.get("data") or {}).get("rows") or []
55
+
56
+ out = [
57
+ {
58
+ "query": r.get("query"),
59
+ "project": r.get("project") or "",
60
+ "min_faves": r.get("min_faves"),
61
+ "attempts": int(r.get("attempts") or 0),
62
+ "last_ran_h_ago": round(float(r.get("last_ran_h_ago") or 0), 1),
63
+ }
64
+ for r in rows
65
+ ]
66
+ json.dump(out, sys.stdout)
67
+ print("", file=sys.stdout)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_dud_twitter_topics.py
4
+
5
+ Returns recent Twitter search_topic SEEDS that are pulling in alive-but-off-fit
6
+ candidates: the search returns viral tweets, Phase 1 stamps them on
7
+ twitter_candidates rows, but Phase 2b's draft gate keeps skipping them (or
8
+ they expire un-drafted). The CONCEPT SEED is finding noise, not buyers.
9
+
10
+ Where this fits in the feedback ladder:
11
+ - top_search_topics.py = positive signal (seed -> posted -> engagement)
12
+ - top_dud_twitter_queries.py = "search returned 0 tweets" signal (the query
13
+ phrasing is dead; reword the phrasing)
14
+ - THIS = "search returned viral content, draft gate
15
+ killed it" signal (the CONCEPT is off-fit;
16
+ reword the seed narrower or drop it)
17
+
18
+ Output: JSON list (so build_discover_prompt can paste it directly), sorted
19
+ most-skipped first:
20
+
21
+ [{"search_topic": "...", "project": "...",
22
+ "posted_n": N, "skipped_n": M,
23
+ "avg_virality_posted": F, "avg_virality_skipped": F,
24
+ "omit_rate": 0.NN, "last_skip_h_ago": F.F,
25
+ "sample_skip_reasons": ["off_brand_crypto", "audience_mismatch", ...]}]
26
+
27
+ Usage:
28
+ python3 scripts/top_dud_twitter_topics.py [--project NAME] [--limit 15] [--window-hours 168]
29
+
30
+ Routed through GET /api/v1/twitter-candidates/dud-topics. omit_rate and
31
+ last_skip_h_ago are computed server-side; the route returns the legacy CLI
32
+ contract directly so the Python wrapper is thin.
33
+ """
34
+ import argparse
35
+ import json
36
+ import os
37
+ import sys
38
+
39
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40
+ from http_api import api_get # noqa: E402
41
+
42
+
43
+ def main():
44
+ p = argparse.ArgumentParser()
45
+ p.add_argument("--project", default=None,
46
+ help="Filter to a single project (matches matched_project).")
47
+ p.add_argument("--limit", type=int, default=15)
48
+ p.add_argument("--window-hours", type=int, default=168,
49
+ help="Look back this many hours (default 7d).")
50
+ p.add_argument("--min-skips", type=int, default=3,
51
+ help="Suppress seeds with fewer than this many "
52
+ "skipped/expired candidates in the window "
53
+ "(default 3; below that the signal is too thin).")
54
+ args = p.parse_args()
55
+
56
+ query = {
57
+ "limit": args.limit,
58
+ "window_hours": args.window_hours,
59
+ "min_skips": args.min_skips,
60
+ }
61
+ if args.project:
62
+ query["project"] = args.project
63
+
64
+ resp = api_get("/api/v1/twitter-candidates/dud-topics", query=query)
65
+ rows = (resp.get("data") or {}).get("rows") or []
66
+
67
+ # Route already returns the legacy shape. Mirror keys explicitly so
68
+ # downstream callers see a stable contract even if the route adds
69
+ # extra fields later.
70
+ out = [
71
+ {
72
+ "search_topic": r.get("search_topic"),
73
+ "project": r.get("project") or "",
74
+ "posted_n": int(r.get("posted_n") or 0),
75
+ "skipped_n": int(r.get("skipped_n") or 0),
76
+ "avg_virality_posted": (
77
+ round(float(r["avg_virality_posted"]), 2)
78
+ if r.get("avg_virality_posted") is not None
79
+ else None
80
+ ),
81
+ "avg_virality_skipped": (
82
+ round(float(r["avg_virality_skipped"]), 2)
83
+ if r.get("avg_virality_skipped") is not None
84
+ else None
85
+ ),
86
+ "omit_rate": round(float(r.get("omit_rate") or 0), 2),
87
+ "last_skip_h_ago": (
88
+ round(float(r["last_skip_h_ago"]), 1)
89
+ if r.get("last_skip_h_ago") is not None
90
+ else None
91
+ ),
92
+ "sample_skip_reasons": r.get("sample_skip_reasons") or [],
93
+ }
94
+ for r in rows
95
+ ]
96
+
97
+ json.dump(out, sys.stdout)
98
+ print("", file=sys.stdout)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_linkedin_queries.py
4
+
5
+ Returns the top-performing historical LinkedIn search queries by how many
6
+ candidates they produced that actually got posted. Used as STYLE inspiration
7
+ for the LLM that drafts new queries, NOT as literal keyword reuse (LinkedIn
8
+ SERP shifts daily, so reusing the exact same query is wasteful).
9
+
10
+ Pair with top_dud_linkedin_queries.py (negative signal).
11
+
12
+ python3 scripts/top_linkedin_queries.py [--project NAME] [--search-topic TOPIC] [--limit 20] [--window-days 30]
13
+
14
+ Output: JSON list of {"query": ..., "project": ..., "search_topic": ..., "posts": N, "avg_velocity": X, "avg_serp_quality": Y}
15
+
16
+ Window default 30 days (vs Twitter's 14): LinkedIn cycle is sparser, longer
17
+ window captures enough samples.
18
+
19
+ Migrated 2026-06-01 from direct db.py SELECTs to the s4l.ai HTTP API
20
+ (GET /api/v1/linkedin-candidates/top-queries). No DATABASE_URL needed.
21
+ """
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
28
+ from http_api import api_get
29
+
30
+
31
+ def main():
32
+ p = argparse.ArgumentParser()
33
+ p.add_argument("--limit", type=int, default=20)
34
+ p.add_argument("--window-days", type=int, default=30)
35
+ p.add_argument("--project", default=None)
36
+ p.add_argument("--search-topic", default=None)
37
+ args = p.parse_args()
38
+
39
+ query = {
40
+ "limit": args.limit,
41
+ "window_days": args.window_days,
42
+ }
43
+ if args.project:
44
+ query["project"] = args.project
45
+ if args.search_topic:
46
+ query["search_topic"] = args.search_topic
47
+
48
+ resp = api_get("/api/v1/linkedin-candidates/top-queries", query)
49
+ out = (resp.get("data") or {}).get("queries") or []
50
+ json.dump(out, sys.stdout)
51
+ print("", file=sys.stdout)
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ top_omitted_reddit_topics.py
4
+
5
+ Returns recent Reddit search_topic seeds whose threads consistently survived
6
+ the ripen gate (numerical engagement check) but were then OMITTED by
7
+ post_reddit.py's draft-time SELECTION GATE (build_draft_prompt's bridge test).
8
+
9
+ Why this matters:
10
+ - top_search_topics.py = positive signal (seed -> posted -> engagement)
11
+ - top_dud_reddit_queries.py = "no results returned" signal (search dud)
12
+ - THIS = "results returned, ripen survived, draft gate killed them" signal
13
+ i.e. the seed is producing alive-but-unfit threads. Category-level
14
+ mismatch, the LLM should drop or rephrase that seed.
15
+
16
+ Output: JSON list (so build_discover_prompt can paste it directly), sorted
17
+ by most-omitted first:
18
+
19
+ [{"search_topic": "...", "project": "...",
20
+ "draft_omits": N, "ripen_survivors": M, "posted": P,
21
+ "omit_rate": 0.NN, "last_omit_h_ago": F.F,
22
+ "sample_subreddits": ["r/foo", "r/bar", ...]}]
23
+
24
+ Usage:
25
+ python3 scripts/top_omitted_reddit_topics.py [--project NAME] [--limit 15] [--window-hours 168]
26
+
27
+ Routed through GET /api/v1/reddit-candidates/omitted-topics on the
28
+ website. omit_rate and last_omit_h_ago are computed server-side; the
29
+ shape returned by the route already matches the legacy CLI contract,
30
+ so callers (build_discover_prompt et al.) don't need to change.
31
+ """
32
+ import argparse
33
+ import json
34
+ import os
35
+ import sys
36
+
37
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
38
+ from http_api import api_get
39
+
40
+
41
+ def main():
42
+ p = argparse.ArgumentParser()
43
+ p.add_argument("--project", default=None,
44
+ help="Filter to a single project (matches matched_project).")
45
+ p.add_argument("--limit", type=int, default=15)
46
+ p.add_argument("--window-hours", type=int, default=168,
47
+ help="Look back this many hours (default 7d).")
48
+ p.add_argument("--min-omits", type=int, default=1,
49
+ help="Suppress seeds with fewer than this many draft omits "
50
+ "in the window (default 1).")
51
+ args = p.parse_args()
52
+
53
+ query = {
54
+ "limit": args.limit,
55
+ "window_hours": args.window_hours,
56
+ "min_omits": args.min_omits,
57
+ }
58
+ if args.project:
59
+ query["project"] = args.project
60
+
61
+ resp = api_get("/api/v1/reddit-candidates/omitted-topics", query=query)
62
+ data = (resp or {}).get("data") or {}
63
+ rows = data.get("rows") or []
64
+
65
+ # Route already returns the legacy shape. Mirror keys explicitly so
66
+ # downstream callers see a stable contract even if the route adds
67
+ # extra fields later.
68
+ out = [
69
+ {
70
+ "search_topic": r.get("search_topic"),
71
+ "project": r.get("project") or "",
72
+ "draft_omits": int(r.get("draft_omits") or 0),
73
+ "ripen_survivors": int(r.get("ripen_survivors") or 0),
74
+ "posted": int(r.get("posted") or 0),
75
+ "omit_rate": round(float(r.get("omit_rate") or 0), 2),
76
+ "last_omit_h_ago": (
77
+ round(float(r["last_omit_h_ago"]), 1)
78
+ if r.get("last_omit_h_ago") is not None
79
+ else None
80
+ ),
81
+ "sample_subreddits": r.get("sample_subreddits") or [],
82
+ }
83
+ for r in rows
84
+ ]
85
+
86
+ json.dump(out, sys.stdout)
87
+ print("", file=sys.stdout)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()