@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,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) ==="