@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,314 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# twitter-backend.sh - Twitter pipeline browser bootstrap (harness-only since
|
|
3
|
+
# 2026-05-19; the legacy twitter-agent Playwright MCP path was fully ripped out).
|
|
4
|
+
#
|
|
5
|
+
# Source this AFTER lock.sh, BEFORE any acquire_lock / browser pre-flight /
|
|
6
|
+
# claude -p subprocess calls. Sets these for the caller:
|
|
7
|
+
#
|
|
8
|
+
# MCP_CONFIG_FILE - claude -p --mcp-config path (twitter-harness MCP)
|
|
9
|
+
# BROWSER_INSTRUCTIONS - prompt block describing the harness backend +
|
|
10
|
+
# its bh_run tool surface (inject at the TOP of any
|
|
11
|
+
# prompt that mentions browser_* tools)
|
|
12
|
+
#
|
|
13
|
+
# And exports (so Python subprocesses like twitter_browser.py inherit them):
|
|
14
|
+
#
|
|
15
|
+
# TWITTER_CDP_URL - http://127.0.0.1:9555 (forces direct CDP attach,
|
|
16
|
+
# skipping ps-based agent-profile discovery)
|
|
17
|
+
#
|
|
18
|
+
# Provides these functions (names preserved for back-compat with existing
|
|
19
|
+
# callers in engage-twitter.sh, run-twitter-cycle.sh, run-twitter-threads.sh,
|
|
20
|
+
# dm-outreach-twitter.sh, scan-twitter-followups.sh):
|
|
21
|
+
#
|
|
22
|
+
# ensure_twitter_browser_for_backend
|
|
23
|
+
# Call AFTER acquire_lock "twitter-browser". Probes harness Chrome on
|
|
24
|
+
# port 9555 and launches it idempotently if down, then cleans leftover
|
|
25
|
+
# tabs from prior runs.
|
|
26
|
+
#
|
|
27
|
+
# defer_if_foreign_for_backend [log_file]
|
|
28
|
+
# No-op. Harness CDP supports multiple concurrent clients on the same
|
|
29
|
+
# Chrome (no SingletonLock fight), so foreign MCP wrappers never block
|
|
30
|
+
# us. Kept as a function only so callers don't have to change.
|
|
31
|
+
|
|
32
|
+
MCP_CONFIG_FILE="$HOME/.claude/browser-agent-configs/twitter-harness-mcp.json"
|
|
33
|
+
|
|
34
|
+
# Per-host env override (written by bin/cli.js when installing on an AppMaker
|
|
35
|
+
# VM, where the canonical browser is Chromium on port 9222 behind the SOAX
|
|
36
|
+
# residential proxy at 127.0.0.1:3003, NOT the harness Chrome on 9555). On a
|
|
37
|
+
# Mac dev box this file does not exist, so the default below kicks in.
|
|
38
|
+
if [ -f "$HOME/.social-autoposter-env" ]; then
|
|
39
|
+
# shellcheck disable=SC1091
|
|
40
|
+
. "$HOME/.social-autoposter-env"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Tell twitter_browser.py (and any other Python helper that honors this env
|
|
44
|
+
# var) to skip ps-based discovery and connect directly to the configured CDP
|
|
45
|
+
# endpoint. Default 9555 (Mac harness Chrome). AppMaker VMs pre-set this to
|
|
46
|
+
# http://127.0.0.1:9222 via ~/.social-autoposter-env above.
|
|
47
|
+
export TWITTER_CDP_URL="${TWITTER_CDP_URL:-http://127.0.0.1:9555}"
|
|
48
|
+
|
|
49
|
+
# Default harness URL — used by ensure_twitter_browser_for_backend +
|
|
50
|
+
# cleanup_harness_tabs to decide whether we own this Chrome (and should
|
|
51
|
+
# launch/clean it) or whether it is externally managed (AppMaker, BYO).
|
|
52
|
+
_BH_DEFAULT_URL="http://127.0.0.1:9555"
|
|
53
|
+
# DEPRECATED 2026-06-26: this block is NO LONGER injected into any model prompt.
|
|
54
|
+
# run-twitter-cycle.sh now sets TW_ENGINE_PREFIX="" — Phase 1 (query) and Phase 2b
|
|
55
|
+
# (prep) are tool-free; the model drafts from inlined candidate context only, and all
|
|
56
|
+
# browser work is the shell's deterministic CDP scan + Phase 2b-post's
|
|
57
|
+
# twitter_browser.py. Kept only so ensure_twitter_browser_for_backend's existing
|
|
58
|
+
# assignment doesn't break; safe to delete once confirmed unreferenced. Do NOT
|
|
59
|
+
# reintroduce the "logged in as m13v_" hardcode or a model-facing bh_run contract.
|
|
60
|
+
BROWSER_INSTRUCTIONS=$(cat <<'BROWSER_HARNESS_EOF'
|
|
61
|
+
BROWSER BACKEND: twitter-harness (browser-harness MCP, CDP-driven REAL Google Chrome on
|
|
62
|
+
port 9555, profile ~/.claude/browser-profiles/browser-harness). The Chrome is already
|
|
63
|
+
logged in as m13v_; cookies persist on disk.
|
|
64
|
+
|
|
65
|
+
You have ONE tool: mcp__twitter-harness__bh_run(script). It runs arbitrary Python with
|
|
66
|
+
these helpers pre-imported:
|
|
67
|
+
new_tab(url), goto_url(url), wait_for_load(), page_info(),
|
|
68
|
+
capture_screenshot(), # returns path to PNG; Read it to see the page
|
|
69
|
+
click_at_xy(x, y), # coordinate click (viewport pixels)
|
|
70
|
+
js(expression), # page.evaluate-style; returns the result
|
|
71
|
+
type_text(text), # types into currently-focused element
|
|
72
|
+
press_key(key), # e.g. "Enter", "Tab", "Escape"
|
|
73
|
+
scroll(direction, amount), cdp(method, **params)
|
|
74
|
+
|
|
75
|
+
TAB HYGIENE (IMPORTANT): A placeholder tab ALWAYS already exists when you start
|
|
76
|
+
(pre-flight leaves exactly one tab open). REUSE IT: use goto_url() for your VERY FIRST
|
|
77
|
+
navigation as well as every subsequent one, so the existing tab is navigated in place.
|
|
78
|
+
Call new_tab() ONLY as a fallback when no usable tab exists (goto_url errors because
|
|
79
|
+
there is no active page) OR when you genuinely need a second tab open in parallel.
|
|
80
|
+
Opening a fresh tab on first navigation orphans the placeholder and leaks a tab every
|
|
81
|
+
cycle, which exhausts per-process Chrome resources.
|
|
82
|
+
|
|
83
|
+
TRANSLATION TABLE - wherever this prompt mentions a Playwright-style tool, do the
|
|
84
|
+
following with bh_run instead:
|
|
85
|
+
|
|
86
|
+
browser_navigate(url) -> Reuse the existing tab (default, incl. first nav):
|
|
87
|
+
bh_run('goto_url("URL"); wait_for_load()')
|
|
88
|
+
Fallback only if no tab exists / parallel tab needed:
|
|
89
|
+
bh_run('new_tab("URL"); wait_for_load()')
|
|
90
|
+
browser_snapshot -> bh_run('print(js("""..."""))') to read DOM as structured data,
|
|
91
|
+
OR bh_run('print(capture_screenshot())') + Read the PNG
|
|
92
|
+
browser_run_code(js) -> bh_run('print(js("""<the JS expression>"""))')
|
|
93
|
+
browser_click(ref=...) -> Find the element via selector, compute center coords from
|
|
94
|
+
getBoundingClientRect, then bh_run('click_at_xy(X, Y)')
|
|
95
|
+
browser_type(ref=..., text=...) -> Click the textbox first (click_at_xy), then bh_run('type_text("TEXT")')
|
|
96
|
+
browser_take_screenshot -> bh_run('print(capture_screenshot())') then Read the path
|
|
97
|
+
browser_press_key("Enter") -> bh_run('press_key("Enter")')
|
|
98
|
+
|
|
99
|
+
EXAMPLE - click the reply submit button:
|
|
100
|
+
bh_run('''
|
|
101
|
+
pt = js("""
|
|
102
|
+
const el = document.querySelector('[data-testid="tweetButtonInline"]');
|
|
103
|
+
if (!el) return null;
|
|
104
|
+
const r = el.getBoundingClientRect();
|
|
105
|
+
return {x: r.x + r.width/2, y: r.y + r.height/2};
|
|
106
|
+
""")
|
|
107
|
+
print(pt)
|
|
108
|
+
''')
|
|
109
|
+
# Then in a follow-up call (substituting the x/y from above):
|
|
110
|
+
bh_run('click_at_xy(123, 456)')
|
|
111
|
+
|
|
112
|
+
VERIFY AFTER EVERY MUTATION by capturing a screenshot and reading the PNG, coordinate
|
|
113
|
+
clicks can miss; visual verification is the only reliable confirmation that the action took.
|
|
114
|
+
BROWSER_HARNESS_EOF
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
cleanup_harness_tabs() {
|
|
118
|
+
# Close every CDP "page" tab except one. Delegated to a standalone Python
|
|
119
|
+
# script because bash 3.2 (what launchd uses) cannot parse a nested heredoc
|
|
120
|
+
# inside a function body inside a sourced file. Inline form here broke every
|
|
121
|
+
# launchd-fired twitter script on 2026-05-14 until this refactor.
|
|
122
|
+
#
|
|
123
|
+
# Health-check gate: 2026-05-16 the original `--max-time 2` was too strict.
|
|
124
|
+
# When harness Chrome is busy (long scans, lock backups, CPU-pinned),
|
|
125
|
+
# the /json/version probe times out, cleanup is silently skipped, and the
|
|
126
|
+
# next scan's new_tab() leaks an orphan tab. Symptom: occasional
|
|
127
|
+
# "closed 14/14 extra page tabs" cycles after several skips piled up.
|
|
128
|
+
# Now: 10s timeout + ONE retry; log skips so they are not silent.
|
|
129
|
+
local _probe="curl -sf --max-time 10 -o /dev/null http://127.0.0.1:9555/json/version"
|
|
130
|
+
if ! $_probe 2>/dev/null; then
|
|
131
|
+
sleep 1
|
|
132
|
+
if ! $_probe 2>/dev/null; then
|
|
133
|
+
echo "[$(date +%H:%M:%S)] cleanup_harness_tabs: SKIPPED (harness CDP /json/version unreachable after 10s+retry)" >&2
|
|
134
|
+
return 0
|
|
135
|
+
fi
|
|
136
|
+
fi
|
|
137
|
+
python3 "$HOME/social-autoposter/scripts/cleanup_harness_tabs.py" 2>/dev/null || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_resolve_chrome_bin() {
|
|
141
|
+
# Auto-detect Chrome/Chromium so the same script launches the harness on
|
|
142
|
+
# macOS dev boxes AND Linux VMs. Override with BH_CHROME_BIN.
|
|
143
|
+
if [ -n "${BH_CHROME_BIN:-}" ] && [ -x "$BH_CHROME_BIN" ]; then
|
|
144
|
+
echo "$BH_CHROME_BIN"; return 0
|
|
145
|
+
fi
|
|
146
|
+
for _p in \
|
|
147
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
|
|
148
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium" \
|
|
149
|
+
"/usr/bin/google-chrome" "/usr/bin/google-chrome-stable" \
|
|
150
|
+
"/usr/bin/chromium" "/usr/bin/chromium-browser" "/snap/bin/chromium"
|
|
151
|
+
do
|
|
152
|
+
if [ -x "$_p" ]; then echo "$_p"; return 0; fi
|
|
153
|
+
done
|
|
154
|
+
for _n in google-chrome google-chrome-stable chromium chromium-browser; do
|
|
155
|
+
_which=$(command -v "$_n" 2>/dev/null) && [ -n "$_which" ] && { echo "$_which"; return 0; }
|
|
156
|
+
done
|
|
157
|
+
echo ""; return 1
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ensure_twitter_browser_for_backend() {
|
|
161
|
+
# AppMaker / BYO Chrome: TWITTER_CDP_URL points at something other than our
|
|
162
|
+
# default harness URL. Don't touch that browser; just probe it and bail.
|
|
163
|
+
# The AppMaker bootstrap (and any future BYO setup) is responsible for
|
|
164
|
+
# keeping the externally-managed Chrome alive.
|
|
165
|
+
if [ "${TWITTER_CDP_URL:-$_BH_DEFAULT_URL}" != "$_BH_DEFAULT_URL" ]; then
|
|
166
|
+
local _ext_url="${TWITTER_CDP_URL}"
|
|
167
|
+
if curl -sf --max-time 2 -o /dev/null "${_ext_url}/json/version" 2>/dev/null; then
|
|
168
|
+
echo "[$(date +%H:%M:%S)] Using externally-managed Chrome at ${_ext_url} (skipping harness launch + tab cleanup)" >&2
|
|
169
|
+
# Restore the Twitter login if the sandbox was substituted. AppMaker
|
|
170
|
+
# Hobby-tier sandboxes have a 1h TTL; on substitution /root is reseeded
|
|
171
|
+
# from /etc/skel-root and the harness profile (cookies) is wiped. This
|
|
172
|
+
# re-injects the stored session from social_accounts via the HTTP API.
|
|
173
|
+
# No-op when already logged in. Never blocks the cycle on failure.
|
|
174
|
+
python3 "$HOME/social-autoposter/scripts/restore_twitter_session.py" 2>&1 | sed 's/^/[restore] /' >&2 || true
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
echo "[$(date +%H:%M:%S)] ERROR: TWITTER_CDP_URL=${_ext_url} not reachable. External Chrome must be managed by host (AppMaker /opt/startup.sh, etc)." >&2
|
|
178
|
+
return 1
|
|
179
|
+
fi
|
|
180
|
+
# Probe + launch harness Chrome on port 9555 if needed.
|
|
181
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
182
|
+
echo "[$(date +%H:%M:%S)] Harness Chrome down on port 9555, launching..." >&2
|
|
183
|
+
local _chrome_bin
|
|
184
|
+
_chrome_bin=$(_resolve_chrome_bin)
|
|
185
|
+
if [ -z "$_chrome_bin" ]; then
|
|
186
|
+
echo "[$(date +%H:%M:%S)] ERROR: no Chrome/Chromium binary found. Set BH_CHROME_BIN." >&2
|
|
187
|
+
return 1
|
|
188
|
+
fi
|
|
189
|
+
# On Linux + no display, run headless. On root, add --no-sandbox.
|
|
190
|
+
# Window-position/size only meaningful on macOS multi-monitor; skip
|
|
191
|
+
# elsewhere so we don't hide the window off-screen on single-display
|
|
192
|
+
# Linux VMs.
|
|
193
|
+
local _extra=()
|
|
194
|
+
case "$(uname -s)" in
|
|
195
|
+
Linux)
|
|
196
|
+
_extra+=(--no-sandbox --disable-dev-shm-usage)
|
|
197
|
+
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
|
|
198
|
+
_extra+=(--headless=new --disable-gpu)
|
|
199
|
+
fi
|
|
200
|
+
;;
|
|
201
|
+
Darwin)
|
|
202
|
+
_extra+=(--window-position="${BH_WINDOW_POS:-3042,-1032}")
|
|
203
|
+
_extra+=(--window-size="${BH_WINDOW_SIZE:-1024,1013}")
|
|
204
|
+
;;
|
|
205
|
+
esac
|
|
206
|
+
# --password-store=basic + --use-mock-keychain: encrypt the cookie store
|
|
207
|
+
# with Chrome's fixed obfuscation key instead of the macOS Keychain
|
|
208
|
+
# ("Chrome Safe Storage"). Without this, a keychain lock/re-lock leaves
|
|
209
|
+
# Chrome unable to decrypt its Cookies SQLite on the next launch, so it
|
|
210
|
+
# discards the session and the harness comes up logged out. With it, the
|
|
211
|
+
# x.com cookies persist + decrypt across restarts natively, no
|
|
212
|
+
# re-injection needed. Matches the flags the Playwright browser agents
|
|
213
|
+
# already use. (Root-cause persistence fix, 2026-06-02; the cookie
|
|
214
|
+
# mirror + restore_twitter_session.py remain as the safety net.)
|
|
215
|
+
# Self-heal (2026-06-03): if a Chrome already holds THIS profile dir but
|
|
216
|
+
# is not answering CDP on our port, a fresh launch hands off to it via
|
|
217
|
+
# Chrome's SingletonLock and exits without ever binding our port — the
|
|
218
|
+
# old "failed to start within 12s" loop (8h Twitter outage overnight
|
|
219
|
+
# 2026-06-02/03, root cause: a server.py regression that dropped
|
|
220
|
+
# BH_PROFILE_NAME and collapsed the linkedin/twitter harness profiles
|
|
221
|
+
# onto this one, stranding an orphan on 9556). Reap the stale owner of
|
|
222
|
+
# our EXACT profile dir (trailing space in the pattern so browser-harness
|
|
223
|
+
# never matches browser-harness-linkedin) before relaunching.
|
|
224
|
+
local _prof_dir="$HOME/.claude/browser-profiles/browser-harness"
|
|
225
|
+
local _stale_pids
|
|
226
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
227
|
+
if [ -n "$_stale_pids" ] && ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
228
|
+
echo "[$(date +%H:%M:%S)] CDP down but Chrome still holds $_prof_dir (pids: $(echo $_stale_pids | tr '\n' ' ')); reaping stale profile owner before relaunch" >&2
|
|
229
|
+
kill $_stale_pids 2>/dev/null || true
|
|
230
|
+
sleep 2
|
|
231
|
+
_stale_pids=$(pgrep -f -- "--user-data-dir=$_prof_dir " 2>/dev/null || true)
|
|
232
|
+
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
233
|
+
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
234
|
+
fi
|
|
235
|
+
"$_chrome_bin" \
|
|
236
|
+
--remote-debugging-port=9555 \
|
|
237
|
+
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness" \
|
|
238
|
+
--no-first-run --no-default-browser-check \
|
|
239
|
+
--password-store=basic --use-mock-keychain \
|
|
240
|
+
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
|
|
241
|
+
--disable-backgrounding-occluded-windows \
|
|
242
|
+
"${_extra[@]}" \
|
|
243
|
+
"${BH_LAUNCH_URL:-https://x.com}" >/dev/null 2>&1 &
|
|
244
|
+
disown
|
|
245
|
+
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
246
|
+
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null && break
|
|
247
|
+
sleep 1
|
|
248
|
+
done
|
|
249
|
+
if ! curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null; then
|
|
250
|
+
echo "[$(date +%H:%M:%S)] ERROR: harness Chrome failed to start within 12s" >&2
|
|
251
|
+
return 1
|
|
252
|
+
fi
|
|
253
|
+
echo "[$(date +%H:%M:%S)] Harness Chrome up on port 9555" >&2
|
|
254
|
+
fi
|
|
255
|
+
# Re-inject the stored X session if the harness Chrome is logged out — e.g. a
|
|
256
|
+
# keychain re-lock wiped Chrome's encrypted Cookies SQLite on this launch
|
|
257
|
+
# (Gap B, 2026-06-02). restore_twitter_session.py reads the keychain-
|
|
258
|
+
# independent local cookie mirror (written by connect_x) and injects via CDP.
|
|
259
|
+
# No-op when already logged in; never blocks the cycle on failure. Runs on
|
|
260
|
+
# both the freshly-launched and already-up paths so a mid-life logout heals.
|
|
261
|
+
TWITTER_CDP_URL="http://127.0.0.1:9555" \
|
|
262
|
+
python3 "$HOME/social-autoposter/scripts/restore_twitter_session.py" 2>&1 \
|
|
263
|
+
| sed 's/^/[restore] /' >&2 || true
|
|
264
|
+
# Always close leftover tabs from prior runs. Safe under acquire_lock
|
|
265
|
+
# "twitter-browser" serialization (every caller of this function holds
|
|
266
|
+
# that lock), so we will not race with another active twitter run.
|
|
267
|
+
cleanup_harness_tabs
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
defer_if_foreign_for_backend() {
|
|
271
|
+
# Harness Chrome accepts multiple concurrent CDP clients on the same
|
|
272
|
+
# browser-harness profile, so a foreign MCP wrapper (Fazm Dev / IDE)
|
|
273
|
+
# cannot cause the SingletonLock contention that historically blocked
|
|
274
|
+
# the twitter-agent profile. Always return 1 (do not defer).
|
|
275
|
+
return 1
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# --- browser-harness `-c` capability self-heal (added 2026-06-02) -----------
|
|
279
|
+
# A stale ~/Developer/browser-harness checkout that PREDATES the `-c` interface
|
|
280
|
+
# makes `browser-harness -c "<script>"` print its usage string instead of
|
|
281
|
+
# running the script. The Phase 1 scan loop in run-twitter-cycle.sh then yields
|
|
282
|
+
# zero tweets with no obvious cause. cli.js documents the same failure for the
|
|
283
|
+
# bh_run MCP path. When this bit the testing machine, the debugging agent saw
|
|
284
|
+
# the `-c` flag, WRONGLY assumed it was unsupported, and proposed rewriting the
|
|
285
|
+
# call to a nonexistent "stdin form" (browser-harness has no stdin mode — `-c`
|
|
286
|
+
# is the only interface; see run.py). This runs at source-time, before any
|
|
287
|
+
# `-c` call, so all twitter harness scripts (cycle/threads/engage/dm/followups)
|
|
288
|
+
# get auto-repair. Static probe is one grep when fresh (zero steady-state cost);
|
|
289
|
+
# the git+uv refresh only fires when the checkout is actually stale.
|
|
290
|
+
_sa_harness_log() {
|
|
291
|
+
# Use the caller's log() FUNCTION when present; `declare -F` matches only a
|
|
292
|
+
# shell function, never the macOS /usr/bin/log binary (command -v would).
|
|
293
|
+
if declare -F log >/dev/null 2>&1; then log "$*"; else echo "[$(date +%H:%M:%S)] $*" >&2; fi
|
|
294
|
+
}
|
|
295
|
+
_sa_resolve_uv() {
|
|
296
|
+
local c
|
|
297
|
+
c="$(command -v uv 2>/dev/null)" && { echo "$c"; return 0; }
|
|
298
|
+
for c in "$HOME/.local/bin/uv" /opt/homebrew/bin/uv /usr/local/bin/uv; do
|
|
299
|
+
[ -x "$c" ] && { echo "$c"; return 0; }
|
|
300
|
+
done
|
|
301
|
+
return 1
|
|
302
|
+
}
|
|
303
|
+
ensure_harness_c_support() {
|
|
304
|
+
# Retired 2026-06-02. Upstream browser-harness removed `-c` in favor of
|
|
305
|
+
# stdin-heredoc (commits after merge-base 0e679e2); our server.py wrapper
|
|
306
|
+
# now passes scripts via stdin (input=script) so the CLI shape doesn't
|
|
307
|
+
# need any pre-flight probing. The old gate grepped run.py for `"-c"`
|
|
308
|
+
# which always fails against current upstream, and its "self-heal" was a
|
|
309
|
+
# `git reset --hard FETCH_HEAD` on ~/Developer/browser-harness that
|
|
310
|
+
# would clobber local commits AND not actually re-add `-c`. Keep the
|
|
311
|
+
# name + no-op return so older sourced contexts that call it don't break.
|
|
312
|
+
return 0
|
|
313
|
+
}
|
|
314
|
+
ensure_harness_c_support || true
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# link-edit-github.sh — Edit existing GitHub issue comments to append a project link.
|
|
3
|
+
# Uses the gh CLI (no browser needed). GitHub has no upvote system, so eligibility
|
|
4
|
+
# is based on our comment being posted 6h+ ago (engagement = a reply in the issue thread).
|
|
5
|
+
# Called by launchd (com.m13v.social-link-edit-github) every 6 hours.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Cycle ID for cross-cycle cost accounting (see run-github.sh for the same
|
|
10
|
+
# pattern). Stamps claude_sessions.cycle_id via env inheritance.
|
|
11
|
+
BATCH_ID="${BATCH_ID:-legh-$(date +%Y%m%d-%H%M%S)}"
|
|
12
|
+
export BATCH_ID
|
|
13
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
14
|
+
|
|
15
|
+
# Platform lock: wait up to 45min for any previous link-edit-github run, then skip
|
|
16
|
+
source "$(dirname "$0")/lock.sh"
|
|
17
|
+
acquire_lock "link-edit-github" 2700
|
|
18
|
+
|
|
19
|
+
# Load secrets
|
|
20
|
+
# shellcheck source=/dev/null
|
|
21
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
22
|
+
|
|
23
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
24
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
25
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
26
|
+
# HTTP-only lane (2026-06-01): all reads/writes go through the s4l.ai API via
|
|
27
|
+
# scripts/link_edit_helper.py. No DATABASE_URL, no psql, no fallback.
|
|
28
|
+
LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
29
|
+
|
|
30
|
+
mkdir -p "$LOG_DIR"
|
|
31
|
+
LOG_FILE="$LOG_DIR/link-edit-github-$(date +%Y-%m-%d_%H%M%S).log"
|
|
32
|
+
|
|
33
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
34
|
+
|
|
35
|
+
RUN_START=$(date +%s)
|
|
36
|
+
log "=== GitHub Link Edit Run: $(date) ==="
|
|
37
|
+
|
|
38
|
+
# A/B gate: per-post deterministic coin flip for the page-gen lane. Mirrors
|
|
39
|
+
# scripts/twitter_gen_links.py's TWITTER_PAGE_GEN_RATE behavior and the
|
|
40
|
+
# Reddit link-edit pipeline's LINK_EDIT_REDDIT_PAGE_GEN_RATE. 0.30 means
|
|
41
|
+
# ~30% of eligible posts get a brand-new SEO landing page built inline via
|
|
42
|
+
# Claude + git push; the other ~70% fall through to the project's homepage
|
|
43
|
+
# with link_source='plain_url_ab_skip'. Per-post hash via Postgres
|
|
44
|
+
# hashtext() so the same post stays in the same lane across cron retries.
|
|
45
|
+
# Tunable via env var so cadence sweeps don't need code changes. 0.0
|
|
46
|
+
# disables page-gen entirely (link insertion still happens with plain URL);
|
|
47
|
+
# 1.0 restores 100% page-gen.
|
|
48
|
+
# DEFAULT 0.0: GitHub no longer generates custom SEO pages — every eligible
|
|
49
|
+
# post goes through the wrap-an-existing-link route (homepage + short link).
|
|
50
|
+
LINK_EDIT_GITHUB_PAGE_GEN_RATE="${LINK_EDIT_GITHUB_PAGE_GEN_RATE:-0.0}"
|
|
51
|
+
PAGE_GEN_RATE_PCT=$(python3 -c "v=float('$LINK_EDIT_GITHUB_PAGE_GEN_RATE'); v=max(0.0,min(1.0,v)); print(int(round(v*100)))")
|
|
52
|
+
log "A/B gate: LINK_EDIT_GITHUB_PAGE_GEN_RATE=$LINK_EDIT_GITHUB_PAGE_GEN_RATE (page_gen_lane='page_gen' on ~${PAGE_GEN_RATE_PCT}% of eligible posts; rest go to plain_url_ab_skip)"
|
|
53
|
+
|
|
54
|
+
EDITABLE=$(python3 "$LE_HELPER" eligible --platform github --page-gen-rate-pct "$PAGE_GEN_RATE_PCT" --order posted_at 2>/dev/null || echo "")
|
|
55
|
+
|
|
56
|
+
if [ "$EDITABLE" = "null" ] || [ -z "$EDITABLE" ]; then
|
|
57
|
+
log "No GitHub posts eligible for link edit"
|
|
58
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_github" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
EDITABLE_COUNT=$(echo "$EDITABLE" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
63
|
+
log "GitHub: $EDITABLE_COUNT posts eligible for link edit"
|
|
64
|
+
|
|
65
|
+
PROMPT_FILE=$(mktemp)
|
|
66
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
67
|
+
You are the Social Autoposter GitHub link-edit bot.
|
|
68
|
+
|
|
69
|
+
Read $SKILL_FILE for the full workflow. Execute the GitHub link-edit phase only. GitHub edits are done via the gh CLI (no browser). GitHub has no upvote system; engagement = someone replied to our comment in the issue thread.
|
|
70
|
+
|
|
71
|
+
CRITICAL: This is a single-shot run. NEVER call ScheduleWakeup, CronCreate, CronDelete, CronList, EnterPlanMode, EnterWorktree, or any deferred-execution / scheduling tool. You MUST complete or skip every post in this one run; do not defer work to "a future run". If you hit a hard block, mark the post SKIPPED via step 9 and move on to the next post.
|
|
72
|
+
|
|
73
|
+
GitHub posts eligible for editing:
|
|
74
|
+
$EDITABLE
|
|
75
|
+
|
|
76
|
+
Process ALL of them. For each post:
|
|
77
|
+
1. Read ~/social-autoposter/config.json to get the projects list.
|
|
78
|
+
2. Pick the project whose topics are the CLOSEST match to thread_title + our_content. Check the project_name column first; if set, use that project directly. Otherwise match by topics. Be generous: if the thread touches agents, automation, desktop, memory, or anything related to the project descriptions, it's a match. If truly nothing fits, mark it skipped (see step 9) and move on. Frame it as recommending a cool tool you've come across, NOT as something you built.
|
|
79
|
+
3. PAGE-GEN LANE GATE — read the post's \`page_gen_lane\` field (set deterministically by the pipeline; do NOT override).
|
|
80
|
+
- If \`page_gen_lane == "ab_skip"\`: SKIP the full SEO page generation entirely. Set LINK_URL = the matched project's homepage from config.json (the \`website\` field) and LINK_SOURCE="plain_url_ab_skip". Continue to step 4. The /r/<code> short-link wrap in step 5 still mints attribution on the project's own domain, so we get click data for this lane to compare against seo_page lane CTR.
|
|
81
|
+
- If \`page_gen_lane == "page_gen"\` AND the matched project has a landing_pages config: continue to step 3a below.
|
|
82
|
+
- If \`page_gen_lane == "page_gen"\` BUT the matched project has NO landing_pages config: skip page-gen, set LINK_URL = project homepage (website if available, otherwise github), LINK_SOURCE="plain_url_no_lp", continue to step 4.
|
|
83
|
+
|
|
84
|
+
3a. If the matched project has a landing_pages config (with repo, base_url):
|
|
85
|
+
a. Think about what SEO-optimized guide page would fit this specific thread naturally. Consider the thread's audience, their pain points, industry jargon, and what they'd actually find useful. The page should NOT feel like a landing page; it should feel like a genuine 1000-2000 word guide or resource.
|
|
86
|
+
b. cd into the project repo (landing_pages.repo)
|
|
87
|
+
c. Look at existing pages under src/app/t/ to understand the site's style, layout components (Navbar, Footer), and theme
|
|
88
|
+
d. Create a NEW standalone page as src/app/t/{seo-friendly-slug}/page.tsx; this is a real Next.js page with its own Metadata export, not a JSON entry. Include:
|
|
89
|
+
- Proper <Metadata> with title, description, openGraph, twitter tags
|
|
90
|
+
- Reuse the site's Navbar and Footer components (import or inline them)
|
|
91
|
+
- Use the CTAButton component from @/components/cta-button for ALL call-to-action buttons (it tracks clicks in PostHog automatically). Import: import { CTAButton } from "@/components/cta-button";
|
|
92
|
+
- A full article-style page: hero headline, table of contents, 5-7 content sections, comparison tables with real numbers, bullet lists with specific data points, and a CTA section at the bottom
|
|
93
|
+
- The content must be 1000-2000 words. Pull real context from the project's config (pricing, features, proof_points, competitive_positioning) and from web research to make it concrete and authoritative
|
|
94
|
+
- Naturally mention the product as ONE solution among the options discussed; don't make the whole page a sales pitch
|
|
95
|
+
e. git add the new page && git commit -m "Add guide: SHORT_DESCRIPTION" && git push
|
|
96
|
+
f. Wait ~35s for Vercel deploy, then curl -sI {base_url}/t/{slug} to verify HTTP 200
|
|
97
|
+
g. On success, set LINK_URL = the deployed page URL and LINK_SOURCE="seo_page". On deploy failure, fall back GRACEFULLY: set LINK_URL = the project's homepage from config.json (the \`website\` field), set LINK_SOURCE="plain_url_fallback:deploy_failed". Do NOT skip the post; continue to step 4.
|
|
98
|
+
4. Write 1 sentence + project link (GitHub peer tone). Voice depends on the matched project's \`voice_relationship\` field in config.json (read it before drafting):
|
|
99
|
+
- voice_relationship == "first_party": Claim ownership. Examples: "fwiw we built an implementation of this, URL" or "I shipped a tool that does this, URL". NEVER write "I found this", "there's a tool", "came across this implementation".
|
|
100
|
+
- voice_relationship == "third_party": You are an outside observer pointing at the project's mechanism. Example: "fwiw PROJECT_NAME has an implementation of this, URL". Do NOT use "I built" / "we shipped" / "we made". Do NOT use "I found this" / "came across this" either; stay matter-of-fact.
|
|
101
|
+
5. URL-WRAP THE LINK TEXT for click attribution. Run:
|
|
102
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py wrap-post-text \\
|
|
103
|
+
--text "YOUR_LINK_SENTENCE_WITH_URL" \\
|
|
104
|
+
--platform github_issues \\
|
|
105
|
+
--project PROJECT_NAME
|
|
106
|
+
Parse the JSON output. Use \`text\` (URL replaced with /r/<code>) as the FINAL LINK_TEXT for steps 6 and 7. Keep \`minted_session\` for step 8. If wrap returns ok=false, log the error and skip this post (do NOT post a raw URL).
|
|
107
|
+
6. Extract OWNER/REPO from thread_url. Extract COMMENT_ID from our_url; if not directly available, use gh api to find our comment on that issue.
|
|
108
|
+
7. Edit the existing comment (append the wrapped LINK_TEXT to the existing content) using gh:
|
|
109
|
+
gh api repos/OWNER/REPO/issues/comments/COMMENT_ID -X PATCH -f body="EXISTING_CONTENT
|
|
110
|
+
|
|
111
|
+
WRAPPED_LINK_TEXT"
|
|
112
|
+
8. After each successful edit, update the DB (via the HTTP API helper; pass link_source so we can A/B compare seo_page vs plain_url_ab_skip vs plain_url_fallback:* vs plain_url_no_lp click-through rates, same as Twitter does in scripts/twitter_gen_links.py and the Reddit link-edit pipeline does) and backfill short-link attribution:
|
|
113
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-edited --post-id POST_ID --content "LINK_TEXT" --source "LINK_SOURCE"
|
|
114
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py backfill-post --minted-session MINTED_SESSION --post-id POST_ID
|
|
115
|
+
9. COMMITMENT GUARDRAILS (never violate these):
|
|
116
|
+
- NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.
|
|
117
|
+
- NEVER promise to share links, files, or resources you don't have right now. Only share links from config.json projects (plus any new landing page you just deployed).
|
|
118
|
+
- NEVER offer to DM or send anything outside the comment.
|
|
119
|
+
- NEVER make time-bound promises.
|
|
120
|
+
10. If a post is SKIPPED (no project match, comment not found, issue locked, 404, bad URL), ALWAYS mark it so it won't be retried:
|
|
121
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-skipped --post-id POST_ID --reason "REASON"
|
|
122
|
+
PROMPT_EOF
|
|
123
|
+
|
|
124
|
+
gtimeout 1800 "$REPO_DIR/scripts/run_claude.sh" "link-edit-github" --strict-mcp-config --mcp-config "$HOME/.claude/browser-agent-configs/no-agents-mcp.json" --disallowed-tools "ScheduleWakeup,CronCreate,CronDelete,CronList,EnterPlanMode,EnterWorktree" --output-format stream-json --verbose -p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: GitHub link-edit claude exited with code $?"
|
|
125
|
+
rm -f "$PROMPT_FILE"
|
|
126
|
+
|
|
127
|
+
EDITED=$(python3 "$LE_HELPER" edited-count --platform github 2>/dev/null || echo "0")
|
|
128
|
+
log "GitHub link-edit complete. Total github posts edited (all-time): $EDITED"
|
|
129
|
+
|
|
130
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
131
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "link-edit-github" 2>/dev/null || echo "0.0000")
|
|
132
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_github" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
133
|
+
|
|
134
|
+
find "$LOG_DIR" -name "link-edit-github-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
135
|
+
|
|
136
|
+
log "=== GitHub link-edit complete: $(date) ==="
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# link-edit-moltbook.sh — Edit high-performing Moltbook comments to append a project link.
|
|
3
|
+
# Moltbook uses the PATCH API (no browser needed).
|
|
4
|
+
# Called by launchd (com.m13v.social-link-edit-moltbook) every 6 hours.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Cycle ID for cross-cycle cost accounting (see run-moltbook.sh for the same
|
|
9
|
+
# pattern). Stamps claude_sessions.cycle_id via env inheritance.
|
|
10
|
+
BATCH_ID="${BATCH_ID:-lemb-$(date +%Y%m%d-%H%M%S)}"
|
|
11
|
+
export BATCH_ID
|
|
12
|
+
export SA_CYCLE_ID="$BATCH_ID"
|
|
13
|
+
|
|
14
|
+
# Platform lock: wait up to 45min for any previous link-edit-moltbook run, then skip
|
|
15
|
+
source "$(dirname "$0")/lock.sh"
|
|
16
|
+
acquire_lock "link-edit-moltbook" 2700
|
|
17
|
+
|
|
18
|
+
# Load secrets
|
|
19
|
+
# shellcheck source=/dev/null
|
|
20
|
+
[ -f "$HOME/social-autoposter/.env" ] && source "$HOME/social-autoposter/.env"
|
|
21
|
+
|
|
22
|
+
REPO_DIR="$HOME/social-autoposter"
|
|
23
|
+
SKILL_FILE="$REPO_DIR/SKILL.md"
|
|
24
|
+
LOG_DIR="$REPO_DIR/skill/logs"
|
|
25
|
+
# HTTP-only lane (2026-06-01): all reads/writes go through the s4l.ai API via
|
|
26
|
+
# scripts/link_edit_helper.py. No DATABASE_URL, no psql, no fallback.
|
|
27
|
+
LE_HELPER="$REPO_DIR/scripts/link_edit_helper.py"
|
|
28
|
+
|
|
29
|
+
mkdir -p "$LOG_DIR"
|
|
30
|
+
LOG_FILE="$LOG_DIR/link-edit-moltbook-$(date +%Y-%m-%d_%H%M%S).log"
|
|
31
|
+
|
|
32
|
+
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
|
|
33
|
+
|
|
34
|
+
RUN_START=$(date +%s)
|
|
35
|
+
log "=== Moltbook Link Edit Run: $(date) ==="
|
|
36
|
+
|
|
37
|
+
EDITABLE=$(python3 "$LE_HELPER" eligible --platform moltbook --min-upvotes-exclusive 2 --order upvotes 2>/dev/null || echo "")
|
|
38
|
+
|
|
39
|
+
if [ "$EDITABLE" = "null" ] || [ -z "$EDITABLE" ]; then
|
|
40
|
+
log "No Moltbook posts eligible for link edit"
|
|
41
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_moltbook" --posted 0 --skipped 0 --failed 0 --cost 0 --elapsed $(( $(date +%s) - RUN_START ))
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
EDITABLE_COUNT=$(echo "$EDITABLE" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
46
|
+
log "Moltbook: $EDITABLE_COUNT posts eligible for link edit"
|
|
47
|
+
|
|
48
|
+
PROMPT_FILE=$(mktemp)
|
|
49
|
+
cat > "$PROMPT_FILE" <<PROMPT_EOF
|
|
50
|
+
You are the Social Autoposter Moltbook link-edit bot.
|
|
51
|
+
|
|
52
|
+
Read $SKILL_FILE for the full workflow. Execute the Moltbook link-edit phase only. Moltbook uses the PATCH API; no browser is needed.
|
|
53
|
+
|
|
54
|
+
CRITICAL: This is a single-shot run. NEVER call ScheduleWakeup, CronCreate, CronDelete, CronList, EnterPlanMode, EnterWorktree, or any deferred-execution / scheduling tool. You MUST complete or skip every post in this one run; do not defer work to "a future run". If you hit a hard block, mark the post SKIPPED via step 9 and move on to the next post.
|
|
55
|
+
|
|
56
|
+
Moltbook posts eligible for editing:
|
|
57
|
+
$EDITABLE
|
|
58
|
+
|
|
59
|
+
Process ALL of them. For each post:
|
|
60
|
+
1. Read ~/social-autoposter/config.json to get the projects list.
|
|
61
|
+
2. Pick the project whose topics are the CLOSEST match to thread_title + our_content. Check the project_name column first; if set, use that project directly. Otherwise match by topics. Be generous: if the thread touches agents, automation, desktop, memory, or anything related to the project descriptions, it's a match. If truly nothing fits, mark it skipped (see step 8) and move on. Frame it as recommending a cool tool you've come across, NOT as something you built.
|
|
62
|
+
3. If the matched project has a landing_pages config (with repo, base_url):
|
|
63
|
+
a. Think about what SEO-optimized guide page would fit this specific thread naturally. Consider the thread's audience, their pain points, industry jargon, and what they'd actually find useful. The page should NOT feel like a landing page; it should feel like a genuine 1000-2000 word guide or resource.
|
|
64
|
+
b. cd into the project repo (landing_pages.repo)
|
|
65
|
+
c. Look at existing pages under src/app/t/ to understand the site's style, layout components (Navbar, Footer), and theme
|
|
66
|
+
d. Create a NEW standalone page as src/app/t/{seo-friendly-slug}/page.tsx; this is a real Next.js page with its own Metadata export, not a JSON entry. Include:
|
|
67
|
+
- Proper <Metadata> with title, description, openGraph, twitter tags
|
|
68
|
+
- Reuse the site's Navbar and Footer components (import or inline them)
|
|
69
|
+
- Use the CTAButton component from @/components/cta-button for ALL call-to-action buttons (it tracks clicks in PostHog automatically). Import: import { CTAButton } from "@/components/cta-button";
|
|
70
|
+
- A full article-style page: hero headline, table of contents, 5-7 content sections, comparison tables with real numbers, bullet lists with specific data points, and a CTA section at the bottom
|
|
71
|
+
- The content must be 1000-2000 words. Pull real context from the project's config (pricing, features, proof_points, competitive_positioning) and from web research to make it concrete and authoritative
|
|
72
|
+
- Naturally mention the product as ONE solution among the options discussed; don't make the whole page a sales pitch
|
|
73
|
+
e. git add the new page && git commit -m "Add guide: SHORT_DESCRIPTION" && git push
|
|
74
|
+
f. Wait ~35s for Vercel deploy, then curl -sI {base_url}/t/{slug} to verify HTTP 200
|
|
75
|
+
g. Use THAT page URL in the link edit. If deploy fails, fall back to the project's website URL.
|
|
76
|
+
If no landing_pages config: use website if available, otherwise github.
|
|
77
|
+
4. Write 1 casual sentence + project link (Moltbook agent voice). Voice depends on the matched project's \`voice_relationship\` field in config.json (read it before drafting):
|
|
78
|
+
- voice_relationship == "first_party": Claim ownership. Examples: "I built X for this kind of thing, URL" or "we made this tool that handles it, URL". NEVER write "there's this cool tool", "I found this", "came across this".
|
|
79
|
+
- voice_relationship == "third_party": You are an outside observer pointing at the project's mechanism. Example: "PROJECT_NAME handles this kind of thing, URL". Do NOT use "I built" / "we made". Do NOT use "I found this" / "came across this" either; stay matter-of-fact.
|
|
80
|
+
5. URL-WRAP THE LINK TEXT for click attribution. Run:
|
|
81
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py wrap-post-text \\
|
|
82
|
+
--text "YOUR_LINK_SENTENCE_WITH_URL" \\
|
|
83
|
+
--platform moltbook \\
|
|
84
|
+
--project PROJECT_NAME
|
|
85
|
+
Parse the JSON output. Use \`text\` (URL replaced with /r/<code>) as the FINAL LINK_TEXT for steps 6 and 7. Keep \`minted_session\` for step 8. If wrap returns ok=false, log the error and skip this post (do NOT post a raw URL).
|
|
86
|
+
6. Append the wrapped LINK_TEXT to our_content with a blank line separator.
|
|
87
|
+
7. Extract the comment UUID from our_url (the part after #comment-), then PATCH the comment:
|
|
88
|
+
source ~/social-autoposter/.env
|
|
89
|
+
curl -s -X PATCH -H "Authorization: Bearer \$MOLTBOOK_API_KEY" \\
|
|
90
|
+
-H "Content-Type: application/json" \\
|
|
91
|
+
-d '{"content": "FULL_CONTENT"}' \\
|
|
92
|
+
"https://www.moltbook.com/api/v1/comments/COMMENT_UUID"
|
|
93
|
+
8. After each successful edit, update the DB (via the HTTP API helper) and backfill short-link attribution:
|
|
94
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-edited --post-id POST_ID --content "LINK_TEXT"
|
|
95
|
+
python3 ~/social-autoposter/scripts/dm_short_links.py backfill-post --minted-session MINTED_SESSION --post-id POST_ID
|
|
96
|
+
9. COMMITMENT GUARDRAILS (never violate these):
|
|
97
|
+
- NEVER suggest, offer, or agree to calls, meetings, demos, or video chats.
|
|
98
|
+
- NEVER promise to share links, files, or resources you don't have right now. Only share links from config.json projects (plus any new landing page you just deployed).
|
|
99
|
+
- NEVER offer to DM or send anything outside the comment.
|
|
100
|
+
- NEVER make time-bound promises.
|
|
101
|
+
10. If a post is SKIPPED (no project match, comment not found, removed, bad URL), ALWAYS mark it so it won't be retried:
|
|
102
|
+
python3 ~/social-autoposter/scripts/link_edit_helper.py mark-skipped --post-id POST_ID --reason "REASON"
|
|
103
|
+
PROMPT_EOF
|
|
104
|
+
|
|
105
|
+
gtimeout 1800 "$REPO_DIR/scripts/run_claude.sh" "link-edit-moltbook" --strict-mcp-config --mcp-config "$HOME/.claude/browser-agent-configs/no-agents-mcp.json" --disallowed-tools "ScheduleWakeup,CronCreate,CronDelete,CronList,EnterPlanMode,EnterWorktree" -p "$(cat "$PROMPT_FILE")" 2>&1 | tee -a "$LOG_FILE" || log "WARNING: Moltbook link-edit claude exited with code $?"
|
|
106
|
+
rm -f "$PROMPT_FILE"
|
|
107
|
+
|
|
108
|
+
EDITED=$(python3 "$LE_HELPER" edited-count --platform moltbook 2>/dev/null || echo "0")
|
|
109
|
+
log "Moltbook link-edit complete. Total moltbook posts edited (all-time): $EDITED"
|
|
110
|
+
|
|
111
|
+
RUN_ELAPSED=$(( $(date +%s) - RUN_START ))
|
|
112
|
+
_COST=$(python3 "$REPO_DIR/scripts/get_run_cost.py" --since "$RUN_START" --scripts "link-edit-moltbook" 2>/dev/null || echo "0.0000")
|
|
113
|
+
python3 "$REPO_DIR/scripts/log_run.py" --script "link_edit_moltbook" --posted 0 --skipped 0 --failed 0 --cost "$_COST" --elapsed "$RUN_ELAPSED"
|
|
114
|
+
|
|
115
|
+
find "$LOG_DIR" -name "link-edit-moltbook-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
116
|
+
|
|
117
|
+
log "=== Moltbook link-edit complete: $(date) ==="
|