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