@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,200 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ reddit_query_bank.py — programmatic discover-phase query bank for the Reddit cycle.
4
+
5
+ Reddit analog of scripts/qualified_query_bank.py (Twitter). Before 2026-06-01 the
6
+ Reddit discover phase spent a full Claude LLM session just to pick query phrasings
7
+ and fire `reddit_tools.py search` Bash calls in OPAQUE mode (Claude never even saw
8
+ the results). That LLM call added ~zero value: query selection + search execution
9
+ are both deterministic. This module replaces the "Claude picks queries" half so
10
+ discover can run fully in Python, matching Twitter (scan = deterministic Python,
11
+ Claude only drafts).
12
+
13
+ Where the queries come from (in priority order):
14
+ 1. PROVEN queries — /api/v1/search-topics/ranked?platform=reddit&project=X.
15
+ On Reddit, the harvested `search_topic` IS the raw query string that was run
16
+ (see post_reddit._discover_iteration harvest: search_topic = payload["query"]),
17
+ so the ranked-topics route already returns proven query phrasings with their
18
+ clicks / posts / composite. A row qualifies if it has produced at least one
19
+ posted candidate (posts > 0) OR at least one real click (clicks_total > 0).
20
+ Ranked clicks-first, then composite (clicks*100 + comments + upvotes).
21
+ 2. CONFIG seeds — config.json `search_topics` for the project (via
22
+ project_topics.topics_for_project). These give cold-start + coverage for
23
+ projects/angles that have not converted yet. Appended after proven queries,
24
+ deduped by normalized core so a seed that already converted isn't run twice.
25
+
26
+ Output (stdout, --json or default): a JSON list shaped like
27
+ [{"project": "...", "query": "...", "source": "proven|seed",
28
+ "clicks": <int>, "posts": <int>, "composite": <int>}, ...]
29
+ ranked strongest-first. `_discover_iteration` consumes the `query` field, caps to
30
+ S4L_REDDIT_MAX_SEARCHES, and runs each via reddit_tools.cmd_search.
31
+
32
+ Usage:
33
+ python3 scripts/reddit_query_bank.py --project fazm
34
+ python3 scripts/reddit_query_bank.py --project Podlog --limit 6 --json
35
+ python3 scripts/reddit_query_bank.py --project fazm --no-seeds # proven only
36
+ python3 scripts/reddit_query_bank.py --all # per-project sizes
37
+ """
38
+ import argparse
39
+ import json
40
+ import os
41
+ import re
42
+ import sys
43
+
44
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
45
+ from http_api import api_get # noqa: E402
46
+
47
+ try:
48
+ from project_topics import topics_for_project
49
+ except Exception: # pragma: no cover - defensive; bank still works proven-only
50
+ topics_for_project = None
51
+
52
+
53
+ def normalize(q: str) -> str:
54
+ """Collapse a query to a comparable core for dedup. Reddit queries carry no
55
+ per-cycle operators (no since:/min_faves), so this is just lowercase +
56
+ punctuation/whitespace normalization."""
57
+ q = (q or "").lower()
58
+ q = re.sub(r'["()]', "", q)
59
+ q = re.sub(r"\s+", " ", q).strip()
60
+ return q
61
+
62
+
63
+ def fetch_proven(project, window_days=30, limit=40):
64
+ """Proven reddit query phrasings from /api/v1/search-topics/ranked.
65
+
66
+ Returns bank rows for every ranked search_topic that has converted
67
+ (posts > 0) or driven a click (clicks_total > 0). Ranked clicks-first then
68
+ composite, mirroring the route's own ordering. NO direct-DB fallback (same
69
+ convention as qualified_query_bank.py)."""
70
+ q = {"platform": "reddit", "window_days": int(window_days), "limit": int(limit)}
71
+ if project:
72
+ q["project"] = project
73
+ try:
74
+ resp = api_get("/api/v1/search-topics/ranked", q)
75
+ except SystemExit as e:
76
+ print(f"reddit_query_bank: search-topics/ranked fetch failed for "
77
+ f"{project!r}: {e}", file=sys.stderr)
78
+ return []
79
+ rows = ((resp or {}).get("data") or {}).get("rows") or []
80
+ bank = []
81
+ for r in rows:
82
+ topic = (r.get("search_topic") or "").strip()
83
+ if not topic:
84
+ continue
85
+ clicks = int(r.get("clicks_total") or 0)
86
+ posts = int(r.get("posts") or 0)
87
+ composite = int(r.get("composite_score") or 0)
88
+ if posts <= 0 and clicks <= 0:
89
+ continue # never converted, never clicked — not "proven"
90
+ bank.append({
91
+ "project": project,
92
+ "query": topic,
93
+ "source": "proven",
94
+ "clicks": clicks,
95
+ "posts": posts,
96
+ "composite": composite,
97
+ })
98
+ bank.sort(key=lambda b: (b["clicks"], b["composite"], b["posts"]), reverse=True)
99
+ return bank
100
+
101
+
102
+ def seeds_from_config(project):
103
+ """config.json `search_topics` for the project as bank rows (source=seed,
104
+ zero stats so they sort below proven queries)."""
105
+ if not topics_for_project:
106
+ return []
107
+ try:
108
+ seeds = list(topics_for_project(project or "") or [])
109
+ except Exception as e:
110
+ print(f"reddit_query_bank: topics_for_project failed for {project!r}: {e}",
111
+ file=sys.stderr)
112
+ return []
113
+ out = []
114
+ for s in seeds:
115
+ s = (s or "").strip()
116
+ if not s:
117
+ continue
118
+ out.append({
119
+ "project": project,
120
+ "query": s,
121
+ "source": "seed",
122
+ "clicks": 0,
123
+ "posts": 0,
124
+ "composite": 0,
125
+ })
126
+ return out
127
+
128
+
129
+ def build_bank(project, limit=None, include_seeds=True, window_days=30):
130
+ """Proven queries first, then config seeds not already covered (deduped by
131
+ normalized core). Capped to `limit` if given."""
132
+ proven = fetch_proven(project, window_days=window_days)
133
+ seen = {normalize(b["query"]) for b in proven}
134
+ bank = list(proven)
135
+ if include_seeds:
136
+ for s in seeds_from_config(project):
137
+ core = normalize(s["query"])
138
+ if not core or core in seen:
139
+ continue
140
+ seen.add(core)
141
+ bank.append(s)
142
+ if limit:
143
+ bank = bank[: int(limit)]
144
+ return bank
145
+
146
+
147
+ def main():
148
+ ap = argparse.ArgumentParser()
149
+ ap.add_argument("--project", help="Project name (config.json casing).")
150
+ ap.add_argument("--limit", type=int, default=None,
151
+ help="Cap the bank to the top-N strongest queries.")
152
+ ap.add_argument("--window-days", type=int, default=30,
153
+ help="Lookback window for proven-query stats (default 30).")
154
+ ap.add_argument("--no-seeds", action="store_true",
155
+ help="Proven queries only; skip the config.json seed tail.")
156
+ ap.add_argument("--json", action="store_true",
157
+ help="Force JSON output (default is already JSON).")
158
+ ap.add_argument("--all", action="store_true",
159
+ help="Debug: print proven-bank size per project from config.json.")
160
+ args = ap.parse_args()
161
+
162
+ if args.all:
163
+ # Lazy import config so --project path has no dependency on it.
164
+ try:
165
+ import config_loader # type: ignore
166
+ projects = [p.get("name") for p in (config_loader.load() or {}).get("projects", [])]
167
+ except Exception:
168
+ cfg_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
169
+ "config.json")
170
+ with open(cfg_path) as f:
171
+ projects = [p.get("name") for p in (json.load(f) or {}).get("projects", [])]
172
+ out = []
173
+ for name in filter(None, projects):
174
+ proven = fetch_proven(name)
175
+ out.append({"project": name, "proven": len(proven),
176
+ "seeds": len(seeds_from_config(name))})
177
+ json.dump(out, sys.stdout, indent=2)
178
+ print()
179
+ return 0
180
+
181
+ if not args.project:
182
+ print("reddit_query_bank: --project required (or --all)", file=sys.stderr)
183
+ return 2
184
+
185
+ bank = build_bank(args.project, limit=args.limit,
186
+ include_seeds=not args.no_seeds,
187
+ window_days=args.window_days)
188
+ proven_n = sum(1 for b in bank if b["source"] == "proven")
189
+ seed_n = len(bank) - proven_n
190
+ json.dump(bank, sys.stdout)
191
+ print()
192
+ print(f"reddit_query_bank: {proven_n} proven + {seed_n} seed = {len(bank)} "
193
+ f"queries for project={args.project!r}"
194
+ f"{' (limit=' + str(args.limit) + ')' if args.limit else ''}",
195
+ file=sys.stderr)
196
+ return 0
197
+
198
+
199
+ if __name__ == "__main__":
200
+ sys.exit(main())
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """reddit_threads_helper.py — CLI wrapper used by skill/run-reddit-threads.sh
3
+ to replace the four inline `psql "$DATABASE_URL"` reads that built prompt
4
+ context. The direct-Postgres lane was removed 2026-06-01; DATABASE_URL is
5
+ deliberately ignored, no DB, no fallback. Each subcommand prints exactly what
6
+ the psql call printed (one row per line, `|`-delimited like psql -t -A) so the
7
+ surrounding shell capture ($(...)) is unchanged.
8
+
9
+ Subcommands:
10
+ recent-posts-sub --sub SLUG [--limit 10]
11
+ -> own threads in r/SLUG, newest first. Prints
12
+ "<thread_title> |ENDING| <last 200 chars of our_content>" per line.
13
+ recent-posts-project --project P [--days 14] [--limit 15]
14
+ -> own threads project-wide in the last N days, newest first. Same shape.
15
+ recent-styles --project P [--limit 5]
16
+ -> engagement_style of recent own threads (non-empty only), newest first.
17
+ top-posts --project P [--min-score 5] [--limit 10]
18
+ -> top own active threads by (upvotes + comments*3), highest first.
19
+ Prints "<thread_title>|<upvotes>|<comments_count>|<views>" per line.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import os
25
+ import sys
26
+ from datetime import datetime, timedelta, timezone
27
+
28
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
29
+ from http_api import api_get # noqa: E402
30
+
31
+
32
+ def _posts(query: dict) -> list:
33
+ resp = api_get("/api/v1/posts", query=query)
34
+ return (resp.get("data") or {}).get("posts") or []
35
+
36
+
37
+ def _ending_line(p: dict) -> str:
38
+ title = p.get("thread_title") or ""
39
+ content = p.get("our_content") or ""
40
+ return f"{title} |ENDING| {content[-200:]}"
41
+
42
+
43
+ def cmd_recent_posts_sub(sub: str, limit: int) -> int:
44
+ posts = _posts({
45
+ "platform": "reddit",
46
+ "own_threads_only": "true",
47
+ "thread_url_contains": f"/r/{sub}/",
48
+ "order_by": "posted_at",
49
+ "order_dir": "desc",
50
+ "limit": limit,
51
+ })
52
+ for p in posts:
53
+ print(_ending_line(p))
54
+ return 0
55
+
56
+
57
+ def cmd_recent_posts_project(project: str, days: int, limit: int) -> int:
58
+ since = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
59
+ posts = _posts({
60
+ "platform": "reddit",
61
+ "project": project,
62
+ "own_threads_only": "true",
63
+ "since": since,
64
+ "order_by": "posted_at",
65
+ "order_dir": "desc",
66
+ "limit": limit,
67
+ })
68
+ for p in posts:
69
+ print(_ending_line(p))
70
+ return 0
71
+
72
+
73
+ def cmd_recent_styles(project: str, limit: int) -> int:
74
+ # Over-fetch then filter to non-empty engagement_style, mirroring the old
75
+ # WHERE engagement_style IS NOT NULL AND != '' applied before LIMIT.
76
+ posts = _posts({
77
+ "platform": "reddit",
78
+ "project": project,
79
+ "own_threads_only": "true",
80
+ "order_by": "posted_at",
81
+ "order_dir": "desc",
82
+ "limit": max(limit * 10, 50),
83
+ })
84
+ printed = 0
85
+ for p in posts:
86
+ style = (p.get("engagement_style") or "").strip()
87
+ if not style:
88
+ continue
89
+ print(style)
90
+ printed += 1
91
+ if printed >= limit:
92
+ break
93
+ return 0
94
+
95
+
96
+ def cmd_top_posts(project: str, min_score: int, limit: int) -> int:
97
+ posts = _posts({
98
+ "platform": "reddit",
99
+ "project": project,
100
+ "own_threads_only": "true",
101
+ "status": "active",
102
+ "min_engagement_score": min_score,
103
+ "order_by": "engagement_score",
104
+ "limit": limit,
105
+ })
106
+ for p in posts:
107
+ title = p.get("thread_title") or ""
108
+ upvotes = p.get("upvotes") or 0
109
+ comments = p.get("comments_count") or 0
110
+ views = p.get("views")
111
+ views_str = "" if views is None else str(views)
112
+ print(f"{title}|{upvotes}|{comments}|{views_str}")
113
+ return 0
114
+
115
+
116
+ def main() -> int:
117
+ p = argparse.ArgumentParser()
118
+ sub = p.add_subparsers(dest="cmd", required=True)
119
+
120
+ ps = sub.add_parser("recent-posts-sub")
121
+ ps.add_argument("--sub", required=True)
122
+ ps.add_argument("--limit", type=int, default=10)
123
+
124
+ pp = sub.add_parser("recent-posts-project")
125
+ pp.add_argument("--project", required=True)
126
+ pp.add_argument("--days", type=int, default=14)
127
+ pp.add_argument("--limit", type=int, default=15)
128
+
129
+ pst = sub.add_parser("recent-styles")
130
+ pst.add_argument("--project", required=True)
131
+ pst.add_argument("--limit", type=int, default=5)
132
+
133
+ pt = sub.add_parser("top-posts")
134
+ pt.add_argument("--project", required=True)
135
+ pt.add_argument("--min-score", type=int, default=5)
136
+ pt.add_argument("--limit", type=int, default=10)
137
+
138
+ args = p.parse_args()
139
+ if args.cmd == "recent-posts-sub":
140
+ return cmd_recent_posts_sub(args.sub, args.limit)
141
+ if args.cmd == "recent-posts-project":
142
+ return cmd_recent_posts_project(args.project, args.days, args.limit)
143
+ if args.cmd == "recent-styles":
144
+ return cmd_recent_styles(args.project, args.limit)
145
+ if args.cmd == "top-posts":
146
+ return cmd_top_posts(args.project, args.min_score, args.limit)
147
+ return 1
148
+
149
+
150
+ if __name__ == "__main__":
151
+ sys.exit(main())