@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,287 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
twitter_gen_links.py — Phase 2b-gen helper for run-twitter-cycle.sh.
|
|
4
|
+
|
|
5
|
+
Reads a candidate plan JSON file produced by Phase 2b-prep, generates the
|
|
6
|
+
matching landing-page (or falls back to the plain project URL) for each
|
|
7
|
+
candidate, and writes the file back with a `link_url` field per candidate.
|
|
8
|
+
|
|
9
|
+
The browser lock is NOT held while this runs. generate_page.py is pure HTTP +
|
|
10
|
+
git + Cloud-Run-deploy work, no twitter-harness browser use, so other twitter
|
|
11
|
+
pipelines can use the browser during the 10-40 minute landing-page build.
|
|
12
|
+
|
|
13
|
+
Plan file shape (in/out):
|
|
14
|
+
{
|
|
15
|
+
"candidates": [
|
|
16
|
+
{
|
|
17
|
+
"candidate_id": int,
|
|
18
|
+
"candidate_url": str,
|
|
19
|
+
"thread_author": str,
|
|
20
|
+
"thread_text": str,
|
|
21
|
+
"matched_project": str,
|
|
22
|
+
"reply_text": str,
|
|
23
|
+
"engagement_style": str,
|
|
24
|
+
"language": str,
|
|
25
|
+
"has_landing_pages": bool,
|
|
26
|
+
"link_keyword": str, # only when has_landing_pages=true
|
|
27
|
+
"link_slug": str, # only when has_landing_pages=true
|
|
28
|
+
...
|
|
29
|
+
# Written by THIS script:
|
|
30
|
+
"link_url": str, # final URL to embed in the reply (may be "")
|
|
31
|
+
"link_source": str, # seo_page | plain_url_fallback | plain_url_no_lp |
|
|
32
|
+
# plain_url_timeout_fallback | empty
|
|
33
|
+
},
|
|
34
|
+
...
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
python3 twitter_gen_links.py --plan /tmp/twitter_cycle_plan_<batch>.json
|
|
40
|
+
|
|
41
|
+
Exits 0 on best-effort completion (each candidate gets a link_url, even if
|
|
42
|
+
generation failed; the fallback chain protects the cycle from blocking on
|
|
43
|
+
SEO infra issues). Exits non-zero only when the plan file itself is unreadable
|
|
44
|
+
or empty.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import argparse
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import random
|
|
51
|
+
import subprocess
|
|
52
|
+
import sys
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
56
|
+
import audience_pages as audience_pages_mod # noqa: E402
|
|
57
|
+
|
|
58
|
+
REPO_DIR = os.path.expanduser("~/social-autoposter")
|
|
59
|
+
GENERATE_PAGE = os.path.join(REPO_DIR, "seo", "generate_page.py")
|
|
60
|
+
CONFIG_PATH = os.path.join(REPO_DIR, "config.json")
|
|
61
|
+
GEN_TIMEOUT_SEC = 3600 # 60 min per page; observed legit runs take 45-50 min
|
|
62
|
+
# (pre-Claude inventory + decision + improve/new pipeline +
|
|
63
|
+
# deploy verify). Don't lower without re-measuring.
|
|
64
|
+
MAX_AB_HITS_PER_CYCLE = 2 # cap cumulative gen budget at ~2 x 60 min worst case
|
|
65
|
+
# so cycle has room under the 180-min watchdog cap.
|
|
66
|
+
|
|
67
|
+
# A/B gate: per-candidate coin flip for the page-gen lane. 0.25 means 25% of
|
|
68
|
+
# eligible candidates (project has landing_pages config + LLM provided
|
|
69
|
+
# keyword/slug) actually trigger generate_page.py; the rest fall through to
|
|
70
|
+
# the plain project URL with link_source='plain_url_ab_skip'. Tunable via
|
|
71
|
+
# env var so cadence can be swept without a code change. 0.0 disables
|
|
72
|
+
# page-gen entirely; 1.0 restores the pre-A/B behaviour.
|
|
73
|
+
def _page_gen_rate() -> float:
|
|
74
|
+
# Bumped from 0.25 -> 0.30 on 2026-05-08 after CTA pipeline review:
|
|
75
|
+
# /t/ pages convert better than /r/ short-link-only fallbacks (Reddit data
|
|
76
|
+
# showed 17-71% click->signup vs 0% on plain_url_ab_skip). Bumping the
|
|
77
|
+
# default rate gives Twitter a higher share of full landing pages while
|
|
78
|
+
# still leaving 70% on the cheap path for budget reasons. See chat note
|
|
79
|
+
# 2026-05-07 "link suffix pipeline rewrite".
|
|
80
|
+
raw = os.environ.get("TWITTER_PAGE_GEN_RATE", "0.0")
|
|
81
|
+
try:
|
|
82
|
+
v = float(raw)
|
|
83
|
+
except ValueError:
|
|
84
|
+
return 0.30
|
|
85
|
+
if v < 0.0:
|
|
86
|
+
return 0.0
|
|
87
|
+
if v > 1.0:
|
|
88
|
+
return 1.0
|
|
89
|
+
return v
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_projects() -> dict:
|
|
93
|
+
"""Map name -> project dict."""
|
|
94
|
+
with open(CONFIG_PATH) as f:
|
|
95
|
+
cfg = json.load(f)
|
|
96
|
+
return {p["name"]: p for p in cfg.get("projects", [])}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_generate(product: str, keyword: str, slug: str) -> tuple[str, str]:
|
|
100
|
+
"""Run generate_page.py for a single candidate.
|
|
101
|
+
|
|
102
|
+
Returns (page_url, source_tag). On success: (real_url, "seo_page"). On
|
|
103
|
+
any failure: ("", "<reason>") so the caller can fall back to the plain URL.
|
|
104
|
+
"""
|
|
105
|
+
cmd = [
|
|
106
|
+
"python3",
|
|
107
|
+
GENERATE_PAGE,
|
|
108
|
+
"--product", product,
|
|
109
|
+
"--keyword", keyword,
|
|
110
|
+
"--slug", slug,
|
|
111
|
+
"--trigger", "twitter",
|
|
112
|
+
]
|
|
113
|
+
print(f"[gen] product={product} keyword={keyword!r} slug={slug!r}", flush=True)
|
|
114
|
+
try:
|
|
115
|
+
r = subprocess.run(cmd, capture_output=True, text=True, timeout=GEN_TIMEOUT_SEC)
|
|
116
|
+
except subprocess.TimeoutExpired:
|
|
117
|
+
print(f"[gen] TIMEOUT after {GEN_TIMEOUT_SEC}s", flush=True)
|
|
118
|
+
return ("", "timeout")
|
|
119
|
+
print(f"[gen] exit={r.returncode}", flush=True)
|
|
120
|
+
if r.stderr:
|
|
121
|
+
# Trail-truncate so we don't blow out the cycle log on a verbose failure.
|
|
122
|
+
print("[gen][stderr-tail]", r.stderr[-2000:], flush=True)
|
|
123
|
+
# generate_page.py prints its final result via json.dumps(result, indent=2),
|
|
124
|
+
# so the success object is a pretty-printed multi-line block. Scan stdout
|
|
125
|
+
# for every top-level JSON object via JSONDecoder.raw_decode and keep the
|
|
126
|
+
# last dict we can parse: that's the final result line regardless of
|
|
127
|
+
# whether it was emitted as one line or many.
|
|
128
|
+
page_url = ""
|
|
129
|
+
last_obj = None
|
|
130
|
+
decoder = json.JSONDecoder()
|
|
131
|
+
text = r.stdout
|
|
132
|
+
i = text.find("{")
|
|
133
|
+
while i != -1:
|
|
134
|
+
try:
|
|
135
|
+
obj, end = decoder.raw_decode(text, i)
|
|
136
|
+
except json.JSONDecodeError:
|
|
137
|
+
i = text.find("{", i + 1)
|
|
138
|
+
continue
|
|
139
|
+
if isinstance(obj, dict):
|
|
140
|
+
last_obj = obj
|
|
141
|
+
i = text.find("{", end)
|
|
142
|
+
if last_obj and last_obj.get("success") and last_obj.get("page_url"):
|
|
143
|
+
page_url = last_obj["page_url"]
|
|
144
|
+
if not page_url:
|
|
145
|
+
print("[gen] no page_url in stdout; tail=", flush=True)
|
|
146
|
+
print(r.stdout[-2000:], flush=True)
|
|
147
|
+
return ("", "no_page_url")
|
|
148
|
+
return (page_url, "seo_page")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_link(candidate: dict, projects: dict, page_gen_rate: float) -> tuple[str, str]:
|
|
152
|
+
"""Decide the link URL for a single candidate.
|
|
153
|
+
|
|
154
|
+
Order of preference:
|
|
155
|
+
1. CURATED AUDIENCE PAGE (landing_pages.audience_pages) — wins outright
|
|
156
|
+
when the candidate's link_keyword / search_topic / reply_text matches
|
|
157
|
+
any entry's match_keywords. Skips the A/B gate entirely; curated pages
|
|
158
|
+
are higher-quality than auto-generated /t/<slug> pages.
|
|
159
|
+
2. SEO page (when has_landing_pages AND dice lands in gen lane)
|
|
160
|
+
3. plain project URL
|
|
161
|
+
4. ""
|
|
162
|
+
|
|
163
|
+
The per-candidate dice roll (random.random() < page_gen_rate) only fires
|
|
164
|
+
for projects that actually support landing pages and where the LLM
|
|
165
|
+
supplied a keyword + slug. Eligible-but-lost candidates surface as
|
|
166
|
+
link_source='plain_url_ab_skip' so post-hoc engagement analysis can
|
|
167
|
+
compare the two lanes apples-to-apples.
|
|
168
|
+
|
|
169
|
+
Audience-page hits surface as link_source='audience_page:<angle>' so the
|
|
170
|
+
dashboard and stats can break out curated-page traffic separately.
|
|
171
|
+
"""
|
|
172
|
+
proj_name = candidate.get("matched_project") or ""
|
|
173
|
+
proj = projects.get(proj_name) or {}
|
|
174
|
+
# Personal-brand (persona) lane is link-free by definition. Self-promotion
|
|
175
|
+
# mode is pure organic engagement: no company, no signup, no profile URL. Any
|
|
176
|
+
# `website`/`url` a persona project happens to carry (some installs got the
|
|
177
|
+
# user's own X profile written there) must NEVER become a tail link. Enforce
|
|
178
|
+
# it here at the single source so no downstream surface (review card, manual
|
|
179
|
+
# post_drafts) has to strip a link that should never have been generated.
|
|
180
|
+
if proj.get("persona"):
|
|
181
|
+
return ("", "persona_no_link")
|
|
182
|
+
plain_url = proj.get("website") or proj.get("url") or ""
|
|
183
|
+
has_lp = bool(candidate.get("has_landing_pages"))
|
|
184
|
+
keyword = (candidate.get("link_keyword") or "").strip()
|
|
185
|
+
slug = (candidate.get("link_slug") or "").strip()
|
|
186
|
+
|
|
187
|
+
# (1) Curated audience-page short-circuit. Runs BEFORE the A/B gate so a
|
|
188
|
+
# well-targeted curated page always beats a freshly-spun SEO /t/<slug>.
|
|
189
|
+
# Signals checked: link_keyword (LLM nomination), search_topic (the topic
|
|
190
|
+
# bucket the candidate was discovered under), reply_text (the actual draft),
|
|
191
|
+
# and thread_title (raw thread title from Twitter). First match wins per
|
|
192
|
+
# the audience_pages list order in config.json.
|
|
193
|
+
audience_hit = audience_pages_mod.match_by_keyword(
|
|
194
|
+
proj_name,
|
|
195
|
+
keyword=keyword,
|
|
196
|
+
topic=candidate.get("search_topic"),
|
|
197
|
+
reply_text=candidate.get("reply_text"),
|
|
198
|
+
thread_title=candidate.get("thread_title") or candidate.get("thread_text"),
|
|
199
|
+
)
|
|
200
|
+
if audience_hit:
|
|
201
|
+
angle = audience_hit.get("angle") or "unknown"
|
|
202
|
+
url = audience_hit.get("url") or ""
|
|
203
|
+
if url:
|
|
204
|
+
print(f"[gen] audience_page hit: angle={angle} url={url} "
|
|
205
|
+
f"(skipping A/B page-gen)", flush=True)
|
|
206
|
+
return (url, f"audience_page:{angle}")
|
|
207
|
+
|
|
208
|
+
if proj.get("page_gen_disabled"):
|
|
209
|
+
print(f"[gen] page_gen_disabled=true for {proj_name}; using plain URL", flush=True)
|
|
210
|
+
return (plain_url, "plain_url_no_lp")
|
|
211
|
+
|
|
212
|
+
if has_lp and keyword and slug and proj.get("landing_pages"):
|
|
213
|
+
roll = random.random()
|
|
214
|
+
if roll >= page_gen_rate:
|
|
215
|
+
print(f"[gen] AB skip: roll={roll:.3f} >= rate={page_gen_rate:.3f}; "
|
|
216
|
+
f"using plain URL", flush=True)
|
|
217
|
+
if plain_url:
|
|
218
|
+
return (plain_url, "plain_url_ab_skip")
|
|
219
|
+
return ("", "empty_ab_skip")
|
|
220
|
+
print(f"[gen] AB hit: roll={roll:.3f} < rate={page_gen_rate:.3f}; "
|
|
221
|
+
f"running generate_page.py", flush=True)
|
|
222
|
+
page_url, source = run_generate(proj_name, keyword, slug)
|
|
223
|
+
if page_url:
|
|
224
|
+
return (page_url, "seo_page")
|
|
225
|
+
# Fell through; fall back to plain project URL.
|
|
226
|
+
if plain_url:
|
|
227
|
+
return (plain_url, f"plain_url_fallback:{source}")
|
|
228
|
+
return ("", f"empty:{source}")
|
|
229
|
+
# No landing-pages config or LLM didn't supply keyword/slug.
|
|
230
|
+
if plain_url:
|
|
231
|
+
return (plain_url, "plain_url_no_lp")
|
|
232
|
+
return ("", "empty")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main() -> int:
|
|
236
|
+
ap = argparse.ArgumentParser()
|
|
237
|
+
ap.add_argument("--plan", required=True,
|
|
238
|
+
help="Path to the plan JSON file (read+rewrite in place)")
|
|
239
|
+
args = ap.parse_args()
|
|
240
|
+
|
|
241
|
+
plan_path = Path(args.plan)
|
|
242
|
+
if not plan_path.exists():
|
|
243
|
+
print(f"[gen] plan file not found: {plan_path}", file=sys.stderr)
|
|
244
|
+
return 2
|
|
245
|
+
try:
|
|
246
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8"))
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print(f"[gen] plan file unreadable: {e}", file=sys.stderr)
|
|
249
|
+
return 2
|
|
250
|
+
|
|
251
|
+
candidates = plan.get("candidates") or []
|
|
252
|
+
if not candidates:
|
|
253
|
+
print("[gen] plan has 0 candidates; nothing to do", flush=True)
|
|
254
|
+
return 0
|
|
255
|
+
|
|
256
|
+
projects = load_projects()
|
|
257
|
+
page_gen_rate = _page_gen_rate()
|
|
258
|
+
print(f"[gen] page_gen_rate={page_gen_rate:.3f} "
|
|
259
|
+
f"(env TWITTER_PAGE_GEN_RATE)", flush=True)
|
|
260
|
+
print(f"[gen] max_ab_hits_per_cycle={MAX_AB_HITS_PER_CYCLE} "
|
|
261
|
+
f"timeout_per_call_sec={GEN_TIMEOUT_SEC}", flush=True)
|
|
262
|
+
|
|
263
|
+
ab_hits = 0
|
|
264
|
+
for c in candidates:
|
|
265
|
+
cap_reached = ab_hits >= MAX_AB_HITS_PER_CYCLE
|
|
266
|
+
if cap_reached:
|
|
267
|
+
print(f"[gen] AB cap reached ({ab_hits}/"
|
|
268
|
+
f"{MAX_AB_HITS_PER_CYCLE}); forcing plain URL", flush=True)
|
|
269
|
+
link_url, source = resolve_link(c, projects,
|
|
270
|
+
0.0 if cap_reached else page_gen_rate)
|
|
271
|
+
if source == "seo_page":
|
|
272
|
+
ab_hits += 1
|
|
273
|
+
elif cap_reached and source == "plain_url_ab_skip":
|
|
274
|
+
source = "plain_url_ab_cap"
|
|
275
|
+
c["link_url"] = link_url
|
|
276
|
+
c["link_source"] = source
|
|
277
|
+
print(f"[gen] candidate_id={c.get('candidate_id')} "
|
|
278
|
+
f"link_url={link_url!r} source={source}", flush=True)
|
|
279
|
+
|
|
280
|
+
plan_path.write_text(json.dumps(plan, indent=2), encoding="utf-8")
|
|
281
|
+
print(f"[gen] plan rewritten with link_url for {len(candidates)} candidates",
|
|
282
|
+
flush=True)
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
sys.exit(main())
|