@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.
Files changed (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1314 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +497 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. 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())