@m13v/s4l 1.6.197-rc.10

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 +1336 -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 +513 -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,556 @@
1
+ #!/usr/bin/env python3
2
+ """On-page status overlay for the social-autoposter browser harness.
3
+
4
+ When the twitter harness drives its dedicated Chrome (port 9555 by default),
5
+ the window can look frozen for long stretches while it scans / drafts / posts.
6
+ This module injects a small, non-interactive overlay into the harness Chrome so
7
+ the user knows (a) they can keep working in other apps and leave the window in
8
+ the background (just don't close it), and (b) what the harness is doing right
9
+ now, streamed live.
10
+
11
+ Design constraints, deliberately:
12
+ - pointer-events: none on the overlay so it NEVER intercepts the automation's
13
+ own clicks. It is purely cosmetic.
14
+ - CSP-safe: the overlay is built with createElement + element.style.<prop> +
15
+ textContent only. No innerHTML-with-style-attributes and no injected <style>
16
+ tag, both of which x.com's CSP can refuse. The "pulse" + "updated Ns ago"
17
+ ticker are driven by a JS setInterval, not CSS @keyframes.
18
+ - Survives navigation two ways: (1) Playwright add_init_script registers the
19
+ builder on the browser context so every new document re-creates it, and
20
+ (2) the watch loop re-asserts it via evaluate every couple seconds.
21
+
22
+ This file is standalone and owns its own integration. It does NOT edit any of
23
+ the locked pipeline scripts. Drive it from the CLI:
24
+
25
+ python3 harness_overlay.py install # show overlay now
26
+ python3 harness_overlay.py status "drafting reply" # update the status line
27
+ python3 harness_overlay.py clear # remove the overlay
28
+ python3 harness_overlay.py watch # stream the live cycle
29
+ # log into the overlay
30
+
31
+ `watch` is the always-on mode: it tails the newest skill/logs/twitter-cycle-*.log
32
+ and pushes a friendly one-liner into the overlay as each step lands, with a
33
+ heartbeat so even idle-looking moments read as alive. If the harness Chrome is
34
+ down it sleeps and retries; it never crashes the pipeline.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import fcntl
40
+ import glob
41
+ import json
42
+ import os
43
+ import re
44
+ import signal
45
+ import sys
46
+ import time
47
+ from pathlib import Path
48
+
49
+ # --- self-heal interpreter: Playwright must be importable -------------------
50
+ # The pipeline's bare `python3` on this Mac can resolve to a Python without
51
+ # Playwright (3.14). Mirror the linkedin-backend.sh resolver: re-exec under the
52
+ # first interpreter that actually has playwright before doing any real work.
53
+ def _ensure_playwright_interpreter() -> None:
54
+ try:
55
+ import playwright # noqa: F401
56
+ return
57
+ except Exception:
58
+ pass
59
+ if os.environ.get("_S4L_OVERLAY_REEXEC") == "1":
60
+ return # already tried; fall through and let the import error surface
61
+ for cand in (
62
+ "/opt/homebrew/bin/python3.11",
63
+ "/usr/bin/python3",
64
+ "/opt/homebrew/bin/python3",
65
+ ):
66
+ if Path(cand).exists() and os.path.realpath(cand) != os.path.realpath(sys.executable):
67
+ env = dict(os.environ, _S4L_OVERLAY_REEXEC="1")
68
+ os.execve(cand, [cand, os.path.abspath(__file__), *sys.argv[1:]], env)
69
+
70
+
71
+ _ensure_playwright_interpreter()
72
+
73
+ # --- config -----------------------------------------------------------------
74
+
75
+ CDP_URL = os.environ.get("TWITTER_CDP_URL", "http://127.0.0.1:9555").strip()
76
+ LOG_DIR = Path(os.environ.get("S4L_LOG_DIR", str(Path.home() / "social-autoposter" / "skill" / "logs")))
77
+ # How stale a cycle log can be (seconds) before we treat the harness as idle.
78
+ IDLE_AFTER_SEC = int(os.environ.get("S4L_OVERLAY_IDLE_SEC", "240"))
79
+
80
+ TITLE = "S4L"
81
+ REASSURE = (
82
+ "Working in the background. You can keep using other apps and leave this "
83
+ "window behind \u2014 just don\u2019t close it."
84
+ )
85
+
86
+ # --- the page-side overlay builder ------------------------------------------
87
+ # `_BODY` defines window.__sapsPaint(payload): idempotently creates the overlay
88
+ # DOM, then updates its text. A lone setInterval drives both the pulse and the
89
+ # "updated Ns ago" ticker so the overlay always looks alive between status
90
+ # pushes. Built with createElement + element.style.<prop> + textContent only
91
+ # (CSP-safe; no <style> tag, no innerHTML-with-style-attrs). pointer-events is
92
+ # none so the overlay can never intercept the automation's own clicks.
93
+ _BODY = r"""
94
+ window.__sapsAnnounce = function(payload){
95
+ // One-time, dismissible-forever launch notice. The reassurance disclaimer
96
+ // lives HERE (a big centered modal with an OK button) instead of eating space
97
+ // in the always-on status overlay. Once OK is clicked we stamp localStorage so
98
+ // it never shows again. Best-effort + CSP-safe (createElement/style/textContent
99
+ // + addEventListener only); never throws into the page.
100
+ try {
101
+ var KEY = "__saps_announce_v1";
102
+ var dismissed = false;
103
+ try { dismissed = window.localStorage.getItem(KEY) === "1"; } catch(e) {}
104
+ if(window.__sapsAnnounceDismissed) dismissed = true; // session fallback if storage is blocked
105
+ if(dismissed) return;
106
+ if(document.getElementById("__saps_announce")) return;
107
+
108
+ function mk(tag, parent){ var e=document.createElement(tag); if(parent)parent.appendChild(e); return e; }
109
+
110
+ var back = mk("div", document.documentElement); back.id = "__saps_announce";
111
+ var bs = back.style;
112
+ bs.position="fixed"; bs.top="0"; bs.left="0"; bs.width="100vw"; bs.height="100vh";
113
+ bs.zIndex="2147483647"; bs.display="flex";
114
+ bs.alignItems="center"; bs.justifyContent="center";
115
+ bs.background="rgba(0,0,0,0.55)";
116
+ bs.backdropFilter="blur(3px)"; bs.webkitBackdropFilter="blur(3px)";
117
+ // The ENTIRE modal (backdrop + card + text) is pointer-events:none so that a
118
+ // bot click during this one-time window always passes through to the page,
119
+ // even if the user never clicks OK. The OK button is the ONLY element that
120
+ // re-enables pointer-events, so it stays clickable while everything else is
121
+ // transparent to the automation's CDP/hit-test clicks.
122
+ bs.pointerEvents="none";
123
+
124
+ var card = mk("div", back); var cs = card.style;
125
+ cs.pointerEvents="none";
126
+ cs.boxSizing="border-box"; cs.maxWidth="440px"; cs.width="86%";
127
+ cs.padding="26px 26px 22px"; cs.borderRadius="16px";
128
+ cs.background="rgba(20,20,23,0.98)"; cs.color="#fff"; cs.textAlign="center";
129
+ cs.font="14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif";
130
+ cs.boxShadow="0 12px 48px rgba(0,0,0,0.55)"; cs.border="1px solid rgba(255,255,255,0.14)";
131
+
132
+ var ttl = mk("div", card); ttl.textContent = (payload && payload.title) || "S4L is running";
133
+ ttl.style.fontSize="19px"; ttl.style.fontWeight="700"; ttl.style.letterSpacing="0.3px";
134
+ ttl.style.marginBottom="10px";
135
+
136
+ var body = mk("div", card);
137
+ body.textContent = (payload && payload.reassure) || "";
138
+ body.style.opacity="0.82"; body.style.fontSize="14px"; body.style.marginBottom="22px";
139
+
140
+ var ok = mk("button", card); ok.textContent="OK";
141
+ var os_ = ok.style;
142
+ os_.pointerEvents="auto"; // the ONLY clickable thing; rest of modal is click-through
143
+ os_.cursor="pointer"; os_.appearance="none"; os_.webkitAppearance="none";
144
+ os_.border="1px solid rgba(255,255,255,0.18)"; os_.borderRadius="10px";
145
+ os_.padding="9px 30px"; os_.fontSize="14px"; os_.fontWeight="600";
146
+ os_.background="#fff"; os_.color="#111"; os_.font="inherit";
147
+ os_.fontWeight="600"; os_.minWidth="120px";
148
+ ok.addEventListener("click", function(){
149
+ try { window.localStorage.setItem(KEY, "1"); } catch(e) {}
150
+ window.__sapsAnnounceDismissed = true; // session fallback if storage is blocked
151
+ if(back && back.remove) back.remove();
152
+ });
153
+ } catch(e) { /* announcement is best-effort, never throw into the page */ }
154
+ };
155
+
156
+ window.__sapsPaint = function(payload){
157
+ try {
158
+ var ID = "__saps_overlay";
159
+ var st = window.__sapsOverlayState || (window.__sapsOverlayState = {});
160
+ st.title = payload.title; st.reassure = payload.reassure;
161
+ st.status = payload.status; st.ts = payload.ts || Date.now();
162
+
163
+ // Surface the one-time launch notice (carries the reassurance disclaimer).
164
+ try { window.__sapsAnnounce({title: st.title + " is running", reassure: st.reassure}); } catch(e){}
165
+
166
+ function mk(tag, parent){ var e=document.createElement(tag); if(parent)parent.appendChild(e); return e; }
167
+
168
+ var root = document.getElementById(ID);
169
+ if(!root || !root.isConnected){
170
+ root = mk("div", document.documentElement); root.id = ID;
171
+ var s = root.style;
172
+ // Centered both axes. pointerEvents:none so the overlay can NEVER
173
+ // intercept the automation's clicks: the bot clicks by raw CDP screen
174
+ // coordinates (Input.dispatchMouseEvent) and by Playwright hit-testing,
175
+ // both of which an opaque clickable card sitting over a target would eat.
176
+ s.position="fixed"; s.top="50%"; s.left="50%"; s.transform="translate(-50%,-50%)";
177
+ // Sit one below the announce modal (2147483647) so the one-time "S4L is
178
+ // running" notice + its OK button always stack ON TOP of this always-on
179
+ // status box. They're both screen-centered, so equal z-index would let
180
+ // whichever was appended last (this overlay) cover the OK button.
181
+ s.zIndex="2147483646"; s.pointerEvents="none"; s.maxWidth="460px";
182
+ s.boxSizing="border-box"; s.padding="10px 14px"; s.borderRadius="12px";
183
+ s.background="rgba(15,15,17,0.92)"; s.color="#fff";
184
+ s.font="13px/1.35 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif";
185
+ s.boxShadow="0 6px 22px rgba(0,0,0,0.35)"; s.border="1px solid rgba(255,255,255,0.12)";
186
+ s.backdropFilter="blur(6px)"; s.webkitBackdropFilter="blur(6px)";
187
+
188
+ var head = mk("div", root); head.style.display="flex"; head.style.alignItems="center"; head.style.gap="8px";
189
+ head.style.cursor="move"; head.style.userSelect="none"; head.style.webkitUserSelect="none";
190
+ var dot = mk("span", head); st._dot = dot;
191
+ dot.style.width="9px"; dot.style.height="9px"; dot.style.borderRadius="50%";
192
+ dot.style.background="#fff"; dot.style.flex="0 0 auto"; dot.style.opacity="1";
193
+ var ttl = mk("span", head); st._title = ttl;
194
+ ttl.style.fontWeight="600"; ttl.style.letterSpacing="0.2px";
195
+ var ago = mk("span", head); st._ago = ago;
196
+ ago.style.marginLeft="auto"; ago.style.opacity="0.55"; ago.style.fontSize="11px";
197
+ ago.style.fontVariantNumeric="tabular-nums";
198
+
199
+ var stat = mk("div", root); st._status = stat;
200
+ stat.style.marginTop="6px"; stat.style.fontWeight="500";
201
+ stat.style.whiteSpace="nowrap"; stat.style.overflow="hidden"; stat.style.textOverflow="ellipsis";
202
+
203
+ // --- drag-to-move (grab the header) ---------------------------------
204
+ (function(){
205
+ var drag = null; // {dx, dy}
206
+ head.addEventListener("mousedown", function(ev){
207
+ try {
208
+ var r = root.getBoundingClientRect();
209
+ drag = {dx: ev.clientX - r.left, dy: ev.clientY - r.top};
210
+ root.style.transform = "none";
211
+ root.style.left = r.left + "px";
212
+ root.style.top = r.top + "px";
213
+ ev.preventDefault();
214
+ } catch(e) { drag = null; }
215
+ });
216
+ document.addEventListener("mousemove", function(ev){
217
+ if(!drag) return;
218
+ var x = ev.clientX - drag.dx, y = ev.clientY - drag.dy;
219
+ var maxX = Math.max(0, window.innerWidth - root.offsetWidth);
220
+ var maxY = Math.max(0, window.innerHeight - root.offsetHeight);
221
+ root.style.left = Math.min(Math.max(0, x), maxX) + "px";
222
+ root.style.top = Math.min(Math.max(0, y), maxY) + "px";
223
+ });
224
+ document.addEventListener("mouseup", function(){ drag = null; });
225
+ })();
226
+
227
+ if(st._iv) clearInterval(st._iv);
228
+ st._iv = setInterval(function(){
229
+ try{
230
+ var dt = Math.max(0, Math.round((Date.now()-st.ts)/1000));
231
+ st._ago.textContent = dt < 1 ? "now" : (dt < 60 ? dt+"s ago" : Math.round(dt/60)+"m ago");
232
+ var stale = dt > 90; // fade the dot once activity goes quiet
233
+ var phase = (Date.now()/650) % 2;
234
+ st._dot.style.opacity = stale ? "0.3" : (phase < 1 ? "1" : "0.35");
235
+ }catch(e){}
236
+ }, 250);
237
+ }
238
+ st._title.textContent = st.title;
239
+ st._status.textContent = st.status;
240
+ } catch(e) { /* overlay is best-effort, never throw into the page */ }
241
+ };
242
+ """
243
+
244
+ # Playwright evaluate expression: (re)define the painter, then call it with the
245
+ # arg Playwright passes. Used for live updates on existing pages.
246
+ PAINT_EXPR = "(payload) => { " + _BODY + " try { window.__sapsPaint(payload); } catch(e){} }"
247
+
248
+ # Removes the overlay from a page.
249
+ CLEAR_EXPR = (
250
+ "() => { var e=document.getElementById('__saps_overlay'); if(e&&e.remove)e.remove(); "
251
+ "var a=document.getElementById('__saps_announce'); if(a&&a.remove)a.remove(); "
252
+ "var s=window.__sapsOverlayState; if(s&&s._iv)clearInterval(s._iv); }"
253
+ )
254
+
255
+
256
+ def _build_init_script(title: str, reassure: str, status: str) -> str:
257
+ """add_init_script body: define the painter on every new document and seed
258
+ it with the latest known text so a mid-cycle navigation paints instantly."""
259
+ seed = json.dumps({"title": title, "reassure": reassure, "status": status})
260
+ return _BODY + (
261
+ "try { var __p = " + seed + "; __p.ts = Date.now(); window.__sapsPaint(__p); } catch(e){}"
262
+ )
263
+
264
+
265
+ # --- CDP plumbing via Playwright (same path the poster uses) ----------------
266
+
267
+ class Harness:
268
+ """Thin wrapper that attaches to the harness Chrome over CDP and paints the
269
+ overlay onto every page in the default context. Best-effort throughout."""
270
+
271
+ def __init__(self, cdp_url: str = CDP_URL):
272
+ self.cdp_url = cdp_url
273
+ self._pw = None
274
+ self._browser = None
275
+
276
+ def __enter__(self):
277
+ from playwright.sync_api import sync_playwright
278
+ self._pw = sync_playwright().start()
279
+ self._browser = self._pw.chromium.connect_over_cdp(self.cdp_url, timeout=5000)
280
+ return self
281
+
282
+ def __exit__(self, *exc):
283
+ try:
284
+ if self._browser:
285
+ self._browser.close()
286
+ except Exception:
287
+ pass
288
+ try:
289
+ if self._pw:
290
+ self._pw.stop()
291
+ except Exception:
292
+ pass
293
+
294
+ def _pages(self):
295
+ pages = []
296
+ for ctx in self._browser.contexts:
297
+ pages.extend(ctx.pages)
298
+ # Only real http(s) tabs; skip about:blank / devtools.
299
+ return [p for p in pages if (p.url or "").startswith("http")]
300
+
301
+ def register_init(self, title: str, reassure: str, status: str) -> None:
302
+ """Make the overlay survive navigation: every new document rebuilds it."""
303
+ script = _build_init_script(title, reassure, status)
304
+ for ctx in self._browser.contexts:
305
+ try:
306
+ ctx.add_init_script(script)
307
+ except Exception:
308
+ pass
309
+
310
+ def paint(self, title: str, reassure: str, status: str) -> int:
311
+ """Paint/refresh the overlay on every live page. Returns pages touched."""
312
+ n = 0
313
+ payload = {"title": title, "reassure": reassure, "status": status, "ts": int(time.time() * 1000)}
314
+ for p in self._pages():
315
+ try:
316
+ p.evaluate(PAINT_EXPR, payload)
317
+ n += 1
318
+ except Exception:
319
+ pass
320
+ return n
321
+
322
+ def clear(self) -> int:
323
+ n = 0
324
+ for p in self._pages():
325
+ try:
326
+ p.evaluate(CLEAR_EXPR)
327
+ n += 1
328
+ except Exception:
329
+ pass
330
+ return n
331
+
332
+
333
+ # --- cycle-log -> friendly status -------------------------------------------
334
+
335
+ def _safe_mtime(p: str) -> float:
336
+ """getmtime that tolerates the file vanishing mid-scan (log rotation race).
337
+
338
+ The watch loop runs forever while cycles rotate/delete logs underneath it.
339
+ A bare os.path.getmtime on a path that disappeared between the glob and the
340
+ stat raises FileNotFoundError and (previously) killed the whole watcher,
341
+ dropping the overlay until something restarted it. Treat a gone file as
342
+ infinitely old so it just loses the max() race instead of crashing.
343
+ """
344
+ try:
345
+ return os.path.getmtime(p)
346
+ except OSError:
347
+ return 0.0
348
+
349
+
350
+ def _latest_cycle_log() -> Path | None:
351
+ files = glob.glob(str(LOG_DIR / "twitter-cycle-*.log"))
352
+ if not files:
353
+ return None
354
+ newest = max(files, key=_safe_mtime)
355
+ # The winner could STILL have been deleted between selection and use; the
356
+ # caller (_current_status) stats it again, so hand back None if it's gone.
357
+ return Path(newest) if os.path.exists(newest) else None
358
+
359
+
360
+ _RE_SCAN = re.compile(r"project='([^']+)'\s+q=(['\"])(.*?)\2\s+kept=(\d+)")
361
+
362
+
363
+ def _prettify(line: str) -> str | None:
364
+ """Turn a raw cycle-log line into a short human status, or None to skip."""
365
+ line = line.rstrip()
366
+ if not line.strip():
367
+ return None
368
+ low = line.lower()
369
+ m = _RE_SCAN.search(line)
370
+ if m:
371
+ proj, _q, query, kept = m.group(1), None, m.group(3), m.group(4)
372
+ query = query.strip()
373
+ if len(query) > 48:
374
+ query = query[:47] + "\u2026"
375
+ kept_txt = f" \u00b7 kept {kept}" if kept != "0" else ""
376
+ return f"Scanning X \u00b7 {proj} \u00b7 \u201c{query}\u201d{kept_txt}"
377
+ # A few recognizable phase markers; otherwise show the trimmed tail.
378
+ if "posting" in low or "posted reply" in low:
379
+ return "Posting reply on X\u2026"
380
+ if "drafting" in low or "draft" in low and "cycle" not in low:
381
+ return "Drafting replies\u2026"
382
+ if "scanning" in low or "search" in low:
383
+ return line.strip()[:90]
384
+ # Generic fallback: show the most recent meaningful line, compacted.
385
+ compact = re.sub(r"\s+", " ", line.strip())
386
+ return compact[:90] if compact else None
387
+
388
+
389
+ def _tail_last_meaningful(path: Path, max_scan: int = 200) -> str | None:
390
+ try:
391
+ with path.open("rb") as f:
392
+ f.seek(0, os.SEEK_END)
393
+ size = f.tell()
394
+ chunk = min(size, 64 * 1024)
395
+ f.seek(size - chunk)
396
+ data = f.read().decode("utf-8", "replace")
397
+ except OSError:
398
+ return None
399
+ for raw in reversed(data.splitlines()[-max_scan:]):
400
+ pretty = _prettify(raw)
401
+ if pretty:
402
+ return pretty
403
+ return None
404
+
405
+
406
+ def _current_status() -> str:
407
+ log = _latest_cycle_log()
408
+ if not log:
409
+ return "Idle \u2014 waiting for the next cycle\u2026"
410
+ age = time.time() - _safe_mtime(str(log))
411
+ if age > IDLE_AFTER_SEC:
412
+ return "Idle \u2014 waiting for the next cycle\u2026"
413
+ return _tail_last_meaningful(log) or "Working\u2026"
414
+
415
+
416
+ # --- commands ---------------------------------------------------------------
417
+
418
+ def cmd_install(status: str | None = None) -> int:
419
+ status = status or _current_status()
420
+ try:
421
+ with Harness() as h:
422
+ h.register_init(TITLE, REASSURE, status)
423
+ n = h.paint(TITLE, REASSURE, status)
424
+ print(f"overlay installed on {n} page(s): {status}")
425
+ return 0
426
+ except Exception as e:
427
+ print(f"overlay install failed (harness Chrome down?): {e}", file=sys.stderr)
428
+ return 1
429
+
430
+
431
+ def cmd_status(text: str) -> int:
432
+ try:
433
+ with Harness() as h:
434
+ n = h.paint(TITLE, REASSURE, text)
435
+ print(f"status pushed to {n} page(s): {text}")
436
+ return 0
437
+ except Exception as e:
438
+ print(f"status push failed: {e}", file=sys.stderr)
439
+ return 1
440
+
441
+
442
+ def cmd_clear() -> int:
443
+ try:
444
+ with Harness() as h:
445
+ n = h.clear()
446
+ print(f"overlay cleared on {n} page(s)")
447
+ return 0
448
+ except Exception as e:
449
+ print(f"clear failed: {e}", file=sys.stderr)
450
+ return 1
451
+
452
+
453
+ def cmd_watch(interval: float = 2.0) -> int:
454
+ """Continuously stream the live cycle status into the overlay. Self-healing:
455
+ holds ONE CDP connection open across ticks (light, and friendly to the
456
+ poster's concurrent CDP session) and only reconnects when the harness Chrome
457
+ comes/goes. Never raises into the pipeline."""
458
+ # Singleton guard: there must be exactly ONE watcher painting at a time, or
459
+ # two loops fight over the same overlay (double heartbeat, flicker). Two start
460
+ # lanes can race to spawn this: the MCP's foreground KeepAlive launchd job and
461
+ # the best-effort run-overlay-watch.sh supervisor. Hold an exclusive,
462
+ # non-blocking flock for the life of the process; if another watcher already
463
+ # holds it, exit 0 quietly and let that one own the overlay. The lock fd is
464
+ # intentionally leaked (kept open) until the process dies so the OS releases
465
+ # it automatically on exit/kill.
466
+ try:
467
+ _lock_fd = os.open("/tmp/saps_overlay_watch.lock", os.O_CREAT | os.O_RDWR, 0o644)
468
+ fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
469
+ except OSError:
470
+ print("another overlay watcher already running; exiting", file=sys.stderr)
471
+ return 0
472
+ print(f"watching {LOG_DIR}/twitter-cycle-*.log -> overlay on {CDP_URL} (Ctrl-C to stop)")
473
+ # Treat SIGTERM (launchd unload, `kill`) like Ctrl-C so the overlay is
474
+ # cleared on the way out instead of lingering until the next navigation.
475
+ signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
476
+ last_status = None
477
+ h: Harness | None = None
478
+ registered = False
479
+ try:
480
+ while True:
481
+ # Never let status computation (log globbing/stat, all racing against
482
+ # live log rotation) kill the watcher; fall back to a neutral status.
483
+ try:
484
+ status = _current_status()
485
+ except Exception:
486
+ status = "Working\u2026"
487
+ try:
488
+ if h is None:
489
+ h = Harness().__enter__()
490
+ registered = False
491
+ if not registered:
492
+ # Re-register init on each (re)connect so fresh tabs inherit it.
493
+ h.register_init(TITLE, REASSURE, status)
494
+ registered = True
495
+ # Repaint every tick even when text is unchanged: the timestamp
496
+ # reset keeps the heartbeat fresh so the dot never looks dead.
497
+ if h.paint(TITLE, REASSURE, status) == 0:
498
+ # No live page (all tabs closed/navigating) -> drop & retry.
499
+ raise RuntimeError("no live page")
500
+ except Exception:
501
+ # Harness down or transient CDP hiccup; tear down and retry next tick.
502
+ if h is not None:
503
+ try:
504
+ h.__exit__(None, None, None)
505
+ except Exception:
506
+ pass
507
+ h = None
508
+ registered = False
509
+ if status != last_status:
510
+ print(f"[{time.strftime('%H:%M:%S')}] {status}")
511
+ last_status = status
512
+ time.sleep(interval)
513
+ except KeyboardInterrupt:
514
+ print("\nstopping watch; clearing overlay")
515
+ finally:
516
+ if h is not None:
517
+ try:
518
+ h.clear()
519
+ except Exception:
520
+ pass
521
+ try:
522
+ h.__exit__(None, None, None)
523
+ except Exception:
524
+ pass
525
+ else:
526
+ try:
527
+ cmd_clear()
528
+ except Exception:
529
+ pass
530
+ return 0
531
+
532
+
533
+ def main(argv: list[str]) -> int:
534
+ if not argv:
535
+ print(__doc__)
536
+ return 0
537
+ cmd = argv[0]
538
+ if cmd == "install":
539
+ return cmd_install(argv[1] if len(argv) > 1 else None)
540
+ if cmd == "status":
541
+ if len(argv) < 2:
542
+ print("usage: harness_overlay.py status \"text\"", file=sys.stderr)
543
+ return 2
544
+ return cmd_status(argv[1])
545
+ if cmd == "clear":
546
+ return cmd_clear()
547
+ if cmd == "watch":
548
+ iv = float(argv[1]) if len(argv) > 1 else 2.0
549
+ return cmd_watch(iv)
550
+ print(f"unknown command: {cmd}", file=sys.stderr)
551
+ print(__doc__)
552
+ return 2
553
+
554
+
555
+ if __name__ == "__main__":
556
+ sys.exit(main(sys.argv[1:]))