@m13v/s4l 1.7.4-rc.38 → 1.7.4-rc.39
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/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/stall_guard.py +70 -0
- package/scripts/twitter_browser.py +13 -0
- package/scripts/twitter_scan.py +11 -36
- package/skill/lib/browser-launch.sh +54 -0
- package/skill/lib/linkedin-backend.sh +14 -45
- package/skill/lib/reddit-backend.sh +9 -3
- package/skill/lib/twitter-backend.sh +14 -57
package/mcp/dist/version.json
CHANGED
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.4-rc.
|
|
5
|
+
"version": "1.7.4-rc.39",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.4-rc.
|
|
3
|
+
"version": "1.7.4-rc.39",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared zero-progress deadline for browser-touching pipeline steps.
|
|
3
|
+
|
|
4
|
+
Any step that relays CDP to the harness Chrome (scan queries, thread media
|
|
5
|
+
capture, ...) can hang FOREVER against a half-wedged browser or a dead
|
|
6
|
+
renderer: the call never returns, the step holds the browser lock, and the
|
|
7
|
+
cycle-entry health checks that could heal Chrome can't run because the next
|
|
8
|
+
cycle can't start. 2026-07-13: a scan hung 23 min at zero queries; the same
|
|
9
|
+
night media capture hung 34+ min at candidate 24/92 and only the watchdog's
|
|
10
|
+
90-min lock-liveness net was ever going to reap it.
|
|
11
|
+
|
|
12
|
+
One implementation, used by every step (twitter_scan.py Phase-1 queries,
|
|
13
|
+
twitter_browser.py media capture). Usage:
|
|
14
|
+
|
|
15
|
+
from stall_guard import stall_guard
|
|
16
|
+
with stall_guard("thread_media", url): # default deadline 240s
|
|
17
|
+
<one bounded unit of browser work>
|
|
18
|
+
|
|
19
|
+
If the guarded block does not finish inside the deadline the guard:
|
|
20
|
+
1. prints a loud machine-greppable marker to stderr
|
|
21
|
+
([stall_guard] STALL_ABORT step=<step> ...)
|
|
22
|
+
2. stamps the port's wedge strike file, so the next cycle's two-strike
|
|
23
|
+
gate in skill/lib/twitter-backend.sh converges to a kill+relaunch if
|
|
24
|
+
Chrome is genuinely sick
|
|
25
|
+
3. hard-exits the process with EX_TEMPFAIL (75) so the cycle fails fast
|
|
26
|
+
and releases the browser lock + cycle slot.
|
|
27
|
+
|
|
28
|
+
A completed block cancels its timer; a healthy pipeline never notices the
|
|
29
|
+
guard exists. Deadline override: S4L_BROWSER_OP_DEADLINE_S (seconds).
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import contextlib
|
|
34
|
+
import os
|
|
35
|
+
import pathlib
|
|
36
|
+
import sys
|
|
37
|
+
import threading
|
|
38
|
+
|
|
39
|
+
DEFAULT_DEADLINE_S = float(os.environ.get("S4L_BROWSER_OP_DEADLINE_S", "240"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _strike_file(port: int) -> str:
|
|
43
|
+
# Must match the two-strike gate in skill/lib/twitter-backend.sh.
|
|
44
|
+
return f"/tmp/s4l_cdp_wedge_strike_{port}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextlib.contextmanager
|
|
48
|
+
def stall_guard(step: str, detail: str = "", deadline_s: float | None = None, port: int = 9555):
|
|
49
|
+
limit = DEFAULT_DEADLINE_S if deadline_s is None else float(deadline_s)
|
|
50
|
+
|
|
51
|
+
def _fire():
|
|
52
|
+
sys.stderr.write(
|
|
53
|
+
f"[stall_guard] STALL_ABORT step={step} detail={detail!r} "
|
|
54
|
+
f"deadline={limit:.0f}s — browser call hung (wedged Chrome or dead "
|
|
55
|
+
"renderer); stamping wedge strike and aborting so the lock frees\n"
|
|
56
|
+
)
|
|
57
|
+
sys.stderr.flush()
|
|
58
|
+
try:
|
|
59
|
+
pathlib.Path(_strike_file(port)).touch()
|
|
60
|
+
except OSError:
|
|
61
|
+
pass
|
|
62
|
+
os._exit(75) # EX_TEMPFAIL
|
|
63
|
+
|
|
64
|
+
t = threading.Timer(limit, _fire)
|
|
65
|
+
t.daemon = True
|
|
66
|
+
t.start()
|
|
67
|
+
try:
|
|
68
|
+
yield
|
|
69
|
+
finally:
|
|
70
|
+
t.cancel()
|
|
@@ -2732,6 +2732,19 @@ def _anchor_repost_from_tweets(tweets, anchor_tweet_id):
|
|
|
2732
2732
|
|
|
2733
2733
|
|
|
2734
2734
|
def scrape_thread_media(thread_url, scroll_count=1):
|
|
2735
|
+
"""Deadline wrapper: media capture relays CDP to the harness Chrome, and
|
|
2736
|
+
against a half-wedged browser or dead renderer a single evaluate can hang
|
|
2737
|
+
FOREVER (2026-07-13: hung 34+ min at candidate 24/92 holding the browser
|
|
2738
|
+
lock; only the watchdog's 90-min lock-liveness net would have reaped it).
|
|
2739
|
+
stall_guard aborts the process fast, stamps the wedge strike, and lets the
|
|
2740
|
+
cycle-entry heal chain converge. Shared implementation — see
|
|
2741
|
+
scripts/stall_guard.py; do NOT re-implement per step."""
|
|
2742
|
+
from stall_guard import stall_guard
|
|
2743
|
+
with stall_guard("thread_media", thread_url):
|
|
2744
|
+
return _scrape_thread_media_inner(thread_url, scroll_count)
|
|
2745
|
+
|
|
2746
|
+
|
|
2747
|
+
def _scrape_thread_media_inner(thread_url, scroll_count=1):
|
|
2735
2748
|
"""Navigate to a tweet's permalink and return the media of the anchor tweet.
|
|
2736
2749
|
|
|
2737
2750
|
Deterministic, model-free media capture for the MAIN posting cycle: the
|
package/scripts/twitter_scan.py
CHANGED
|
@@ -56,7 +56,6 @@ import os
|
|
|
56
56
|
import pathlib
|
|
57
57
|
import re
|
|
58
58
|
import sys
|
|
59
|
-
import threading
|
|
60
59
|
import time
|
|
61
60
|
import urllib.parse
|
|
62
61
|
|
|
@@ -247,38 +246,13 @@ def _navigate(url: str) -> None:
|
|
|
247
246
|
new_tab(url)
|
|
248
247
|
|
|
249
248
|
|
|
250
|
-
# Per-query stall guard
|
|
251
|
-
#
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
# inside instead: if a single scan() call exceeds the deadline, print a loud
|
|
256
|
-
# marker, stamp the wedge strike file (so the next cycle's two-strike gate in
|
|
257
|
-
# twitter-backend.sh converges to a kill+relaunch if Chrome is still sick),
|
|
258
|
-
# and hard-exit the scan process so the cycle fails fast and releases the
|
|
259
|
-
# lock. A healthy query completes in well under a minute; 240s is generous.
|
|
260
|
-
_SCAN_QUERY_DEADLINE_S = float(os.environ.get("S4L_SCAN_QUERY_DEADLINE_S", "240"))
|
|
261
|
-
_WEDGE_STRIKE_FILE = "/tmp/s4l_cdp_wedge_strike_9555" # matches twitter-backend.sh
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
def _arm_stall_guard(query: str):
|
|
265
|
-
def _fire():
|
|
266
|
-
sys.stderr.write(
|
|
267
|
-
f"[twitter_scan] SCAN_STALL_ABORT query={query!r} "
|
|
268
|
-
f"deadline={_SCAN_QUERY_DEADLINE_S:.0f}s — CDP call hung (wedged "
|
|
269
|
-
"Chrome); stamping wedge strike and aborting the scan process\n"
|
|
270
|
-
)
|
|
271
|
-
sys.stderr.flush()
|
|
272
|
-
try:
|
|
273
|
-
pathlib.Path(_WEDGE_STRIKE_FILE).touch()
|
|
274
|
-
except OSError:
|
|
275
|
-
pass
|
|
276
|
-
os._exit(75) # EX_TEMPFAIL: cycle fails fast, lock + slot free up
|
|
249
|
+
# Per-query stall guard: shared implementation (scripts/stall_guard.py) used
|
|
250
|
+
# by every browser-touching pipeline step — do NOT re-implement per step.
|
|
251
|
+
# S4L_SCAN_QUERY_DEADLINE_S kept as a scan-specific override for continuity
|
|
252
|
+
# with rc.36-38; unset, the shared default applies.
|
|
253
|
+
from stall_guard import stall_guard # noqa: E402
|
|
277
254
|
|
|
278
|
-
|
|
279
|
-
t.daemon = True
|
|
280
|
-
t.start()
|
|
281
|
-
return t
|
|
255
|
+
_SCAN_DEADLINE_OVERRIDE = os.environ.get("S4L_SCAN_QUERY_DEADLINE_S")
|
|
282
256
|
|
|
283
257
|
|
|
284
258
|
def scan(
|
|
@@ -293,11 +267,12 @@ def scan(
|
|
|
293
267
|
###TWEETS_BEGIN###/###TWEETS_END### sentinels for the scan model to relay
|
|
294
268
|
into StructuredOutput; also returns the kept list so direct callers (tests,
|
|
295
269
|
future shell-driven invocations) can consume it without parsing stdout."""
|
|
296
|
-
|
|
297
|
-
|
|
270
|
+
with stall_guard(
|
|
271
|
+
"scan_query",
|
|
272
|
+
query,
|
|
273
|
+
deadline_s=float(_SCAN_DEADLINE_OVERRIDE) if _SCAN_DEADLINE_OVERRIDE else None,
|
|
274
|
+
):
|
|
298
275
|
return _scan_inner(query, project, search_topic, freshness_hours, skip_ids, settle_seconds)
|
|
299
|
-
finally:
|
|
300
|
-
_stall_guard.cancel()
|
|
301
276
|
|
|
302
277
|
|
|
303
278
|
def _scan_inner(
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# browser-launch.sh — the ONE way any backend spawns a harness Chrome.
|
|
3
|
+
#
|
|
4
|
+
# Consolidates what twitter-backend.sh and linkedin-backend.sh grew as
|
|
5
|
+
# hand-rolled near-duplicates on 2026-07-13 (and reddit-backend.sh never got
|
|
6
|
+
# at all). Source this from a backend lib and call:
|
|
7
|
+
#
|
|
8
|
+
# launch_harness_chrome "$_chrome_bin" "$_prof_dir" <chrome arg>...
|
|
9
|
+
#
|
|
10
|
+
# What every launch gets, uniformly:
|
|
11
|
+
# 1. CLEAN-EXIT STAMP: mark the profile's last exit as clean first, so a
|
|
12
|
+
# SIGKILLed Chrome (wedge heal, stall abort) doesn't session-restore a
|
|
13
|
+
# crashed "Aw, Snap" corpse tab on relaunch.
|
|
14
|
+
# 2. NO FOCUS STEAL (macOS + .app bundle): `open -n -g` launches a NEW
|
|
15
|
+
# instance (-n: the user's personal Chrome may already be running;
|
|
16
|
+
# without it LaunchServices pokes that instance and drops our --args)
|
|
17
|
+
# without activation (-g). A directly-exec'd Chrome always activates
|
|
18
|
+
# itself and planted a window over the user's work on every relaunch.
|
|
19
|
+
# LaunchServices also parents the process outside the caller's launchd
|
|
20
|
+
# job process group — independent cover for the pgroup-reaping bug.
|
|
21
|
+
# (Tested alternatives that DON'T work: `-j` — Chrome unhides and
|
|
22
|
+
# activates itself anyway; `--no-startup-window` — the activation just
|
|
23
|
+
# moves to first-tab creation.)
|
|
24
|
+
# 3. DETACHED FALLBACK (Linux / bare chromium binaries): direct exec in a
|
|
25
|
+
# NEW SESSION via os.setsid, so a transient launchd job's exit can't
|
|
26
|
+
# SIGKILL Chrome with the job's process group (2026-07-12 root cause of
|
|
27
|
+
# the kill-relaunch-foreground loop).
|
|
28
|
+
launch_harness_chrome() {
|
|
29
|
+
local _bl_chrome_bin="$1"; shift
|
|
30
|
+
local _bl_prof_dir="$1"; shift
|
|
31
|
+
"${S4L_PYTHON:-python3}" -c 'import json, os, sys
|
|
32
|
+
p = os.path.join(sys.argv[1], "Default", "Preferences")
|
|
33
|
+
try:
|
|
34
|
+
d = json.load(open(p))
|
|
35
|
+
except Exception:
|
|
36
|
+
raise SystemExit(0)
|
|
37
|
+
prof = d.setdefault("profile", {})
|
|
38
|
+
prof["exit_type"] = "Normal"
|
|
39
|
+
prof["exited_cleanly"] = True
|
|
40
|
+
json.dump(d, open(p, "w"))' "$_bl_prof_dir" 2>/dev/null || true
|
|
41
|
+
local _bl_app_bundle=""
|
|
42
|
+
case "$_bl_chrome_bin" in
|
|
43
|
+
*.app/Contents/MacOS/*) _bl_app_bundle="${_bl_chrome_bin%%/Contents/MacOS/*}" ;;
|
|
44
|
+
esac
|
|
45
|
+
if [ "$(uname -s)" = "Darwin" ] && [ -n "$_bl_app_bundle" ] && [ -d "$_bl_app_bundle" ]; then
|
|
46
|
+
open -n -g -a "$_bl_app_bundle" --args "$@" >/dev/null 2>&1 || true
|
|
47
|
+
else
|
|
48
|
+
"${S4L_PYTHON:-python3}" -c 'import os,sys
|
|
49
|
+
os.setsid()
|
|
50
|
+
os.execv(sys.argv[1], sys.argv[1:])' \
|
|
51
|
+
"$_bl_chrome_bin" "$@" >/dev/null 2>&1 &
|
|
52
|
+
disown
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
@@ -333,52 +333,21 @@ ensure_linkedin_browser_for_backend() {
|
|
|
333
333
|
# The occlusion/backgrounding flags matter: the window sits offscreen,
|
|
334
334
|
# and without them Chrome stops laying out SPA-rendered content, so
|
|
335
335
|
# every element measures 0x0 and clicks become impossible (2026-07-03).
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
local _li_launch_args=(
|
|
351
|
-
--remote-debugging-port=9556
|
|
352
|
-
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin"
|
|
353
|
-
--no-first-run --no-default-browser-check
|
|
354
|
-
--disable-features=ChromeWhatsNewUI
|
|
355
|
-
--disable-backgrounding-occluded-windows
|
|
356
|
-
--disable-renderer-backgrounding
|
|
357
|
-
--disable-background-timer-throttling
|
|
358
|
-
"${_extra[@]}"
|
|
336
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh) — see
|
|
337
|
+
# that file for the clean-exit stamp + no-focus-steal + detach
|
|
338
|
+
# rationale. Do NOT hand-roll launch logic per backend.
|
|
339
|
+
# shellcheck disable=SC1091
|
|
340
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
341
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
342
|
+
--remote-debugging-port=9556 \
|
|
343
|
+
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness-linkedin" \
|
|
344
|
+
--no-first-run --no-default-browser-check \
|
|
345
|
+
--disable-features=ChromeWhatsNewUI \
|
|
346
|
+
--disable-backgrounding-occluded-windows \
|
|
347
|
+
--disable-renderer-backgrounding \
|
|
348
|
+
--disable-background-timer-throttling \
|
|
349
|
+
"${_extra[@]}" \
|
|
359
350
|
about:blank
|
|
360
|
-
)
|
|
361
|
-
# macOS + .app-bundled Chrome: `open -n -g` launches a NEW instance
|
|
362
|
-
# WITHOUT stealing the user's focus (a directly-exec'd Chrome always
|
|
363
|
-
# activates itself), and LaunchServices parents it outside this launchd
|
|
364
|
-
# job's process group. Same fix + rationale as twitter-backend.sh
|
|
365
|
-
# (2026-07-13). Fallback: direct exec in a new session — os.setsid so
|
|
366
|
-
# Chrome escapes the job's process group; launchd SIGKILLs a transient
|
|
367
|
-
# job's whole pgroup when the shell exits, and `disown` does not change
|
|
368
|
-
# the pgid (2026-07-12).
|
|
369
|
-
local _li_app_bundle=""
|
|
370
|
-
case "$_chrome_bin" in
|
|
371
|
-
*.app/Contents/MacOS/*) _li_app_bundle="${_chrome_bin%%/Contents/MacOS/*}" ;;
|
|
372
|
-
esac
|
|
373
|
-
if [ "$(uname -s)" = "Darwin" ] && [ -n "$_li_app_bundle" ] && [ -d "$_li_app_bundle" ]; then
|
|
374
|
-
open -n -g -a "$_li_app_bundle" --args "${_li_launch_args[@]}" >/dev/null 2>&1 || true
|
|
375
|
-
else
|
|
376
|
-
"${S4L_PYTHON:-python3}" -c 'import os,sys
|
|
377
|
-
os.setsid()
|
|
378
|
-
os.execv(sys.argv[1], sys.argv[1:])' \
|
|
379
|
-
"$_chrome_bin" "${_li_launch_args[@]}" >/dev/null 2>&1 &
|
|
380
|
-
disown
|
|
381
|
-
fi
|
|
382
351
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
383
352
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9556/json/version 2>/dev/null && break
|
|
384
353
|
sleep 1
|
|
@@ -203,14 +203,20 @@ ensure_reddit_browser_for_backend() {
|
|
|
203
203
|
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
204
204
|
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
205
205
|
fi
|
|
206
|
-
|
|
206
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
|
|
207
|
+
# exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
|
|
208
|
+
# steal, out of this job's pgroup), setsid exec fallback elsewhere.
|
|
209
|
+
# This backend previously used a bare exec and had NONE of those
|
|
210
|
+
# protections (2026-07-13 consolidation).
|
|
211
|
+
# shellcheck disable=SC1091
|
|
212
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
213
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
207
214
|
--remote-debugging-port=9557 \
|
|
208
215
|
--user-data-dir="$HOME/.claude/browser-profiles/reddit-harness" \
|
|
209
216
|
--no-first-run --no-default-browser-check \
|
|
210
217
|
--disable-features=ChromeWhatsNewUI \
|
|
211
218
|
"${_extra[@]}" \
|
|
212
|
-
about:blank
|
|
213
|
-
disown
|
|
219
|
+
about:blank
|
|
214
220
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
215
221
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9557/json/version 2>/dev/null && break
|
|
216
222
|
sleep 1
|
|
@@ -334,64 +334,21 @@ ensure_twitter_browser_for_backend() {
|
|
|
334
334
|
[ -n "$_stale_pids" ] && { kill -9 $_stale_pids 2>/dev/null || true; sleep 1; }
|
|
335
335
|
rm -f "$_prof_dir/SingletonLock" "$_prof_dir/SingletonSocket" "$_prof_dir/SingletonCookie" 2>/dev/null || true
|
|
336
336
|
fi
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
prof["exit_type"] = "Normal"
|
|
352
|
-
prof["exited_cleanly"] = True
|
|
353
|
-
json.dump(d, open(p, "w"))' "$_prof_dir" 2>/dev/null || true
|
|
354
|
-
# Launch flags shared by both spawn paths below.
|
|
355
|
-
local _launch_args=(
|
|
356
|
-
--remote-debugging-port=9555
|
|
357
|
-
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness"
|
|
358
|
-
--no-first-run --no-default-browser-check
|
|
359
|
-
--password-store=basic --use-mock-keychain
|
|
360
|
-
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion
|
|
361
|
-
--disable-backgrounding-occluded-windows
|
|
362
|
-
"${_extra[@]}"
|
|
337
|
+
# Spawn via the SHARED launcher (skill/lib/browser-launch.sh): clean-
|
|
338
|
+
# exit stamp (no Aw-Snap restore), `open -n -g` on macOS (no focus
|
|
339
|
+
# steal, out of this job's pgroup), setsid exec fallback elsewhere.
|
|
340
|
+
# Do NOT hand-roll launch logic per backend.
|
|
341
|
+
# shellcheck disable=SC1091
|
|
342
|
+
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/browser-launch.sh"
|
|
343
|
+
launch_harness_chrome "$_chrome_bin" "$_prof_dir" \
|
|
344
|
+
--remote-debugging-port=9555 \
|
|
345
|
+
--user-data-dir="$HOME/.claude/browser-profiles/browser-harness" \
|
|
346
|
+
--no-first-run --no-default-browser-check \
|
|
347
|
+
--password-store=basic --use-mock-keychain \
|
|
348
|
+
--disable-features=ChromeWhatsNewUI,CalculateNativeWinOcclusion \
|
|
349
|
+
--disable-backgrounding-occluded-windows \
|
|
350
|
+
"${_extra[@]}" \
|
|
363
351
|
"${BH_LAUNCH_URL:-https://x.com}"
|
|
364
|
-
)
|
|
365
|
-
# macOS + .app-bundled Chrome: launch via `open -n -g` so the window
|
|
366
|
-
# NEVER steals the user's focus. A directly-exec'd Chrome always
|
|
367
|
-
# activates itself on launch — every legitimate relaunch (wedge heal,
|
|
368
|
-
# user quit, crash) popped a window over the user's work (the
|
|
369
|
-
# foreground-telemetry `cause:launched` events, 2026-07-13). -n forces
|
|
370
|
-
# a NEW instance (the user's personal Chrome may already be running;
|
|
371
|
-
# without -n LaunchServices would just poke it and drop our --args),
|
|
372
|
-
# -g launches WITHOUT activation. LaunchServices also parents the
|
|
373
|
-
# process outside this launchd job's process group, which independently
|
|
374
|
-
# covers the 2026-07-12 pgroup-reaping bug the setsid fallback fixes.
|
|
375
|
-
local _app_bundle=""
|
|
376
|
-
case "$_chrome_bin" in
|
|
377
|
-
*.app/Contents/MacOS/*) _app_bundle="${_chrome_bin%%/Contents/MacOS/*}" ;;
|
|
378
|
-
esac
|
|
379
|
-
if [ "$(uname -s)" = "Darwin" ] && [ -n "$_app_bundle" ] && [ -d "$_app_bundle" ]; then
|
|
380
|
-
open -n -g -a "$_app_bundle" --args "${_launch_args[@]}" >/dev/null 2>&1 || true
|
|
381
|
-
else
|
|
382
|
-
# Fallback (Linux, bare chromium binaries): direct exec in a NEW
|
|
383
|
-
# SESSION. os.setsid: Chrome must escape THIS job's process group —
|
|
384
|
-
# launchd SIGKILLs a transient job's whole process group the moment
|
|
385
|
-
# the shell exits (no AbandonProcessGroup), and `disown` does not
|
|
386
|
-
# change the pgid, so a plainly-backgrounded Chrome died on every
|
|
387
|
-
# cycle completion and the NEXT cycle's relaunch stole the user's
|
|
388
|
-
# focus (2026-07-12 root cause).
|
|
389
|
-
"${S4L_PYTHON:-python3}" -c 'import os,sys
|
|
390
|
-
os.setsid()
|
|
391
|
-
os.execv(sys.argv[1], sys.argv[1:])' \
|
|
392
|
-
"$_chrome_bin" "${_launch_args[@]}" >/dev/null 2>&1 &
|
|
393
|
-
disown
|
|
394
|
-
fi
|
|
395
352
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
396
353
|
curl -sf --max-time 2 -o /dev/null http://127.0.0.1:9555/json/version 2>/dev/null && break
|
|
397
354
|
sleep 1
|