@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,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
|