@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,282 @@
1
+ #!/bin/bash
2
+ # linkedin-recovery.sh — hourly auto-recovery for the LinkedIn killswitch.
3
+ #
4
+ # Problem this solves: when LinkedIn logs us out / returns an authwall, the
5
+ # killswitch (scripts/linkedin_killswitch.py) engages and every LinkedIn
6
+ # pipeline self-aborts at startup until the session is restored and the flag is
7
+ # cleared. We want that restore to happen on its own when it safely can.
8
+ #
9
+ # This job, fired hourly by launchd (com.m13v.social-linkedin-recovery), runs the
10
+ # state machine in scripts/linkedin_killswitch.py. `recover-check` decides what
11
+ # (if anything) to do this hour and prints the MODE on stdout:
12
+ #
13
+ # (nothing) inactive / terminal / too young / mid-hold -> exit, no Chrome.
14
+ # "login" active >= LINKEDIN_RECOVERY_MIN_AGE_HOURS (default 24h): spin up a
15
+ # Claude session that drives the REAL harness Chrome to actually log
16
+ # back in (the allowed pattern; scripted Python login is the banned
17
+ # one), then record the verdict:
18
+ # held -> login worked; enter a pending-hold window and
19
+ # re-verify later that it STUCK before resuming.
20
+ # hard_block -> checkpoint / captcha / restriction / wrong creds /
21
+ # 2FA: STOP completely, email, never auto-retry.
22
+ # transient -> ambiguous; re-anchor the 24h clock and try again
23
+ # later, up to LINKEDIN_RECOVERY_TRANSIENT_MAX_ATTEMPTS.
24
+ # "hold" a prior login succeeded and the hold window elapsed: run the
25
+ # read-only `recover-hold` re-verify (no Claude, no login).
26
+ # healthy -> clear the flag, the fleet resumes.
27
+ # dropped/logged-out -> "it didn't hold" -> STOP completely, email.
28
+ #
29
+ # The 24h wait + single-attempt-then-stop is the anti-bot rule: we never hammer
30
+ # the login wall, and we never keep re-poking a session that won't hold.
31
+ #
32
+ # When the flag clears, the six LinkedIn launchd jobs resume on their next fire
33
+ # (they all gate on the killswitch file). There is NO launchctl load/unload.
34
+ #
35
+ # This script is a no-op (instant exit, no Chrome) on every hour there is nothing
36
+ # eligible to do, so it is safe to leave loaded.
37
+
38
+ set -uo pipefail
39
+ export PATH="/opt/homebrew/bin:$PATH"
40
+
41
+ REPO_DIR="$HOME/social-autoposter"
42
+ LOG_DIR="$REPO_DIR/skill/logs"
43
+ mkdir -p "$LOG_DIR"
44
+ LOG="$LOG_DIR/linkedin-recovery.log"
45
+
46
+ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG" >&2; }
47
+
48
+ PY="/opt/homebrew/bin/python3"
49
+ [ -x "$PY" ] || PY="/usr/bin/python3"
50
+
51
+ KS="$REPO_DIR/scripts/linkedin_killswitch.py"
52
+
53
+ # Gate + mode. recover-check prints "login" or "hold" on stdout when there is
54
+ # work to do (exit 0); exit !=0 means nothing eligible this hour.
55
+ MODE="$("$PY" "$KS" recover-check 2>>"$LOG")" || exit 0
56
+ MODE="$(printf '%s' "$MODE" | tr -d '[:space:]')"
57
+ log "recover-check eligible; mode=${MODE:-?}"
58
+
59
+ # linkedin-backend.sh exports LINKEDIN_CDP_URL + LINKEDIN_DISCOVER_PYTHON +
60
+ # MCP_CONFIG_FILE and provides ensure_linkedin_browser_for_backend (launches the
61
+ # port-9556 harness Chrome and acquires the cross-pipeline lock).
62
+ export S4L_PIPELINE_NAME="linkedin-recovery"
63
+ # shellcheck disable=SC1091
64
+ source "$REPO_DIR/skill/lib/linkedin-backend.sh"
65
+
66
+ if ! ensure_linkedin_browser_for_backend; then
67
+ log "ERROR: could not bring up linkedin-harness Chrome; will retry next hour"
68
+ exit 0
69
+ fi
70
+
71
+ # The read-only probe needs a Playwright-capable interpreter (3.14 lacks it; the
72
+ # backend resolves a working one into LINKEDIN_DISCOVER_PYTHON).
73
+ PROBE_PY="${LINKEDIN_DISCOVER_PYTHON:-$PY}"
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # MODE: hold — read-only re-verify that a prior successful login actually stuck.
77
+ # ---------------------------------------------------------------------------
78
+ if [ "$MODE" = "hold" ]; then
79
+ RESULT="$("$PROBE_PY" "$KS" recover-hold --cdp-url "$LINKEDIN_CDP_URL" 2>>"$LOG")"
80
+ log "recover-hold result: $RESULT"
81
+ exit 0
82
+ fi
83
+
84
+ if [ "$MODE" != "login" ]; then
85
+ log "unrecognized recover-check mode '${MODE:-}'; nothing to do"
86
+ exit 0
87
+ fi
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # MODE: login — Claude-driven re-login attempt against the real harness Chrome.
91
+ # ---------------------------------------------------------------------------
92
+ # Credentials live in the login keychain (service "LinkedIn m13v"). Exact service
93
+ # name, so a direct lookup is correct here (the auth skill is for interactive,
94
+ # fuzzy lookups, not unattended scripts).
95
+ LI_EMAIL="$(security find-generic-password -s 'LinkedIn m13v' -g 2>&1 \
96
+ | sed -n 's/^[[:space:]]*"acct"<blob>="\(.*\)"$/\1/p')"
97
+ LI_PASSWORD="$(security find-generic-password -s 'LinkedIn m13v' -w 2>/dev/null)"
98
+ [ -n "$LI_EMAIL" ] || LI_EMAIL="i@m13v.com"
99
+
100
+ if [ -z "$LI_PASSWORD" ]; then
101
+ log "ERROR: LinkedIn password unreadable from keychain (locked under launchd?); recording transient"
102
+ "$PY" "$KS" recover-record --verdict transient \
103
+ --detail "keychain password unavailable under launchd" --no-email >>"$LOG" 2>&1
104
+ exit 0
105
+ fi
106
+
107
+ PROMPT_FILE="$(mktemp -t li-relogin.XXXXXX)"
108
+ chmod 600 "$PROMPT_FILE"
109
+ CLAUDE_OUT="$LOG_DIR/linkedin-recovery-login-$(date +%Y-%m-%d_%H%M%S).out"
110
+
111
+ # PASSWORD LEAK PREVENTION: the cleartext password must never land in the prompt,
112
+ # the Claude tool-call args (stream-json), the session transcript under
113
+ # ~/.claude/projects, the session-log copy under skill/logs, or $CLAUDE_OUT.
114
+ # So we write it to its own 0600 temp file and have the harness script READ it at
115
+ # RUNTIME (open(PW_FILE).read()) and feed it to type_text(pw). The bh_run script
116
+ # text that gets recorded contains only the file PATH and `type_text(pw)`, never
117
+ # the secret. The harness CLI runs as this same user (server.py does
118
+ # env=os.environ.copy()), so the read succeeds. Both temps are shredded below.
119
+ PW_FILE="$(mktemp -t li-pw.XXXXXX)"
120
+ chmod 600 "$PW_FILE"
121
+ printf '%s' "$LI_PASSWORD" > "$PW_FILE"
122
+ unset LI_PASSWORD
123
+
124
+ cat > "$PROMPT_FILE" <<PROMPT_EOF
125
+ You are recovering a LOGGED-OUT LinkedIn session inside a real Google Chrome
126
+ (the linkedin-harness, CDP-driven, port 9556). Your job: attempt ONE login and
127
+ report exactly what happened. This is an authorized account-owner recovery.
128
+
129
+ You have ONE tool: mcp__linkedin-harness__bh_run(script) — it runs Python with
130
+ these helpers pre-imported:
131
+ goto_url(url), wait_for_load(), page_info(), capture_screenshot(),
132
+ js(expression), type_text(text), click_at_xy(x, y), press_key(key)
133
+ Reuse the existing tab: use goto_url() for your FIRST navigation as well.
134
+
135
+ HARD ANTI-BOT RULES (never break these):
136
+ - NEVER call /voyager/api/* (Python, fetch(), js()). Internal backend = restriction.
137
+ - No scroll-and-expand loops, no opening post permalinks. This is login ONLY.
138
+
139
+ CREDENTIALS (the account owner authorized this login):
140
+ email: $LI_EMAIL
141
+ password: stored in the file at this path, read it at runtime:
142
+ $PW_FILE
143
+
144
+ SECRET-HANDLING RULES (never break these):
145
+ - NEVER write the literal password anywhere: not in a bh_run script, not in a
146
+ js() expression, not in your text output. The password is a SECRET.
147
+ - The ONLY way to use it: inside a single bh_run script, read it with
148
+ pw = open("$PW_FILE").read().strip() and pass that VARIABLE to type_text(pw).
149
+ - Do NOT build a js() string that contains the password value (e.g. do NOT do
150
+ js('...value="'+pw+'"...')). That would leak it. Use type_text(pw) only.
151
+
152
+ STEPS (make ONE login attempt only; do not retry, do not click around beyond the
153
+ login form):
154
+ 1. bh_run('goto_url("https://www.linkedin.com/feed/"); wait_for_load()').
155
+ Read bh_run('print(js("""return location.href"""))'). If it is a logged-in
156
+ feed (URL contains /feed/ and NOT login / checkpoint / authwall), we are
157
+ already logged in -> verdict "held".
158
+ 2. Otherwise bh_run('goto_url("https://www.linkedin.com/login"); wait_for_load()').
159
+ Type the email into the #username field (type_text), then focus the #password
160
+ field and type the password, then click the "Sign in" submit button. Do it in
161
+ ONE bh_run script so the password stays in a local variable, e.g.:
162
+ pw = open("$PW_FILE").read().strip()
163
+ # click the #username field via click_at_xy at the center of its
164
+ # getBoundingClientRect, then type_text("$LI_EMAIL")
165
+ # click the #password field the same way, then type_text(pw)
166
+ # click the Sign in submit button
167
+ Then bh_run wait_for_load().
168
+ 3. Read bh_run('print(js("""return location.href"""))') AND
169
+ bh_run('print(capture_screenshot())') (Read the PNG) and judge:
170
+ - Landed on /feed/ or any logged-in linkedin page (not login/checkpoint/authwall)
171
+ -> verdict "held".
172
+ - A TEMPORARY restriction that states an explicit lift date/time, e.g.
173
+ "your account is temporarily restricted until June 03 2026 4:05 PM PDT" or
174
+ "you can try again on <date/time>" -> verdict "restricted_temp". Do NOT solve
175
+ anything. In the detail you MUST include the lift time normalized to ISO 8601
176
+ WITH the timezone offset, as a token "lift=<ISO8601>", e.g.:
177
+ lift=2026-06-03T16:05:00-07:00
178
+ (convert the displayed local time to ISO; PDT = -07:00, PST = -08:00, ET in
179
+ summer = -04:00). Keep the rest of the detail short, e.g.
180
+ temporary restriction for automated activity lift=2026-06-03T16:05:00-07:00
181
+ - A checkpoint, captcha, "quick security check", "verify it's you", a phone or
182
+ email verification code, a 2FA prompt, a PERMANENT/no-stated-time restriction,
183
+ or a wrong-password error -> verdict "hard_block". Do NOT solve it, do NOT
184
+ enter any codes. Put the specific reason in the detail.
185
+ - The page failed to load, timed out, or you genuinely cannot tell -> verdict
186
+ "transient".
187
+
188
+ NEVER print the password anywhere in your output.
189
+
190
+ FINAL OUTPUT: print EXACTLY one line and nothing after it, in this exact form
191
+ (verdict is one of held / hard_block / restricted_temp / transient; detail is a
192
+ short plain-ascii phrase with NO quotes and NO pipe characters; for
193
+ restricted_temp the detail MUST contain a lift=<ISO8601> token):
194
+ ===LIVERDICT===<verdict>|<short detail>===END===
195
+ PROMPT_EOF
196
+
197
+ # Pre-assign the session UUID so we know exactly which transcript to scrub
198
+ # afterward (run_claude.sh honors a pre-set CLAUDE_SESSION_ID). AUP retries can
199
+ # still rotate it, so the scrub below is also content-based, not id-only.
200
+ export CLAUDE_SESSION_ID="$(uuidgen | tr 'A-Z' 'a-z')"
201
+
202
+ log "launching Claude re-login session (account=$LI_EMAIL); output -> $CLAUDE_OUT"
203
+ "$REPO_DIR/scripts/run_claude.sh" "linkedin-recovery-login" \
204
+ --strict-mcp-config --mcp-config "$MCP_CONFIG_FILE" \
205
+ --output-format stream-json --verbose \
206
+ -p "$(cat "$PROMPT_FILE")" >"$CLAUDE_OUT" 2>>"$LOG"
207
+ CLAUDE_RC=$?
208
+ log "Claude re-login session exited rc=$CLAUDE_RC"
209
+
210
+ # DEFENSE-IN-DEPTH SCRUB. The prompt tells Claude to read the password from a
211
+ # file and feed it to type_text(pw), so the secret should never reach any
212
+ # transcript. But a disobedient model could still type/echo the literal, so we
213
+ # redact any occurrence of it from every on-disk surface before deleting the
214
+ # password file: $CLAUDE_OUT, the session transcript under ~/.claude/projects,
215
+ # and the archived session-log copy under skill/logs/claude-sessions. The
216
+ # password is passed to the scrubber via env (SCRUB_PW), never argv (ps leak).
217
+ SCRUB_PW="$(cat "$PW_FILE")" SCRUB_OUT="$CLAUDE_OUT" SCRUB_LOGDIR="$LOG_DIR" \
218
+ "$PY" - <<'PYEOF' >>"$LOG" 2>&1
219
+ import os, glob, time
220
+ pw = os.environ.get("SCRUB_PW", "")
221
+ if pw and len(pw) >= 4:
222
+ home = os.path.expanduser("~")
223
+ targets = []
224
+ if os.environ.get("SCRUB_OUT"):
225
+ targets.append(os.environ["SCRUB_OUT"])
226
+ proj = os.path.join(home, ".claude", "projects",
227
+ "-Users-matthewdi-social-autoposter")
228
+ targets += glob.glob(os.path.join(proj, "*.jsonl"))
229
+ logdir = os.environ.get("SCRUB_LOGDIR", "")
230
+ if logdir:
231
+ targets += glob.glob(os.path.join(logdir, "claude-sessions", "*", "*.jsonl"))
232
+ cutoff = time.time() - 3600 # only touch files modified in the last hour
233
+ redacted = 0
234
+ for f in targets:
235
+ try:
236
+ if not os.path.isfile(f) or os.path.getmtime(f) < cutoff:
237
+ continue
238
+ data = open(f, encoding="utf-8", errors="replace").read()
239
+ if pw in data:
240
+ open(f, "w", encoding="utf-8").write(data.replace(pw, "[REDACTED_PW]"))
241
+ redacted += 1
242
+ except Exception:
243
+ pass
244
+ print(f"[scrub] checked {len(targets)} file(s); redacted password in {redacted}")
245
+ PYEOF
246
+
247
+ # Shred the password file (best-effort overwrite, then remove) and drop the prompt.
248
+ command -v gshred >/dev/null 2>&1 && gshred -u "$PW_FILE" 2>/dev/null
249
+ rm -f "$PW_FILE" "$PROMPT_FILE"
250
+
251
+ # Extract the sentinel verdict line (survives stream-json escaping: it carries no
252
+ # quotes/backslashes). Take the last match if the model printed more than one.
253
+ RAW="$("$PY" - "$CLAUDE_OUT" <<'PYEOF'
254
+ import re, sys
255
+ try:
256
+ t = open(sys.argv[1], encoding="utf-8", errors="replace").read()
257
+ except Exception:
258
+ t = ""
259
+ m = re.findall(r"===LIVERDICT===(.*?)===END===", t, re.S)
260
+ print(m[-1].strip() if m else "")
261
+ PYEOF
262
+ )"
263
+
264
+ VERDICT="${RAW%%|*}"
265
+ DETAIL="${RAW#*|}"
266
+ VERDICT="$(printf '%s' "$VERDICT" | tr -d '[:space:]')"
267
+ [ "$DETAIL" = "$RAW" ] && DETAIL="" # no pipe present
268
+ DETAIL="$(printf '%s' "$DETAIL" | tr -d '\n\r' | cut -c1-300)"
269
+
270
+ case "$VERDICT" in
271
+ held|hard_block|restricted_temp|transient) ;;
272
+ *)
273
+ log "no usable verdict parsed (raw='$RAW', rc=$CLAUDE_RC); treating as transient"
274
+ VERDICT="transient"
275
+ DETAIL="no verdict parsed from re-login session (rc=$CLAUDE_RC)"
276
+ ;;
277
+ esac
278
+
279
+ log "re-login verdict=$VERDICT detail=$DETAIL"
280
+ RESULT="$("$PY" "$KS" recover-record --verdict "$VERDICT" --detail "$DETAIL" 2>>"$LOG")"
281
+ log "recover-record result: $RESULT"
282
+ exit 0