@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,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
+ }