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