@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,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Persist a Phase 2b draft on a twitter_candidates row.
|
|
3
|
+
|
|
4
|
+
Called by Claude inside Phase 2b BEFORE the twitter_browser.py post attempt,
|
|
5
|
+
so a CDP / browser / monthly-cap failure doesn't waste the LLM redraft on the
|
|
6
|
+
next cycle. The next cycle's Phase 2b sees draft_reply_text on the salvaged
|
|
7
|
+
row and posts it as-is when fresh (DRAFT_TTL).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/log_draft.py \\
|
|
11
|
+
--candidate-id 12345 \\
|
|
12
|
+
--text "your reply text here" \\
|
|
13
|
+
--style curious_probe \\
|
|
14
|
+
[--assigned-style curious_probe --assigned-mode use] \\
|
|
15
|
+
[--new-style '{"description":"...","example":"...","why_existing_didnt_fit":"..."}'] \\
|
|
16
|
+
[--platform twitter]
|
|
17
|
+
|
|
18
|
+
Engagement-style fields (2026-05-22 cutover, closes the Twitter enforcement gap):
|
|
19
|
+
--assigned-style / --assigned-mode
|
|
20
|
+
Picker output from saps_pick_style. Persisted to
|
|
21
|
+
twitter_candidates.assigned_style + .assigned_mode so
|
|
22
|
+
twitter_post_plan.py can call validate_or_register with the
|
|
23
|
+
original assignment and coerce USE-mode drift back, or accept the
|
|
24
|
+
INVENT-mode invention and POST it to /api/v1/engagement-styles/registry.
|
|
25
|
+
Both flags are optional for backward compatibility; legacy callers
|
|
26
|
+
that don't pass them get NULL columns and the post path falls back
|
|
27
|
+
to legacy (uncoerced) behaviour.
|
|
28
|
+
--new-style
|
|
29
|
+
JSON object literal with at minimum {description, example,
|
|
30
|
+
why_existing_didnt_fit}, optionally {note}. Persisted to
|
|
31
|
+
twitter_candidates.draft_new_style JSONB. Only meaningful in
|
|
32
|
+
INVENT mode; when present, twitter_post_plan.py bundles it into
|
|
33
|
+
the validate_or_register decision so the registry endpoint upserts
|
|
34
|
+
the new style row exactly like Reddit/GitHub/Moltbook do.
|
|
35
|
+
|
|
36
|
+
Output (JSON):
|
|
37
|
+
{"logged": true, "candidate_id": 12345, "drafted_at": "..."}
|
|
38
|
+
{"error": "CANDIDATE_NOT_FOUND", ...}
|
|
39
|
+
{"error": "ALREADY_POSTED", ...} # candidate has status != 'pending'
|
|
40
|
+
{"error": "BAD_NEW_STYLE_JSON", ...} # --new-style was not parseable
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
import sys
|
|
47
|
+
|
|
48
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
49
|
+
from http_api import api_patch
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
p = argparse.ArgumentParser()
|
|
54
|
+
p.add_argument("--candidate-id", type=int, required=True)
|
|
55
|
+
p.add_argument("--text", required=True)
|
|
56
|
+
p.add_argument("--style", default=None)
|
|
57
|
+
p.add_argument(
|
|
58
|
+
"--assigned-style", default=None,
|
|
59
|
+
help="Picker's USE-mode pinned style name (NULL in INVENT mode).",
|
|
60
|
+
)
|
|
61
|
+
p.add_argument(
|
|
62
|
+
"--assigned-mode", default=None, choices=[None, "use", "invent"],
|
|
63
|
+
help="Picker mode for this batch: 'use' or 'invent'.",
|
|
64
|
+
)
|
|
65
|
+
p.add_argument(
|
|
66
|
+
"--new-style", default=None,
|
|
67
|
+
help="JSON object literal {description, example, why_existing_didnt_fit, note?} "
|
|
68
|
+
"when model invents a new style.",
|
|
69
|
+
)
|
|
70
|
+
p.add_argument(
|
|
71
|
+
"--platform",
|
|
72
|
+
default="twitter",
|
|
73
|
+
choices=["twitter"],
|
|
74
|
+
help="Reserved for future Reddit/LinkedIn drafts; only twitter for now.",
|
|
75
|
+
)
|
|
76
|
+
args = p.parse_args()
|
|
77
|
+
|
|
78
|
+
text = args.text.strip()
|
|
79
|
+
if not text:
|
|
80
|
+
print(json.dumps({"error": "EMPTY_TEXT"}))
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
# Parse --new-style early so a malformed JSON arg fails before we touch
|
|
84
|
+
# the DB. We do NOT validate required fields here (description, example,
|
|
85
|
+
# why_existing_didnt_fit); validate_or_register/register_style does that
|
|
86
|
+
# at post time so this stays a pure persistence layer.
|
|
87
|
+
new_style_json = None
|
|
88
|
+
if args.new_style:
|
|
89
|
+
try:
|
|
90
|
+
parsed = json.loads(args.new_style)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(json.dumps({"error": "BAD_NEW_STYLE_JSON", "detail": str(e)}))
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
if not isinstance(parsed, dict):
|
|
95
|
+
print(json.dumps({"error": "BAD_NEW_STYLE_JSON",
|
|
96
|
+
"detail": "must be a JSON object"}))
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
new_style_json = json.dumps(parsed)
|
|
99
|
+
|
|
100
|
+
# platform is twitter-only today; the by-id endpoint targets twitter_candidates.
|
|
101
|
+
payload = {
|
|
102
|
+
"id": args.candidate_id,
|
|
103
|
+
"action": "set_draft",
|
|
104
|
+
"text": text,
|
|
105
|
+
"style": args.style,
|
|
106
|
+
"assigned_style": args.assigned_style,
|
|
107
|
+
"assigned_mode": args.assigned_mode,
|
|
108
|
+
}
|
|
109
|
+
if new_style_json is not None:
|
|
110
|
+
# Pass the already-validated JSON string; the endpoint re-parses it.
|
|
111
|
+
payload["new_style"] = new_style_json
|
|
112
|
+
|
|
113
|
+
resp = api_patch(
|
|
114
|
+
"/api/v1/twitter-candidates/by-id", payload,
|
|
115
|
+
ok_on_conflict=True, ok_on_404=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if (resp or {}).get("_not_found"):
|
|
119
|
+
print(json.dumps({"error": "CANDIDATE_NOT_FOUND", "candidate_id": args.candidate_id}))
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
if not (resp or {}).get("ok"):
|
|
122
|
+
# 409 already_posted (status carried under error.details.status).
|
|
123
|
+
details = ((resp or {}).get("error") or {}).get("details") or {}
|
|
124
|
+
print(json.dumps({
|
|
125
|
+
"error": "ALREADY_POSTED",
|
|
126
|
+
"candidate_id": args.candidate_id,
|
|
127
|
+
"status": details.get("status"),
|
|
128
|
+
}))
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
candidate = ((resp or {}).get("data") or {}).get("candidate") or {}
|
|
132
|
+
print(json.dumps({
|
|
133
|
+
"logged": True,
|
|
134
|
+
"candidate_id": args.candidate_id,
|
|
135
|
+
"drafted_at": candidate.get("drafted_at"),
|
|
136
|
+
"assigned_style": args.assigned_style,
|
|
137
|
+
"assigned_mode": args.assigned_mode,
|
|
138
|
+
"new_style_persisted": bool(new_style_json),
|
|
139
|
+
}))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
main()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
log_linkedin_search_attempts.py
|
|
4
|
+
|
|
5
|
+
Insert one row per (query, project, search_topic, candidates_found,
|
|
6
|
+
serp_quality_score, dropped_below_floor) into linkedin_search_attempts. Reads a JSON array on
|
|
7
|
+
stdin shaped like:
|
|
8
|
+
|
|
9
|
+
[
|
|
10
|
+
{"query": "...", "project": "fazm", "search_topic": "AI agents", "candidates_found": 0, "serp_quality_score": 1.5, "dropped_below_floor": 0},
|
|
11
|
+
{"query": "...", "project": "mediar", "search_topic": "RPA replacement", "candidates_found": 7, "serp_quality_score": 8.0, "dropped_below_floor": 3},
|
|
12
|
+
...
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
candidates_found is the POST-floor count (cards that passed
|
|
16
|
+
discover_linkedin_candidates.py's velocity floor). dropped_below_floor is
|
|
17
|
+
the per-query count of cards that the SERP returned but the floor rejected;
|
|
18
|
+
absent or 0 for queries the floor didn't run on.
|
|
19
|
+
|
|
20
|
+
Used by run-linkedin.sh after Phase A scrape parses queries_used out of the
|
|
21
|
+
LLM envelope. Logging zero-result AND low-quality SERP queries here is the
|
|
22
|
+
whole point: linkedin_candidates only has rows for posts that were actually
|
|
23
|
+
extracted, so "query returned 30 influencer slop posts that we skipped" was
|
|
24
|
+
previously invisible.
|
|
25
|
+
|
|
26
|
+
Pair with top_dud_linkedin_queries.py.
|
|
27
|
+
|
|
28
|
+
python3 scripts/log_linkedin_search_attempts.py --batch-id <id> < queries.json
|
|
29
|
+
|
|
30
|
+
Migrated 2026-06-01 from direct psycopg2/db.py INSERTs to the s4l.ai HTTP API
|
|
31
|
+
(POST /api/v1/linkedin-search-attempts, batch form). No DATABASE_URL needed.
|
|
32
|
+
"""
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
39
|
+
from http_api import api_post
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
p = argparse.ArgumentParser()
|
|
44
|
+
p.add_argument("--batch-id", default=None)
|
|
45
|
+
args = p.parse_args()
|
|
46
|
+
|
|
47
|
+
raw = sys.stdin.read().strip()
|
|
48
|
+
if not raw:
|
|
49
|
+
print("log_linkedin_search_attempts: empty stdin, nothing to log",
|
|
50
|
+
file=sys.stderr)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
rows = json.loads(raw)
|
|
55
|
+
except json.JSONDecodeError as e:
|
|
56
|
+
print(f"log_linkedin_search_attempts: bad JSON on stdin: {e}",
|
|
57
|
+
file=sys.stderr)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
if not isinstance(rows, list) or not rows:
|
|
61
|
+
print("log_linkedin_search_attempts: not a list or empty list, nothing to log",
|
|
62
|
+
file=sys.stderr)
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
payload = []
|
|
66
|
+
for r in rows:
|
|
67
|
+
if not isinstance(r, dict):
|
|
68
|
+
continue
|
|
69
|
+
query = (r.get("query") or "").strip()
|
|
70
|
+
if not query:
|
|
71
|
+
continue
|
|
72
|
+
project = (r.get("project") or "").strip() or None
|
|
73
|
+
search_topic = (r.get("search_topic") or "").strip() or None
|
|
74
|
+
candidates_found = r.get("candidates_found")
|
|
75
|
+
try:
|
|
76
|
+
candidates_found = int(candidates_found if candidates_found is not None else 0)
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
candidates_found = 0
|
|
79
|
+
dropped = r.get("dropped_below_floor")
|
|
80
|
+
try:
|
|
81
|
+
dropped = int(dropped if dropped is not None else 0)
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
dropped = 0
|
|
84
|
+
serp = r.get("serp_quality_score")
|
|
85
|
+
try:
|
|
86
|
+
serp = float(serp) if serp is not None else None
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
serp = None
|
|
89
|
+
payload.append({
|
|
90
|
+
"query": query,
|
|
91
|
+
"project_name": project,
|
|
92
|
+
"search_topic": search_topic,
|
|
93
|
+
"candidates_found": candidates_found,
|
|
94
|
+
"serp_quality_score": serp,
|
|
95
|
+
"candidates_dropped_below_floor": dropped,
|
|
96
|
+
"batch_id": args.batch_id,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if not payload:
|
|
100
|
+
print("log_linkedin_search_attempts: no valid rows after parse, nothing to log",
|
|
101
|
+
file=sys.stderr)
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
resp = api_post("/api/v1/linkedin-search-attempts", payload)
|
|
105
|
+
inserted = (resp.get("data") or {}).get("inserted", len(payload))
|
|
106
|
+
|
|
107
|
+
duds = sum(
|
|
108
|
+
1 for r in rows
|
|
109
|
+
if isinstance(r, dict) and not int(r.get("candidates_found") or 0)
|
|
110
|
+
)
|
|
111
|
+
low_quality = sum(
|
|
112
|
+
1 for r in rows
|
|
113
|
+
if isinstance(r, dict)
|
|
114
|
+
and r.get("serp_quality_score") is not None
|
|
115
|
+
and float(r["serp_quality_score"]) < 4.0
|
|
116
|
+
)
|
|
117
|
+
print(
|
|
118
|
+
f"log_linkedin_search_attempts: inserted {inserted} rows "
|
|
119
|
+
f"({duds} zero-result, {low_quality} low-SERP) for batch={args.batch_id}",
|
|
120
|
+
file=sys.stderr,
|
|
121
|
+
)
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
sys.exit(main())
|