@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.
- package/README.md +143 -0
- package/SKILL.md +342 -0
- package/bin/cli.js +980 -0
- package/bin/cookie-helper.js +315 -0
- package/bin/platform.js +59 -0
- package/bin/scheduler/index.js +12 -0
- package/bin/scheduler/launchd.js +518 -0
- package/browser-agent-configs/all-agents-mcp.json +68 -0
- package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
- package/browser-agent-configs/linkedin-agent.json +17 -0
- package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
- package/browser-agent-configs/reddit-agent-mcp.json +16 -0
- package/browser-agent-configs/reddit-agent.json +17 -0
- package/browser-agent-configs/twitter-harness-mcp.json +18 -0
- package/config.example.json +45 -0
- package/mcp/dist/index.js +4212 -0
- package/mcp/dist/onboarding.js +200 -0
- package/mcp/dist/panel.html +176 -0
- package/mcp/dist/product-link.html +102 -0
- package/mcp/dist/repo.js +222 -0
- package/mcp/dist/runtime.js +1079 -0
- package/mcp/dist/screencast.js +323 -0
- package/mcp/dist/setup.js +545 -0
- package/mcp/dist/telemetry.js +306 -0
- package/mcp/dist/twitterAuth.js +138 -0
- package/mcp/dist/version.js +271 -0
- package/mcp/dist/version.json +4 -0
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/install.mjs +169 -0
- package/mcp/manifest.json +80 -0
- package/mcp/menubar/dashboard_server.py +213 -0
- package/mcp/menubar/s4l_card.py +1336 -0
- package/mcp/menubar/s4l_log_relay.py +179 -0
- package/mcp/menubar/s4l_menubar.py +2439 -0
- package/mcp/menubar/s4l_state.py +891 -0
- package/mcp/package.json +34 -0
- package/mcp/shared/doctor.cjs +437 -0
- package/mcp/shared/onboarding-ledger.cjs +324 -0
- package/mcp-servers/browser-harness/server.py +968 -0
- package/package.json +160 -0
- package/requirements.txt +20 -0
- package/scripts/_compute_allowlist.py +58 -0
- package/scripts/_db_update.py +20 -0
- package/scripts/_filt.py +9 -0
- package/scripts/_li_notif_match.py +76 -0
- package/scripts/_li_notif_orchestrate.py +126 -0
- package/scripts/_lock_preempt_test.py +60 -0
- package/scripts/_run_icp_precheck.py +57 -0
- package/scripts/a16z_pearx_calendar_reminders.py +99 -0
- package/scripts/account_resolver.py +141 -0
- package/scripts/active_campaigns.py +114 -0
- package/scripts/active_users.py +190 -0
- package/scripts/amplitude_24h_signups.py +468 -0
- package/scripts/amplitude_signups.py +177 -0
- package/scripts/apply_onboarding_selections.py +131 -0
- package/scripts/audience_pages.py +243 -0
- package/scripts/audit_helper.py +120 -0
- package/scripts/author_history_block.py +353 -0
- package/scripts/autopilot_stall_watch.py +284 -0
- package/scripts/backfill_twitter_attempts_topic.py +81 -0
- package/scripts/backfill_twitter_log_post_no_id.py +322 -0
- package/scripts/bench_dashboard.sh +138 -0
- package/scripts/bh_send.py +39 -0
- package/scripts/build_persona.py +409 -0
- package/scripts/bulk_icp.py +18 -0
- package/scripts/campaign_bump.py +51 -0
- package/scripts/capture_thread_media.py +288 -0
- package/scripts/check_browser_lock_health.sh +81 -0
- package/scripts/check_external_pool_depth.py +253 -0
- package/scripts/check_unread_web_chats.py +28 -0
- package/scripts/claim_web_chat.py +47 -0
- package/scripts/classify_run_error.py +158 -0
- package/scripts/claude_job.py +988 -0
- package/scripts/clean_stale_singleton.sh +56 -0
- package/scripts/cleanup_harness_tabs.py +68 -0
- package/scripts/copy_browser_cookies.py +454 -0
- package/scripts/counterparty_history.py +350 -0
- package/scripts/db.py +57 -0
- package/scripts/discover_claude_profiles.py +120 -0
- package/scripts/discover_linkedin_candidates.py +984 -0
- package/scripts/dm_conversation.py +682 -0
- package/scripts/dm_db_update.py +69 -0
- package/scripts/dm_engage_helper.py +161 -0
- package/scripts/dm_outreach_helper.py +147 -0
- package/scripts/dm_outreach_twitter_helper.py +129 -0
- package/scripts/dm_send_log.py +106 -0
- package/scripts/dm_short_links.py +1084 -0
- package/scripts/dump_web_chat_history.py +47 -0
- package/scripts/engage_github.py +640 -0
- package/scripts/engage_reddit.py +1235 -0
- package/scripts/engage_twitter_helper.py +301 -0
- package/scripts/engagement_styles.py +1787 -0
- package/scripts/enrich_twitter_candidates.py +82 -0
- package/scripts/feedback_digest.py +448 -0
- package/scripts/fetch_prospect_profile.py +312 -0
- package/scripts/fetch_twitter_t1.py +134 -0
- package/scripts/find_threads.py +530 -0
- package/scripts/follow_gate_log.py +59 -0
- package/scripts/funnel_per_day.py +194 -0
- package/scripts/generate_daily_human_style.py +494 -0
- package/scripts/generation_trace.py +173 -0
- package/scripts/get_run_cost.py +107 -0
- package/scripts/github_engage_helper.py +93 -0
- package/scripts/github_tools.py +509 -0
- package/scripts/harness_overlay.py +556 -0
- package/scripts/harvest_twitter_following.py +243 -0
- package/scripts/heartbeat.sh +70 -0
- package/scripts/history_context.py +284 -0
- package/scripts/http_api.py +206 -0
- package/scripts/human_dm_replies_helper.py +169 -0
- package/scripts/identity.py +302 -0
- package/scripts/ig_batch_creator.sh +93 -0
- package/scripts/ig_post_type_picker.py +243 -0
- package/scripts/ig_scrape_transcribe.sh +91 -0
- package/scripts/ingest_human_dm_replies.py +271 -0
- package/scripts/ingest_web_chat_replies.py +229 -0
- package/scripts/install_fleet.py +187 -0
- package/scripts/invent_mcp_server.py +350 -0
- package/scripts/invent_topics.py +1462 -0
- package/scripts/learned_preferences.py +263 -0
- package/scripts/li_discovery.py +161 -0
- package/scripts/link_edit_helper.py +142 -0
- package/scripts/link_tail.py +592 -0
- package/scripts/linkedin_api.py +561 -0
- package/scripts/linkedin_browser.py +730 -0
- package/scripts/linkedin_cooldown.py +128 -0
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/linkedin_killswitch.py +1333 -0
- package/scripts/linkedin_search_topic_schema.py +49 -0
- package/scripts/linkedin_unipile.py +658 -0
- package/scripts/linkedin_url.py +228 -0
- package/scripts/log_claude_session.py +636 -0
- package/scripts/log_draft.py +143 -0
- package/scripts/log_linkedin_search_attempts.py +126 -0
- package/scripts/log_post.py +651 -0
- package/scripts/log_run.py +364 -0
- package/scripts/log_thread_media.py +108 -0
- package/scripts/log_twitter_search_attempts.py +150 -0
- package/scripts/log_twitter_skips.py +211 -0
- package/scripts/lookup_post.py +78 -0
- package/scripts/mark_web_chat_processed.py +32 -0
- package/scripts/mcp_lock_proxy.py +370 -0
- package/scripts/memory_snapshot.py +972 -0
- package/scripts/merge_review_queue.py +215 -0
- package/scripts/mint_external_pool.py +182 -0
- package/scripts/mint_kent_pool.py +249 -0
- package/scripts/moltbook_post.py +320 -0
- package/scripts/moltbook_tools.py +159 -0
- package/scripts/pending_threads.py +188 -0
- package/scripts/pick_ig_account.py +177 -0
- package/scripts/pick_project.py +208 -0
- package/scripts/pick_search_topic.py +771 -0
- package/scripts/pick_thread_target.py +279 -0
- package/scripts/pick_twitter_thread_target.py +202 -0
- package/scripts/podlog_fetch_batch.sh +32 -0
- package/scripts/post_github.py +1311 -0
- package/scripts/post_reddit.py +2668 -0
- package/scripts/precompute_dashboard_stats.py +204 -0
- package/scripts/preflight.sh +297 -0
- package/scripts/progress.py +88 -0
- package/scripts/project_excludes.py +353 -0
- package/scripts/project_slugs.py +91 -0
- package/scripts/project_stats.py +241 -0
- package/scripts/project_stats_json.py +1563 -0
- package/scripts/project_topics.py +192 -0
- package/scripts/qualified_query_bank.py +436 -0
- package/scripts/reap_stale_claude_sessions.py +867 -0
- package/scripts/reddit_browser.py +2549 -0
- package/scripts/reddit_browser_fetch.py +141 -0
- package/scripts/reddit_browser_lock.py +593 -0
- package/scripts/reddit_chat_sync.py +710 -0
- package/scripts/reddit_query_bank.py +200 -0
- package/scripts/reddit_threads_helper.py +151 -0
- package/scripts/reddit_tools.py +956 -0
- package/scripts/refresh_instagram_tokens.py +280 -0
- package/scripts/release-mcpb.sh +513 -0
- package/scripts/reply_db.py +334 -0
- package/scripts/reply_insert.py +98 -0
- package/scripts/reply_risk_digest.py +761 -0
- package/scripts/reset-test-machine.sh +602 -0
- package/scripts/restore_twitter_session.py +177 -0
- package/scripts/ripen_reddit_plan.py +478 -0
- package/scripts/run_claude.sh +433 -0
- package/scripts/run_moltbook_cycle.py +555 -0
- package/scripts/s4l_box_update.sh +226 -0
- package/scripts/s4l_channel.py +103 -0
- package/scripts/s4l_ctl.sh +75 -0
- package/scripts/s4l_env.py +47 -0
- package/scripts/saps_activity.py +126 -0
- package/scripts/saps_mode.py +328 -0
- package/scripts/scan_dm_candidates.py +580 -0
- package/scripts/scan_github_replies.py +168 -0
- package/scripts/scan_instagram_comments.py +481 -0
- package/scripts/scan_moltbook_replies.py +252 -0
- package/scripts/scan_pii.py +190 -0
- package/scripts/scan_reddit_replies.py +377 -0
- package/scripts/scan_twitter_mentions_browser.py +327 -0
- package/scripts/scan_twitter_thread_followups.py +299 -0
- package/scripts/scan_x_profile.py +384 -0
- package/scripts/schedule_state.py +202 -0
- package/scripts/scheduled_tasks_snapshot.py +123 -0
- package/scripts/score_linkedin_candidates.py +419 -0
- package/scripts/score_twitter_candidates.py +718 -0
- package/scripts/scrape_linkedin_comment_stats.py +1755 -0
- package/scripts/scrape_linkedin_stats_browser.py +52 -0
- package/scripts/scrape_reddit_views.py +365 -0
- package/scripts/seed_search_queries.py +453 -0
- package/scripts/seed_search_topics.py +127 -0
- package/scripts/send_web_chat_reply.py +130 -0
- package/scripts/sentry_init.py +128 -0
- package/scripts/setup_twitter_auth.py +1320 -0
- package/scripts/snapshot.py +583 -0
- package/scripts/stats.py +2702 -0
- package/scripts/stats_helper.py +52 -0
- package/scripts/strike_alert.py +783 -0
- package/scripts/sweep_post_link_clicks.py +107 -0
- package/scripts/sync_ig_to_posts.py +147 -0
- package/scripts/test_browser_lock.py +189 -0
- package/scripts/test_installation_api.sh +52 -0
- package/scripts/test_percard_posting.py +142 -0
- package/scripts/top_dud_linkedin_queries.py +71 -0
- package/scripts/top_dud_reddit_queries.py +67 -0
- package/scripts/top_dud_twitter_queries.py +71 -0
- package/scripts/top_dud_twitter_topics.py +102 -0
- package/scripts/top_linkedin_queries.py +55 -0
- package/scripts/top_omitted_reddit_topics.py +91 -0
- package/scripts/top_performers.py +588 -0
- package/scripts/top_search_topics.py +180 -0
- package/scripts/top_twitter_queries.py +190 -0
- package/scripts/twitter_access_check.py +382 -0
- package/scripts/twitter_account.py +41 -0
- package/scripts/twitter_batch_phase.py +126 -0
- package/scripts/twitter_browser.py +2804 -0
- package/scripts/twitter_cookie_mirror.py +130 -0
- package/scripts/twitter_cycle_helper.py +310 -0
- package/scripts/twitter_gen_links.py +287 -0
- package/scripts/twitter_post_plan.py +1188 -0
- package/scripts/twitter_scan.py +324 -0
- package/scripts/twitter_supply_signal.py +57 -0
- package/scripts/twitter_threads_helper.py +152 -0
- package/scripts/unclaim_web_chat.py +29 -0
- package/scripts/update_instagram_stats.py +261 -0
- package/scripts/update_linkedin_stats_from_feed.py +328 -0
- package/scripts/version.py +72 -0
- package/scripts/watchdog_hung_runs.py +343 -0
- package/scripts/write_generation_trace.py +73 -0
- package/setup/SKILL.md +277 -0
- package/skill/amplitude-24h-signups.sh +38 -0
- package/skill/archive-old-logs.sh +40 -0
- package/skill/audit-dm-staleness.sh +42 -0
- package/skill/audit-linkedin.sh +14 -0
- package/skill/audit-moltbook.sh +4 -0
- package/skill/audit-reddit-resurrect.sh +67 -0
- package/skill/audit-reddit.sh +4 -0
- package/skill/audit-twitter.sh +4 -0
- package/skill/audit.sh +287 -0
- package/skill/backfill-twitter-attempts-topic.sh +19 -0
- package/skill/backfill-twitter-ghost-posts.sh +24 -0
- package/skill/check-external-pool-depth.sh +7 -0
- package/skill/check-web-chats.sh +203 -0
- package/skill/dm-outreach-linkedin.sh +250 -0
- package/skill/dm-outreach-reddit.sh +274 -0
- package/skill/dm-outreach-twitter.sh +265 -0
- package/skill/engage-dm-replies-linkedin.sh +4 -0
- package/skill/engage-dm-replies-reddit.sh +4 -0
- package/skill/engage-dm-replies-twitter.sh +4 -0
- package/skill/engage-dm-replies.sh +1597 -0
- package/skill/engage-linkedin.sh +581 -0
- package/skill/engage-moltbook.sh +36 -0
- package/skill/engage-reddit.sh +146 -0
- package/skill/engage-twitter.sh +467 -0
- package/skill/github-engage.sh +176 -0
- package/skill/ingest-web-chat-replies.sh +38 -0
- package/skill/invent-supply-test.sh +100 -0
- package/skill/invent-topics.sh +50 -0
- package/skill/lib/linkedin-backend.sh +364 -0
- package/skill/lib/platform.sh +48 -0
- package/skill/lib/reddit-backend.sh +234 -0
- package/skill/lib/twitter-backend.sh +314 -0
- package/skill/link-edit-github.sh +136 -0
- package/skill/link-edit-moltbook.sh +117 -0
- package/skill/link-edit-reddit.sh +201 -0
- package/skill/linkedin-presence.sh +182 -0
- package/skill/linkedin-recovery.sh +282 -0
- package/skill/lock.sh +647 -0
- package/skill/memory-snapshot.sh +39 -0
- package/skill/precompute-stats.sh +35 -0
- package/skill/prewarm-funnel.sh +104 -0
- package/skill/refresh-instagram-tokens.sh +57 -0
- package/skill/refresh-twitter-following.sh +52 -0
- package/skill/reply-risk-digest.sh +31 -0
- package/skill/run-cycle-update-guard.sh +44 -0
- package/skill/run-draft-and-publish.sh +123 -0
- package/skill/run-generate-daily-style.sh +50 -0
- package/skill/run-github-launchd.sh +62 -0
- package/skill/run-github.sh +102 -0
- package/skill/run-instagram-daily.sh +149 -0
- package/skill/run-instagram-render.sh +875 -0
- package/skill/run-linkedin-launchd.sh +81 -0
- package/skill/run-linkedin-unipile.sh +130 -0
- package/skill/run-linkedin.sh +1593 -0
- package/skill/run-moltbook-launchd.sh +61 -0
- package/skill/run-moltbook.sh +38 -0
- package/skill/run-overlay-watch.sh +100 -0
- package/skill/run-reddit-search-launchd.sh +64 -0
- package/skill/run-reddit-search.sh +505 -0
- package/skill/run-reddit-threads-double.sh +32 -0
- package/skill/run-reddit-threads.sh +847 -0
- package/skill/run-scan-moltbook-replies.sh +57 -0
- package/skill/run-twitter-cycle-launchd.sh +63 -0
- package/skill/run-twitter-cycle-singleton.sh +62 -0
- package/skill/run-twitter-cycle.sh +2408 -0
- package/skill/run-twitter-threads.sh +592 -0
- package/skill/scan-instagram-replies.sh +61 -0
- package/skill/scan-twitter-followups.sh +57 -0
- package/skill/social-autoposter-update.sh +66 -0
- package/skill/stats-instagram.sh +72 -0
- package/skill/stats-linkedin.sh +271 -0
- package/skill/stats-moltbook.sh +4 -0
- package/skill/stats-reddit.sh +4 -0
- package/skill/stats-twitter.sh +4 -0
- package/skill/stats.sh +521 -0
- package/skill/strike-alert.sh +18 -0
- package/skill/styles.sh +87 -0
- package/skill/sweep-link-clicks.sh +40 -0
- package/skill/topics.sh +51 -0
|
@@ -0,0 +1,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:]))
|