@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.
- package/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1336 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +513 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- 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())
|