@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,312 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Persist prospect profile data to the prospects table.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
upsert - Insert or update a prospect row and return the prospect_id.
|
|
6
|
+
get - Print a prospect row as JSON (for use from shell/Claude prompts).
|
|
7
|
+
link - Link an existing dms row to a prospect by platform+author.
|
|
8
|
+
|
|
9
|
+
The scraping itself is driven by Claude via the per-platform MCP browser
|
|
10
|
+
agents (reddit-agent, twitter-harness, linkedin-agent). This script only
|
|
11
|
+
handles DB persistence: Claude collects the fields and passes them in.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 fetch_prospect_profile.py upsert \\
|
|
15
|
+
--platform linkedin --author "Karl Treen" \\
|
|
16
|
+
--profile-url https://linkedin.com/in/karltreen \\
|
|
17
|
+
--headline "CEO at Foo" --bio "..." --company Foo --role CEO
|
|
18
|
+
|
|
19
|
+
python3 fetch_prospect_profile.py get --platform linkedin --author "Karl Treen"
|
|
20
|
+
|
|
21
|
+
python3 fetch_prospect_profile.py link --dm-id 510
|
|
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
|
+
import db as dbmod
|
|
30
|
+
|
|
31
|
+
# Columns on the prospects table that callers may set (besides platform/author).
|
|
32
|
+
UPDATABLE_COLS = [
|
|
33
|
+
"profile_url",
|
|
34
|
+
"display_name",
|
|
35
|
+
"headline",
|
|
36
|
+
"bio",
|
|
37
|
+
"follower_count",
|
|
38
|
+
"recent_activity",
|
|
39
|
+
"company",
|
|
40
|
+
"role",
|
|
41
|
+
"notes",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def upsert_prospect(conn, platform, author, fields):
|
|
46
|
+
"""Insert (platform, author) if missing, then update any provided fields.
|
|
47
|
+
|
|
48
|
+
Always stamps profile_fetched_at=NOW() when any field is provided.
|
|
49
|
+
Returns the prospect_id.
|
|
50
|
+
"""
|
|
51
|
+
# Ensure row exists.
|
|
52
|
+
conn.execute(
|
|
53
|
+
"""
|
|
54
|
+
INSERT INTO prospects (platform, author)
|
|
55
|
+
VALUES (%s, %s)
|
|
56
|
+
ON CONFLICT ON CONSTRAINT prospects_platform_author_unique DO NOTHING
|
|
57
|
+
""",
|
|
58
|
+
(platform, author),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Fetch id.
|
|
62
|
+
cur = conn.execute(
|
|
63
|
+
"SELECT id FROM prospects WHERE platform=%s AND author=%s",
|
|
64
|
+
(platform, author),
|
|
65
|
+
)
|
|
66
|
+
row = cur.fetchone()
|
|
67
|
+
if not row:
|
|
68
|
+
conn.commit()
|
|
69
|
+
cur = conn.execute(
|
|
70
|
+
"SELECT id FROM prospects WHERE platform=%s AND author=%s",
|
|
71
|
+
(platform, author),
|
|
72
|
+
)
|
|
73
|
+
row = cur.fetchone()
|
|
74
|
+
prospect_id = row["id"]
|
|
75
|
+
|
|
76
|
+
# Apply any non-null, non-empty field updates.
|
|
77
|
+
sets = []
|
|
78
|
+
params = []
|
|
79
|
+
for col in UPDATABLE_COLS:
|
|
80
|
+
val = fields.get(col)
|
|
81
|
+
if val is None:
|
|
82
|
+
continue
|
|
83
|
+
if isinstance(val, str) and val.strip() == "":
|
|
84
|
+
continue
|
|
85
|
+
sets.append(f"{col} = %s")
|
|
86
|
+
params.append(val)
|
|
87
|
+
|
|
88
|
+
if sets:
|
|
89
|
+
sets.append("profile_fetched_at = NOW()")
|
|
90
|
+
sql = f"UPDATE prospects SET {', '.join(sets)} WHERE id = %s"
|
|
91
|
+
params.append(prospect_id)
|
|
92
|
+
conn.execute(sql, params)
|
|
93
|
+
|
|
94
|
+
conn.commit()
|
|
95
|
+
return prospect_id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_prospect(conn, platform, author):
|
|
99
|
+
cur = conn.execute(
|
|
100
|
+
"""
|
|
101
|
+
SELECT id, platform, author, profile_url, display_name, headline, bio,
|
|
102
|
+
follower_count, recent_activity, company, role,
|
|
103
|
+
profile_fetched_at, notes, created_at
|
|
104
|
+
FROM prospects WHERE platform=%s AND author=%s
|
|
105
|
+
""",
|
|
106
|
+
(platform, author),
|
|
107
|
+
)
|
|
108
|
+
row = cur.fetchone()
|
|
109
|
+
if not row:
|
|
110
|
+
return None
|
|
111
|
+
d = dict(row)
|
|
112
|
+
for k, v in d.items():
|
|
113
|
+
if hasattr(v, "isoformat"):
|
|
114
|
+
d[k] = v.isoformat()
|
|
115
|
+
return d
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def link_dm(conn, dm_id):
|
|
119
|
+
"""Link dms.prospect_id to the matching prospect row by (platform, their_author)."""
|
|
120
|
+
cur = conn.execute(
|
|
121
|
+
"SELECT platform, their_author FROM dms WHERE id=%s",
|
|
122
|
+
(dm_id,),
|
|
123
|
+
)
|
|
124
|
+
row = cur.fetchone()
|
|
125
|
+
if not row:
|
|
126
|
+
print(f"ERROR: DM #{dm_id} not found", file=sys.stderr)
|
|
127
|
+
return None
|
|
128
|
+
platform = row["platform"]
|
|
129
|
+
author = row["their_author"]
|
|
130
|
+
|
|
131
|
+
cur = conn.execute(
|
|
132
|
+
"SELECT id FROM prospects WHERE platform=%s AND author=%s",
|
|
133
|
+
(platform, author),
|
|
134
|
+
)
|
|
135
|
+
prow = cur.fetchone()
|
|
136
|
+
if not prow:
|
|
137
|
+
print(
|
|
138
|
+
f"ERROR: no prospect row for {platform}:{author}; run `upsert` first",
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
prospect_id = prow["id"]
|
|
144
|
+
conn.execute(
|
|
145
|
+
"UPDATE dms SET prospect_id=%s WHERE id=%s", (prospect_id, dm_id)
|
|
146
|
+
)
|
|
147
|
+
conn.commit()
|
|
148
|
+
return prospect_id
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _http_upsert(args):
|
|
152
|
+
"""DB-free upsert via POST /api/v1/prospects. Returns prospect_id (int).
|
|
153
|
+
|
|
154
|
+
Mirrors upsert_prospect(): we always create-or-update the (platform,author)
|
|
155
|
+
row and stamp profile_fetched_at only when at least one field was supplied.
|
|
156
|
+
"""
|
|
157
|
+
from http_api import api_post
|
|
158
|
+
|
|
159
|
+
field_map = {
|
|
160
|
+
"profile_url": args.profile_url,
|
|
161
|
+
"display_name": args.display_name,
|
|
162
|
+
"headline": args.headline,
|
|
163
|
+
"bio": args.bio,
|
|
164
|
+
"follower_count": args.follower_count,
|
|
165
|
+
"recent_activity": args.recent_activity,
|
|
166
|
+
"company": args.company,
|
|
167
|
+
"role": args.role,
|
|
168
|
+
"notes": args.notes,
|
|
169
|
+
}
|
|
170
|
+
body = {"platform": args.platform, "author": args.author}
|
|
171
|
+
has_field = False
|
|
172
|
+
for k, v in field_map.items():
|
|
173
|
+
if v is None:
|
|
174
|
+
continue
|
|
175
|
+
if isinstance(v, str) and v.strip() == "":
|
|
176
|
+
continue
|
|
177
|
+
body[k] = v
|
|
178
|
+
has_field = True
|
|
179
|
+
# Match the DB path: profile_fetched_at is stamped only when a field was set.
|
|
180
|
+
if has_field:
|
|
181
|
+
body["profile_fetched_at_now"] = True
|
|
182
|
+
|
|
183
|
+
resp = api_post("/api/v1/prospects", body)
|
|
184
|
+
prospect = (resp.get("data") or {}).get("prospect") or {}
|
|
185
|
+
return prospect.get("id")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _http_get(args):
|
|
189
|
+
"""DB-free get via GET /api/v1/prospects?platform&author. Returns dict|None."""
|
|
190
|
+
from http_api import api_get
|
|
191
|
+
|
|
192
|
+
resp = api_get(
|
|
193
|
+
"/api/v1/prospects",
|
|
194
|
+
query={"platform": args.platform, "author": args.author},
|
|
195
|
+
ok_on_404=True,
|
|
196
|
+
)
|
|
197
|
+
if not resp.get("ok"):
|
|
198
|
+
return None
|
|
199
|
+
return (resp.get("data") or {}).get("prospect")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _http_link_dm(dm_id):
|
|
203
|
+
"""DB-free link via GET dms -> GET prospect -> PATCH dms.prospect_id.
|
|
204
|
+
|
|
205
|
+
Returns prospect_id (int) or None, printing the same stderr the DB path does.
|
|
206
|
+
"""
|
|
207
|
+
from http_api import api_get, api_patch
|
|
208
|
+
|
|
209
|
+
dm_resp = api_get(f"/api/v1/dms/{dm_id}", ok_on_404=True)
|
|
210
|
+
if not dm_resp.get("ok"):
|
|
211
|
+
print(f"ERROR: DM #{dm_id} not found", file=sys.stderr)
|
|
212
|
+
return None
|
|
213
|
+
dm = (dm_resp.get("data") or {}).get("dm") or {}
|
|
214
|
+
platform = dm.get("platform")
|
|
215
|
+
author = dm.get("their_author")
|
|
216
|
+
|
|
217
|
+
p_resp = api_get(
|
|
218
|
+
"/api/v1/prospects",
|
|
219
|
+
query={"platform": platform, "author": author},
|
|
220
|
+
ok_on_404=True,
|
|
221
|
+
)
|
|
222
|
+
prospect = (p_resp.get("data") or {}).get("prospect") if p_resp.get("ok") else None
|
|
223
|
+
if not prospect:
|
|
224
|
+
print(
|
|
225
|
+
f"ERROR: no prospect row for {platform}:{author}; run `upsert` first",
|
|
226
|
+
file=sys.stderr,
|
|
227
|
+
)
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
prospect_id = prospect.get("id")
|
|
231
|
+
api_patch(f"/api/v1/dms/{dm_id}", {"prospect_id": prospect_id})
|
|
232
|
+
return prospect_id
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _http_dispatch(args):
|
|
236
|
+
"""Handle a subcommand over HTTP when DATABASE_URL is absent.
|
|
237
|
+
|
|
238
|
+
Prints identical stdout to the DB path. Exits the process on the same
|
|
239
|
+
conditions (get-miss -> exit 1; link-failure -> exit 1).
|
|
240
|
+
"""
|
|
241
|
+
from http_api import api_patch
|
|
242
|
+
|
|
243
|
+
if args.cmd == "upsert":
|
|
244
|
+
pid = _http_upsert(args)
|
|
245
|
+
if args.link_dm is not None:
|
|
246
|
+
api_patch(f"/api/v1/dms/{args.link_dm}", {"prospect_id": pid})
|
|
247
|
+
if args.json:
|
|
248
|
+
out = _http_get(args) or {"id": pid}
|
|
249
|
+
print(json.dumps(out))
|
|
250
|
+
else:
|
|
251
|
+
print(f"prospect_id={pid}")
|
|
252
|
+
return
|
|
253
|
+
if args.cmd == "get":
|
|
254
|
+
row = _http_get(args)
|
|
255
|
+
if row is None:
|
|
256
|
+
print("null")
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
print(json.dumps(row, indent=2))
|
|
259
|
+
return
|
|
260
|
+
if args.cmd == "link":
|
|
261
|
+
pid = _http_link_dm(args.dm_id)
|
|
262
|
+
if pid is None:
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
print(f"prospect_id={pid} linked to DM #{args.dm_id}")
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main():
|
|
269
|
+
ap = argparse.ArgumentParser()
|
|
270
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
271
|
+
|
|
272
|
+
up = sub.add_parser("upsert", help="Insert or update a prospect row")
|
|
273
|
+
up.add_argument("--platform", required=True, choices=["reddit", "twitter", "linkedin"])
|
|
274
|
+
up.add_argument("--author", required=True)
|
|
275
|
+
up.add_argument("--profile-url")
|
|
276
|
+
up.add_argument("--display-name")
|
|
277
|
+
up.add_argument("--headline")
|
|
278
|
+
up.add_argument("--bio")
|
|
279
|
+
up.add_argument("--follower-count", type=int)
|
|
280
|
+
up.add_argument("--recent-activity")
|
|
281
|
+
up.add_argument("--company")
|
|
282
|
+
up.add_argument("--role")
|
|
283
|
+
up.add_argument("--notes")
|
|
284
|
+
up.add_argument(
|
|
285
|
+
"--link-dm",
|
|
286
|
+
type=int,
|
|
287
|
+
help="Also set dms.prospect_id on this dm_id after upsert",
|
|
288
|
+
)
|
|
289
|
+
up.add_argument(
|
|
290
|
+
"--json", action="store_true", help="Emit {id,platform,author,...} as JSON"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
gp = sub.add_parser("get", help="Print a prospect row as JSON")
|
|
294
|
+
gp.add_argument("--platform", required=True)
|
|
295
|
+
gp.add_argument("--author", required=True)
|
|
296
|
+
|
|
297
|
+
lk = sub.add_parser("link", help="Link a dms row to its prospect by platform+author")
|
|
298
|
+
lk.add_argument("--dm-id", type=int, required=True)
|
|
299
|
+
|
|
300
|
+
args = ap.parse_args()
|
|
301
|
+
|
|
302
|
+
# HTTP-only lane: every subcommand routes through the s4l.ai HTTP API. The
|
|
303
|
+
# direct-Postgres lane was removed 2026-06-01 — there is NO database-driven
|
|
304
|
+
# path any more, not as primary, not as fallback. DATABASE_URL, if present
|
|
305
|
+
# in the environment, is deliberately ignored; all reads/writes go through
|
|
306
|
+
# _http_dispatch against /api/v1/prospects and /api/v1/dms.
|
|
307
|
+
dbmod.load_env()
|
|
308
|
+
_http_dispatch(args)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
main()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
fetch_twitter_t1.py
|
|
4
|
+
|
|
5
|
+
Phase 2 of the twitter-cycle. Re-polls fxtwitter for every candidate in a
|
|
6
|
+
given batch_id, writes T1 engagement columns and computes delta_score.
|
|
7
|
+
|
|
8
|
+
python3 scripts/fetch_twitter_t1.py --batch-id <id>
|
|
9
|
+
|
|
10
|
+
delta_score formula:
|
|
11
|
+
Δlikes + 3*Δretweets + 2*Δreplies + Δviews/1000 + Δbookmarks
|
|
12
|
+
Weights picked so retweets/replies (stronger virality signals) beat raw likes,
|
|
13
|
+
views are divided down so they don't dominate.
|
|
14
|
+
|
|
15
|
+
Migrated 2026-05-18: pending-batch read and per-row T1 writes now go through
|
|
16
|
+
the s4l.ai HTTP API (/api/v1/twitter-candidates/pending-batch +
|
|
17
|
+
/api/v1/twitter-candidates/by-id action=set_t1) instead of psycopg2.
|
|
18
|
+
"""
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import urllib.request
|
|
25
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
from http_api import api_get, api_patch # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_fxtwitter(handle, tweet_id):
|
|
32
|
+
url = f"https://api.fxtwitter.com/{handle}/status/{tweet_id}"
|
|
33
|
+
req = urllib.request.Request(url, headers={"User-Agent": "social-autoposter/1.0"})
|
|
34
|
+
try:
|
|
35
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
36
|
+
return json.loads(resp.read())
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f" fxtwitter error for {handle}/{tweet_id}: {e}", file=sys.stderr)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse(url):
|
|
43
|
+
m = re.search(r"x\.com/([^/]+)/status/(\d+)", url or "")
|
|
44
|
+
if not m:
|
|
45
|
+
m = re.search(r"twitter\.com/([^/]+)/status/(\d+)", url or "")
|
|
46
|
+
return (m.group(1), m.group(2)) if m else (None, None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_delta(t0, t1):
|
|
50
|
+
dl = (t1.get("likes", 0) or 0) - (t0.get("likes", 0) or 0)
|
|
51
|
+
dr = (t1.get("retweets", 0) or 0) - (t0.get("retweets", 0) or 0)
|
|
52
|
+
dp = (t1.get("replies", 0) or 0) - (t0.get("replies", 0) or 0)
|
|
53
|
+
dv = (t1.get("views", 0) or 0) - (t0.get("views", 0) or 0)
|
|
54
|
+
db = (t1.get("bookmarks", 0) or 0) - (t0.get("bookmarks", 0) or 0)
|
|
55
|
+
return dl + 3 * dr + 2 * dp + dv / 1000.0 + db
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main():
|
|
59
|
+
p = argparse.ArgumentParser()
|
|
60
|
+
p.add_argument("--batch-id", required=True)
|
|
61
|
+
args = p.parse_args()
|
|
62
|
+
|
|
63
|
+
resp = api_get(
|
|
64
|
+
"/api/v1/twitter-candidates/pending-batch",
|
|
65
|
+
query={"batch_id": args.batch_id},
|
|
66
|
+
)
|
|
67
|
+
rows = (resp.get("data") or {}).get("candidates") or []
|
|
68
|
+
|
|
69
|
+
if not rows:
|
|
70
|
+
print(f"No pending rows for batch {args.batch_id}", file=sys.stderr)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
print(f"Re-polling {len(rows)} candidates for batch {args.batch_id}", file=sys.stderr)
|
|
74
|
+
|
|
75
|
+
def fetch_row(row):
|
|
76
|
+
cid = row["id"]
|
|
77
|
+
url = row["tweet_url"]
|
|
78
|
+
l0 = row.get("likes_t0")
|
|
79
|
+
r0 = row.get("retweets_t0")
|
|
80
|
+
p0 = row.get("replies_t0")
|
|
81
|
+
v0 = row.get("views_t0")
|
|
82
|
+
b0 = row.get("bookmarks_t0")
|
|
83
|
+
handle, tweet_id = parse(url)
|
|
84
|
+
if not handle:
|
|
85
|
+
return None
|
|
86
|
+
data = fetch_fxtwitter(handle, tweet_id)
|
|
87
|
+
if not data or not data.get("tweet"):
|
|
88
|
+
return None
|
|
89
|
+
t = data["tweet"]
|
|
90
|
+
t1 = {
|
|
91
|
+
"likes": t.get("likes", 0),
|
|
92
|
+
"retweets": t.get("retweets", 0),
|
|
93
|
+
"replies": t.get("replies", 0),
|
|
94
|
+
"views": t.get("views", 0),
|
|
95
|
+
"bookmarks": t.get("bookmarks", 0),
|
|
96
|
+
}
|
|
97
|
+
t0 = {"likes": l0 or 0, "retweets": r0 or 0, "replies": p0 or 0, "views": v0 or 0, "bookmarks": b0 or 0}
|
|
98
|
+
return (cid, url, t1, compute_delta(t0, t1))
|
|
99
|
+
|
|
100
|
+
with ThreadPoolExecutor(max_workers=8) as ex:
|
|
101
|
+
results = list(ex.map(fetch_row, rows))
|
|
102
|
+
|
|
103
|
+
for result in results:
|
|
104
|
+
if result is None:
|
|
105
|
+
continue
|
|
106
|
+
cid, url, t1, delta = result
|
|
107
|
+
try:
|
|
108
|
+
api_patch(
|
|
109
|
+
"/api/v1/twitter-candidates/by-id",
|
|
110
|
+
{
|
|
111
|
+
"id": cid,
|
|
112
|
+
"action": "set_t1",
|
|
113
|
+
"likes_t1": t1["likes"],
|
|
114
|
+
"retweets_t1": t1["retweets"],
|
|
115
|
+
"replies_t1": t1["replies"],
|
|
116
|
+
"views_t1": t1["views"],
|
|
117
|
+
"bookmarks_t1": t1["bookmarks"],
|
|
118
|
+
"delta_score": delta,
|
|
119
|
+
"likes": t1["likes"],
|
|
120
|
+
"retweets": t1["retweets"],
|
|
121
|
+
"replies": t1["replies"],
|
|
122
|
+
"views": t1["views"],
|
|
123
|
+
"bookmarks": t1["bookmarks"],
|
|
124
|
+
},
|
|
125
|
+
ok_on_404=True,
|
|
126
|
+
)
|
|
127
|
+
print(f" #{cid} {url} Δ={delta:.1f}", file=sys.stderr)
|
|
128
|
+
except SystemExit as e:
|
|
129
|
+
print(f" #{cid} {url} set_t1 failed: {e}", file=sys.stderr)
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
main()
|