@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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live browser preview via CDP screencast.
|
|
3
|
+
*
|
|
4
|
+
* Attaches to a running managed Chrome over the Chrome DevTools Protocol (on the
|
|
5
|
+
* harness's remote-debugging port) and runs `Page.startScreencast`, buffering the
|
|
6
|
+
* most recent JPEG frame. The `show_browser_to_user` tool hands that frame to the
|
|
7
|
+
* panel, which polls for fresh frames and paints them — a low-latency "watch what
|
|
8
|
+
* the bot is doing" view.
|
|
9
|
+
*
|
|
10
|
+
* The frames travel back to the panel through the NORMAL MCP tool-result channel
|
|
11
|
+
* as a `data:` URL, which the default panel CSP already permits. So this needs no
|
|
12
|
+
* CSP widening, no localhost network access from the iframe, and no extra
|
|
13
|
+
* dependency: it uses Node's built-in global `WebSocket` (Node >= 21) and `fetch`.
|
|
14
|
+
*
|
|
15
|
+
* A future high-FPS upgrade (panel opens a `ws://` straight to a local relay) is a
|
|
16
|
+
* separate step gated on the host honoring a `connectDomains` localhost entry;
|
|
17
|
+
* this module is the robust baseline that works regardless.
|
|
18
|
+
*/
|
|
19
|
+
import { execFile } from "node:child_process";
|
|
20
|
+
// Untyped indirection: Node ships a global WebSocket at runtime (>=21) but
|
|
21
|
+
// @types/node doesn't always declare it as a value, and MessageEvent isn't typed
|
|
22
|
+
// without the DOM lib. Reach for it dynamically and keep the event handlers `any`.
|
|
23
|
+
const WS = globalThis.WebSocket;
|
|
24
|
+
// Ports we manage a Chrome on, most-likely-active first. TWITTER_CDP_URL (the
|
|
25
|
+
// twitter harness) wins if set; the rest cover linkedin (9556) / reddit (9557) /
|
|
26
|
+
// browser-harness / assrt.
|
|
27
|
+
function candidatePorts() {
|
|
28
|
+
const ports = [];
|
|
29
|
+
const env = process.env.TWITTER_CDP_URL || "";
|
|
30
|
+
const m = env.match(/:(\d+)/);
|
|
31
|
+
if (m)
|
|
32
|
+
ports.push(Number(m[1]));
|
|
33
|
+
for (const p of [9555, 9556, 9557, 9222, 9223, 9755]) {
|
|
34
|
+
if (!ports.includes(p))
|
|
35
|
+
ports.push(p);
|
|
36
|
+
}
|
|
37
|
+
return ports;
|
|
38
|
+
}
|
|
39
|
+
async function fetchJson(url, timeoutMs = 1500) {
|
|
40
|
+
try {
|
|
41
|
+
const ctrl = new AbortController();
|
|
42
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
43
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
44
|
+
clearTimeout(t);
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
return null;
|
|
47
|
+
return await res.json();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Pick a real, visible page target on a port (skip devtools:// and extension
|
|
54
|
+
// targets, which have no useful screencast).
|
|
55
|
+
async function findPageTarget(port) {
|
|
56
|
+
const list = await fetchJson(`http://127.0.0.1:${port}/json`);
|
|
57
|
+
if (!Array.isArray(list))
|
|
58
|
+
return null;
|
|
59
|
+
const pages = list.filter((t) => t &&
|
|
60
|
+
t.type === "page" &&
|
|
61
|
+
typeof t.webSocketDebuggerUrl === "string" &&
|
|
62
|
+
!String(t.url || "").startsWith("devtools://"));
|
|
63
|
+
if (!pages.length)
|
|
64
|
+
return null;
|
|
65
|
+
// Prefer an actual http(s) page over about:blank / chrome:// scaffolding.
|
|
66
|
+
return pages.find((t) => /^https?:/i.test(t.url)) || pages[0];
|
|
67
|
+
}
|
|
68
|
+
export async function findActivePort() {
|
|
69
|
+
for (const port of candidatePorts()) {
|
|
70
|
+
const target = await findPageTarget(port);
|
|
71
|
+
if (target)
|
|
72
|
+
return { port, target };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
class Screencast {
|
|
77
|
+
ws = null;
|
|
78
|
+
msgId = 1;
|
|
79
|
+
latest = null; // base64 JPEG, no data: prefix
|
|
80
|
+
lastFrameAt = 0;
|
|
81
|
+
connecting = false;
|
|
82
|
+
port = 0;
|
|
83
|
+
targetTitle = "";
|
|
84
|
+
targetUrl = "";
|
|
85
|
+
get running() {
|
|
86
|
+
return !!this.ws && this.ws.readyState === 1 /* OPEN */;
|
|
87
|
+
}
|
|
88
|
+
// Ensure a screencast is running. Reuses an existing connection; otherwise
|
|
89
|
+
// resolves a target (explicit port, else auto-detect) and connects.
|
|
90
|
+
async ensure(port) {
|
|
91
|
+
if (this.running || this.connecting)
|
|
92
|
+
return { ok: true };
|
|
93
|
+
if (!WS)
|
|
94
|
+
return { ok: false, error: "no_websocket" };
|
|
95
|
+
this.connecting = true;
|
|
96
|
+
try {
|
|
97
|
+
let chosenPort = port;
|
|
98
|
+
let target = null;
|
|
99
|
+
if (chosenPort)
|
|
100
|
+
target = await findPageTarget(chosenPort);
|
|
101
|
+
if (!target) {
|
|
102
|
+
const found = await findActivePort();
|
|
103
|
+
if (!found)
|
|
104
|
+
return { ok: false, error: "no_browser" };
|
|
105
|
+
chosenPort = found.port;
|
|
106
|
+
target = found.target;
|
|
107
|
+
}
|
|
108
|
+
await this.connect(target.webSocketDebuggerUrl);
|
|
109
|
+
this.port = chosenPort;
|
|
110
|
+
this.targetTitle = target.title || "";
|
|
111
|
+
this.targetUrl = target.url || "";
|
|
112
|
+
return { ok: true };
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return { ok: false, error: String(e?.message || e) };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.connecting = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
connect(wsUrl) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
let ws;
|
|
124
|
+
try {
|
|
125
|
+
ws = new WS(wsUrl);
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
reject(e);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let opened = false;
|
|
132
|
+
const to = setTimeout(() => {
|
|
133
|
+
if (!opened) {
|
|
134
|
+
try {
|
|
135
|
+
ws.close();
|
|
136
|
+
}
|
|
137
|
+
catch { /* ignore */ }
|
|
138
|
+
reject(new Error("cdp_ws_timeout"));
|
|
139
|
+
}
|
|
140
|
+
}, 5000);
|
|
141
|
+
ws.onopen = () => {
|
|
142
|
+
opened = true;
|
|
143
|
+
clearTimeout(to);
|
|
144
|
+
this.ws = ws;
|
|
145
|
+
this.send("Page.enable");
|
|
146
|
+
// Activate this tab first. Chrome only streams screencast frames for a
|
|
147
|
+
// page whose RenderWidget is visible; a background tab (or one behind
|
|
148
|
+
// another window) emits zero frames, which strands the panel on
|
|
149
|
+
// "Connecting…". bringToFront makes the attached tab the foreground tab
|
|
150
|
+
// so startScreencast has something to render.
|
|
151
|
+
this.send("Page.bringToFront");
|
|
152
|
+
this.send("Page.startScreencast", {
|
|
153
|
+
format: "jpeg",
|
|
154
|
+
quality: 55,
|
|
155
|
+
maxWidth: 1280,
|
|
156
|
+
maxHeight: 800,
|
|
157
|
+
everyNthFrame: 1,
|
|
158
|
+
});
|
|
159
|
+
resolve();
|
|
160
|
+
};
|
|
161
|
+
ws.onmessage = (ev) => this.onMessage(ev);
|
|
162
|
+
ws.onerror = () => { };
|
|
163
|
+
ws.onclose = () => {
|
|
164
|
+
clearTimeout(to);
|
|
165
|
+
if (this.ws === ws) {
|
|
166
|
+
this.ws = null;
|
|
167
|
+
this.latest = null;
|
|
168
|
+
}
|
|
169
|
+
if (!opened)
|
|
170
|
+
reject(new Error("cdp_ws_closed"));
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
send(method, params) {
|
|
175
|
+
if (!this.ws)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
this.ws.send(JSON.stringify({ id: this.msgId++, method, params: params || {} }));
|
|
179
|
+
}
|
|
180
|
+
catch { /* ignore */ }
|
|
181
|
+
}
|
|
182
|
+
onMessage(ev) {
|
|
183
|
+
let msg;
|
|
184
|
+
try {
|
|
185
|
+
const raw = typeof ev?.data === "string" ? ev.data : String(ev?.data ?? "");
|
|
186
|
+
msg = JSON.parse(raw);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (msg && msg.method === "Page.screencastFrame") {
|
|
192
|
+
const data = msg.params?.data;
|
|
193
|
+
const sid = msg.params?.sessionId;
|
|
194
|
+
if (typeof data === "string") {
|
|
195
|
+
this.latest = data;
|
|
196
|
+
this.lastFrameAt = Date.now();
|
|
197
|
+
}
|
|
198
|
+
// Must ack every frame or Chrome stops sending them.
|
|
199
|
+
if (sid != null)
|
|
200
|
+
this.send("Page.screencastFrameAck", { sessionId: sid });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
stop() {
|
|
204
|
+
if (this.ws) {
|
|
205
|
+
this.send("Page.stopScreencast");
|
|
206
|
+
try {
|
|
207
|
+
this.ws.close();
|
|
208
|
+
}
|
|
209
|
+
catch { /* ignore */ }
|
|
210
|
+
}
|
|
211
|
+
this.ws = null;
|
|
212
|
+
this.latest = null;
|
|
213
|
+
}
|
|
214
|
+
frame() {
|
|
215
|
+
return this.latest;
|
|
216
|
+
}
|
|
217
|
+
status() {
|
|
218
|
+
return {
|
|
219
|
+
running: this.running,
|
|
220
|
+
port: this.port || null,
|
|
221
|
+
title: this.targetTitle,
|
|
222
|
+
url: this.targetUrl,
|
|
223
|
+
age_ms: this.lastFrameAt ? Date.now() - this.lastFrameAt : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export const screencast = new Screencast();
|
|
228
|
+
// ---- bring browser to front ------------------------------------------------
|
|
229
|
+
// Raise the managed Chrome above other apps so the user can interact with it
|
|
230
|
+
// directly. Two steps: (1) CDP Page.bringToFront raises the active TAB inside
|
|
231
|
+
// the browser; (2) on macOS, raise the browser's OS WINDOW above Claude Desktop
|
|
232
|
+
// by activating the process that owns the CDP port. Without (2), the tab would
|
|
233
|
+
// be focused but the window could still sit behind the panel.
|
|
234
|
+
// Fire a single CDP command on a target's debugger websocket and resolve once it
|
|
235
|
+
// acknowledges (or times out). Used for one-shot commands like Page.bringToFront
|
|
236
|
+
// where we don't need a persistent connection.
|
|
237
|
+
function cdpCommand(wsUrl, method, params, timeoutMs = 3000) {
|
|
238
|
+
return new Promise((resolve) => {
|
|
239
|
+
if (!WS) {
|
|
240
|
+
resolve(false);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
let ws;
|
|
244
|
+
try {
|
|
245
|
+
ws = new WS(wsUrl);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
resolve(false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let done = false;
|
|
252
|
+
const finish = (ok) => {
|
|
253
|
+
if (done)
|
|
254
|
+
return;
|
|
255
|
+
done = true;
|
|
256
|
+
try {
|
|
257
|
+
ws.close();
|
|
258
|
+
}
|
|
259
|
+
catch { /* ignore */ }
|
|
260
|
+
resolve(ok);
|
|
261
|
+
};
|
|
262
|
+
const to = setTimeout(() => finish(false), timeoutMs);
|
|
263
|
+
ws.onopen = () => {
|
|
264
|
+
try {
|
|
265
|
+
ws.send(JSON.stringify({ id: 1, method, params: params || {} }));
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
clearTimeout(to);
|
|
269
|
+
finish(false);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
ws.onmessage = (ev) => {
|
|
273
|
+
try {
|
|
274
|
+
const raw = typeof ev?.data === "string" ? ev.data : String(ev?.data ?? "");
|
|
275
|
+
const msg = JSON.parse(raw);
|
|
276
|
+
if (msg && msg.id === 1) {
|
|
277
|
+
clearTimeout(to);
|
|
278
|
+
finish(!msg.error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { /* ignore */ }
|
|
282
|
+
};
|
|
283
|
+
ws.onerror = () => { clearTimeout(to); finish(false); };
|
|
284
|
+
ws.onclose = () => { clearTimeout(to); finish(done); };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// macOS only: activate the GUI process that owns the CDP port so its window
|
|
288
|
+
// rises above everything else. We find the PID via the listener on the port
|
|
289
|
+
// (not the established CDP client connections) and activate it by unix id, which
|
|
290
|
+
// works regardless of the app's display name (Chrome vs Chromium vs harness).
|
|
291
|
+
function raiseMacWindow(port) {
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
execFile("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"], { timeout: 2500 }, (_err, stdout) => {
|
|
294
|
+
const pid = String(stdout || "").split(/\s+/).filter(Boolean)[0];
|
|
295
|
+
if (!pid) {
|
|
296
|
+
resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const osa = `tell application "System Events" to set frontmost of (first process whose unix id is ${pid}) to true`;
|
|
300
|
+
// execFile (no shell) passes the AppleScript as one argv, so its inner
|
|
301
|
+
// double quotes need no escaping.
|
|
302
|
+
execFile("osascript", ["-e", osa], { timeout: 2500 }, () => resolve());
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
export async function bringBrowserToFront(port) {
|
|
307
|
+
let chosenPort = port || screencast.port || 0;
|
|
308
|
+
let target = chosenPort ? await findPageTarget(chosenPort) : null;
|
|
309
|
+
if (!target) {
|
|
310
|
+
const found = await findActivePort();
|
|
311
|
+
if (!found)
|
|
312
|
+
return { ok: false, error: "no_browser" };
|
|
313
|
+
chosenPort = found.port;
|
|
314
|
+
target = found.target;
|
|
315
|
+
}
|
|
316
|
+
if (target.webSocketDebuggerUrl) {
|
|
317
|
+
await cdpCommand(target.webSocketDebuggerUrl, "Page.bringToFront");
|
|
318
|
+
}
|
|
319
|
+
if (process.platform === "darwin") {
|
|
320
|
+
await raiseMacWindow(chosenPort);
|
|
321
|
+
}
|
|
322
|
+
return { ok: true, port: chosenPort };
|
|
323
|
+
}
|