@m13v/s4l 1.7.7-rc.8 → 1.7.7
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/bin/cli.js +12 -0
- package/mcp/dist/runtime.js +103 -55
- 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/browser_lifecycle.py +19 -1
- package/scripts/copy_browser_cookies.py +3 -1
- package/scripts/patches/browser-harness-daemon-blank-tab-attach.patch +30 -0
- package/scripts/reddit_ban_check.py +4 -1
package/bin/cli.js
CHANGED
|
@@ -456,6 +456,18 @@ function installBrowserHarness() {
|
|
|
456
456
|
label: 'focus-suppression + tab-event observability',
|
|
457
457
|
absentWarn: 'harness Chrome may steal OS focus on every tab switch',
|
|
458
458
|
},
|
|
459
|
+
{
|
|
460
|
+
// Daemon attach_first_page: reuse an existing blank tab and create in
|
|
461
|
+
// the BACKGROUND when truly tabless. Without it, a blank-only browser
|
|
462
|
+
// traps the daemon in a stale-session loop where every re-attach mints
|
|
463
|
+
// a foreground about:blank = one macOS focus steal per iteration
|
|
464
|
+
// (2026-08-03, 30+/day on the reddit harness).
|
|
465
|
+
file: 'browser-harness-daemon-blank-tab-attach.patch',
|
|
466
|
+
sentinelFile: path.join('src', 'browser_harness', 'daemon.py'),
|
|
467
|
+
sentinelStr: 'no page tabs at all',
|
|
468
|
+
label: 'daemon blank-tab background attach',
|
|
469
|
+
absentWarn: 'harness daemon re-attach may pop the Chrome window (focus steal)',
|
|
470
|
+
},
|
|
459
471
|
];
|
|
460
472
|
for (const p of vendoredPatches) {
|
|
461
473
|
const patchPath = path.join(PKG_ROOT, 'scripts', 'patches', p.file);
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -34,6 +34,82 @@ const BROWSER_HARNESS_PIN = "6d20866664ea3d9691b27bbf64f42ae097437dc3";
|
|
|
34
34
|
const BROWSER_HARNESS_REPO = "https://github.com/browser-use/browser-harness";
|
|
35
35
|
const HARNESS_DIR = path.join(os.homedir(), "Developer", "browser-harness");
|
|
36
36
|
const HARNESS_BIN = path.join(os.homedir(), ".local", "bin", "browser-harness");
|
|
37
|
+
// Vendored fixes applied on top of the pin (we don't control upstream).
|
|
38
|
+
// KEEP IN SYNC WITH bin/cli.js `vendoredPatches`. Each: patch file under
|
|
39
|
+
// scripts/patches/ + a sentinel (file, substring) that means the fix is
|
|
40
|
+
// already present (upstream merged it, or we applied it), so a non-applying
|
|
41
|
+
// patch only warns when the fix is genuinely absent. Order matters: patches
|
|
42
|
+
// are generated to apply sequentially onto the pristine pin (each also
|
|
43
|
+
// applies standalone, verified 2026-08-03). NEVER fail install over these.
|
|
44
|
+
const HARNESS_VENDORED_PATCHES = [
|
|
45
|
+
{
|
|
46
|
+
file: "browser-harness-loopback-cdp-proxy.patch",
|
|
47
|
+
sentinelFile: path.join("src", "browser_harness", "admin.py"),
|
|
48
|
+
sentinelStr: "cdp_urlopen",
|
|
49
|
+
label: "loopback-proxy",
|
|
50
|
+
absentWarn: "CDP probes may 403 behind a system proxy",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// Suppress OS-focus-stealing Target.activateTarget on offscreen automation
|
|
54
|
+
// harnesses; [bh_tab_event] observability. Was previously applied only by
|
|
55
|
+
// the npm installer (bin/cli.js) — a bare .mcpb provision skipped it, so
|
|
56
|
+
// mcpb-only boxes ran upstream helpers that steal focus on every tab
|
|
57
|
+
// switch (gap closed 2026-08-03).
|
|
58
|
+
file: "browser-harness-focus-and-observability.patch",
|
|
59
|
+
sentinelFile: path.join("src", "browser_harness", "helpers.py"),
|
|
60
|
+
sentinelStr: "_is_offscreen_harness",
|
|
61
|
+
label: "focus-suppression + tab-event observability",
|
|
62
|
+
absentWarn: "harness Chrome may steal OS focus on every tab switch",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
// Daemon attach_first_page: reuse an existing blank tab and create in the
|
|
66
|
+
// BACKGROUND when truly tabless. Without it, a blank-only browser traps
|
|
67
|
+
// the daemon in a stale-session loop where every re-attach mints a
|
|
68
|
+
// foreground about:blank = one macOS focus steal per iteration.
|
|
69
|
+
file: "browser-harness-daemon-blank-tab-attach.patch",
|
|
70
|
+
sentinelFile: path.join("src", "browser_harness", "daemon.py"),
|
|
71
|
+
sentinelStr: "no page tabs at all",
|
|
72
|
+
label: "daemon blank-tab background attach",
|
|
73
|
+
absentWarn: "harness daemon re-attach may pop the Chrome window (focus steal)",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
/** Apply every vendored patch to HARNESS_DIR (check-then-apply, sentinel-aware).
|
|
77
|
+
* Returns a human-readable summary; never throws. */
|
|
78
|
+
async function applyHarnessVendoredPatches() {
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const p of HARNESS_VENDORED_PATCHES) {
|
|
81
|
+
const patchPath = path.join(MATERIALIZED_REPO, "scripts", "patches", p.file);
|
|
82
|
+
if (!fs.existsSync(patchPath)) {
|
|
83
|
+
results.push(`${p.label}: patch file missing`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", patchPath], {
|
|
87
|
+
timeoutMs: 30000,
|
|
88
|
+
});
|
|
89
|
+
if (chk.code === 0) {
|
|
90
|
+
const app = await sh("git", ["-C", HARNESS_DIR, "apply", patchPath], { timeoutMs: 30000 });
|
|
91
|
+
results.push(`${p.label}: ${app.code === 0 ? "applied" : `apply failed (${app.code})`}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let present = false;
|
|
95
|
+
try {
|
|
96
|
+
present = fs
|
|
97
|
+
.readFileSync(path.join(HARNESS_DIR, p.sentinelFile), "utf-8")
|
|
98
|
+
.includes(p.sentinelStr);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* file moved — fall through to the warning */
|
|
102
|
+
}
|
|
103
|
+
if (present) {
|
|
104
|
+
results.push(`${p.label}: already present`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.error(`[runtime] browser-harness ${p.label} patch no longer applies and the fix is absent upstream; ${p.absentWarn}`);
|
|
108
|
+
results.push(`${p.label}: ABSENT (patch stale)`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results.join("; ");
|
|
112
|
+
}
|
|
37
113
|
// The harness drives a REAL Google Chrome over CDP (see twitter-backend.sh
|
|
38
114
|
// _resolve_chrome_bin). Nothing installs Chrome, the runtime only ever
|
|
39
115
|
// downloaded Playwright's Chromium (which the cycle does NOT use), so a .mcpb
|
|
@@ -733,40 +809,37 @@ export async function ensureHarnessPatched() {
|
|
|
733
809
|
// installing) rather than racing on the same checkout.
|
|
734
810
|
if (anotherProcessProvisioning())
|
|
735
811
|
return { ok: false, detail: "provisioning in progress elsewhere" };
|
|
736
|
-
// Version
|
|
737
|
-
// in its newest revision (
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// previous revision.
|
|
812
|
+
// Version markers of the CURRENT vendored patches. A patch revision's
|
|
813
|
+
// marker is a symbol that exists only in its newest revision (the proxy
|
|
814
|
+
// patch's v2 added the daemon.py websocket-proxy neutralization —
|
|
815
|
+
// s4l_no_proxy_ws — after v1's cdp_urlopen alone proved insufficient).
|
|
816
|
+
// A NEW fix gets a NEW patch file + sentinel (see HARNESS_VENDORED_PATCHES)
|
|
817
|
+
// rather than new hunks in an old patch, or upgraded installs whose old
|
|
818
|
+
// sentinel still matches would silently keep the previous revision.
|
|
743
819
|
const daemonPy = path.join(HARNESS_DIR, "src", "browser_harness", "daemon.py");
|
|
744
820
|
if (!fs.existsSync(daemonPy))
|
|
745
821
|
return { ok: false, detail: "harness not installed yet" };
|
|
746
|
-
|
|
822
|
+
const daemonSrc = fs.readFileSync(daemonPy, "utf-8");
|
|
823
|
+
const helpersPy = path.join(HARNESS_DIR, "src", "browser_harness", "helpers.py");
|
|
824
|
+
const helpersSrc = fs.existsSync(helpersPy) ? fs.readFileSync(helpersPy, "utf-8") : "";
|
|
825
|
+
if (daemonSrc.includes("s4l_no_proxy_ws") &&
|
|
826
|
+
daemonSrc.includes("no page tabs at all") &&
|
|
827
|
+
helpersSrc.includes("_is_offscreen_harness")) {
|
|
747
828
|
return { ok: true, detail: "already patched" };
|
|
748
829
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
830
|
+
// Discard any PRIOR vendored-patch revision so the patch set applies onto
|
|
831
|
+
// pristine pinned sources, then re-apply ALL vendored patches (the old
|
|
832
|
+
// single-patch flow re-applied only the proxy fix here, silently wiping
|
|
833
|
+
// the focus + daemon patches whenever this boot-repair fired). Only src/
|
|
834
|
+
// is touched; the checkout is a managed artifact, never a place for
|
|
835
|
+
// local edits.
|
|
755
836
|
await sh("git", ["-C", HARNESS_DIR, "checkout", "--", "src/browser_harness"], {
|
|
756
837
|
timeoutMs: 30000,
|
|
757
838
|
});
|
|
758
|
-
const
|
|
759
|
-
timeoutMs: 30000,
|
|
760
|
-
});
|
|
761
|
-
if (chk.code !== 0) {
|
|
762
|
-
return { ok: false, detail: "patch does not apply (dirty or diverged checkout)" };
|
|
763
|
-
}
|
|
764
|
-
const app = await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
|
|
765
|
-
if (app.code !== 0)
|
|
766
|
-
return { ok: false, detail: `git apply failed (exit ${app.code})` };
|
|
839
|
+
const summary = await applyHarnessVendoredPatches();
|
|
767
840
|
// Reload the daemon so the long-lived process re-imports the patched source.
|
|
768
841
|
await sh(HARNESS_BIN, ["--reload"], { timeoutMs: 30000 });
|
|
769
|
-
return { ok: true, detail:
|
|
842
|
+
return { ok: true, detail: `patched + daemon reloaded (${summary})` };
|
|
770
843
|
}
|
|
771
844
|
catch (e) {
|
|
772
845
|
return { ok: false, detail: String(e?.message || e) };
|
|
@@ -1011,37 +1084,12 @@ async function provision(progress) {
|
|
|
1011
1084
|
timeoutMs: 120000,
|
|
1012
1085
|
});
|
|
1013
1086
|
await sh("git", ["-C", HARNESS_DIR, "reset", "--hard", "FETCH_HEAD"], { timeoutMs: 60000 });
|
|
1014
|
-
// Vendored
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
|
|
1020
|
-
// cleanly, upstream has either merged the fix (detected below; fine) or
|
|
1021
|
-
// refactored the files (warn); never fail the install over it.
|
|
1022
|
-
const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
|
|
1023
|
-
if (fs.existsSync(harnessPatch)) {
|
|
1024
|
-
const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
|
|
1025
|
-
timeoutMs: 30000,
|
|
1026
|
-
});
|
|
1027
|
-
if (chk.code === 0) {
|
|
1028
|
-
await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
|
|
1029
|
-
}
|
|
1030
|
-
else {
|
|
1031
|
-
let fixedUpstream = false;
|
|
1032
|
-
try {
|
|
1033
|
-
fixedUpstream = fs
|
|
1034
|
-
.readFileSync(path.join(HARNESS_DIR, "src", "browser_harness", "admin.py"), "utf-8")
|
|
1035
|
-
.includes("cdp_urlopen");
|
|
1036
|
-
}
|
|
1037
|
-
catch {
|
|
1038
|
-
/* file moved — fall through to the warning */
|
|
1039
|
-
}
|
|
1040
|
-
if (!fixedUpstream) {
|
|
1041
|
-
console.error("[runtime] browser-harness loopback-proxy patch no longer applies and the fix is absent upstream; CDP probes may 403 behind a system proxy");
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1087
|
+
// Vendored fixes on top of the pin (loopback-proxy, focus-suppression,
|
|
1088
|
+
// daemon blank-tab attach — see HARNESS_VENDORED_PATCHES). Applied at
|
|
1089
|
+
// install time on every machine since we don't control upstream. This
|
|
1090
|
+
// previously applied ONLY the proxy patch, so bare .mcpb boxes ran
|
|
1091
|
+
// upstream helpers/daemon that steal OS focus (gap closed 2026-08-03).
|
|
1092
|
+
await applyHarnessVendoredPatches();
|
|
1045
1093
|
// Install the CLI via uv tool (lands at ~/.local/bin/browser-harness).
|
|
1046
1094
|
// --force so a refreshed source / changed entry point is reinstalled.
|
|
1047
1095
|
const inst = await sh(uv, ["tool", "install", "--force", "-e", HARNESS_DIR], {
|
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.7
|
|
5
|
+
"version": "1.7.7",
|
|
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.7
|
|
3
|
+
"version": "1.7.7",
|
|
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
|
@@ -103,7 +103,25 @@ def background_new_page(browser, context, url: str = "about:blank", timeout_ms:
|
|
|
103
103
|
cdp.detach()
|
|
104
104
|
except Exception:
|
|
105
105
|
pass
|
|
106
|
-
except Exception:
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# LOUD fallback: this is the one remaining way a harness tab can be
|
|
108
|
+
# created in the foreground (= macOS focus steal). Log to stderr AND
|
|
109
|
+
# the universal browser-activity.log, because several lanes' stderr is
|
|
110
|
+
# captured into MCP results and never reaches disk (2026-08-04: an
|
|
111
|
+
# unattributed focus-pop hit exactly this observability hole).
|
|
112
|
+
msg = (f"[background_new_page] CDP background create failed "
|
|
113
|
+
f"({type(e).__name__}: {str(e)[:120]}); falling back to "
|
|
114
|
+
f"FOREGROUND new_page (focus steal likely)")
|
|
115
|
+
print(msg, file=sys.stderr)
|
|
116
|
+
try:
|
|
117
|
+
_p = os.path.expanduser("~/.claude/browser-profiles/browser-activity.log")
|
|
118
|
+
with open(_p, "a") as _f:
|
|
119
|
+
import time as _t
|
|
120
|
+
_f.write(f"[{_t.strftime('%Y-%m-%d %H:%M:%S')}] pycdp "
|
|
121
|
+
f"script=browser_lifecycle.py action=fg_fallback "
|
|
122
|
+
f"pid={os.getpid()} detail={type(e).__name__}\n")
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
107
125
|
return context.new_page()
|
|
108
126
|
|
|
109
127
|
|
|
@@ -364,7 +364,9 @@ def inject_via_cdp(cookies: Iterable[Cookie], cdp_url: str = "http://127.0.0.1:9
|
|
|
364
364
|
log.info("Storage.setCookies unavailable (no tabs); opening stub tab and retrying")
|
|
365
365
|
target_id = None
|
|
366
366
|
try:
|
|
367
|
-
|
|
367
|
+
# background: a foreground createTarget activates Chrome on macOS
|
|
368
|
+
# and steals app focus, even for a utility tab like this one.
|
|
369
|
+
r = _send("Target.createTarget", {"url": "about:blank", "background": True})
|
|
368
370
|
target_id = r.get("result", {}).get("targetId")
|
|
369
371
|
if not target_id:
|
|
370
372
|
log.warning("Couldn't create stub tab: %s", r)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
diff --git a/src/browser_harness/daemon.py b/src/browser_harness/daemon.py
|
|
2
|
+
index de51934..fb443c2 100644
|
|
3
|
+
--- a/src/browser_harness/daemon.py
|
|
4
|
+
+++ b/src/browser_harness/daemon.py
|
|
5
|
+
@@ -207,9 +207,22 @@ class Daemon:
|
|
6
|
+
targets = (await self.cdp.send_raw("Target.getTargets"))["targetInfos"]
|
|
7
|
+
pages = [t for t in targets if is_real_page(t)]
|
|
8
|
+
if not pages:
|
|
9
|
+
- # No real pages — create one instead of attaching to omnibox popup
|
|
10
|
+
- tid = (await self.cdp.send_raw("Target.createTarget", {"url": "about:blank"}))["targetId"]
|
|
11
|
+
- log(f"no real pages found, created about:blank ({tid})")
|
|
12
|
+
+ # No real pages: attach to an existing plain page tab (e.g. an
|
|
13
|
+
+ # about:blank left by a prior attach) before creating anything.
|
|
14
|
+
+ # is_real_page() rejects about: urls, so without this fallback a
|
|
15
|
+
+ # blank-only browser mints a NEW blank on every re-attach, forever.
|
|
16
|
+
+ pages = [t for t in targets if t.get("type") == "page"]
|
|
17
|
+
+ if pages:
|
|
18
|
+
+ log(f"no real pages found, reusing {pages[0].get('url','')[:60]} ({pages[0]['targetId']})")
|
|
19
|
+
+ if not pages:
|
|
20
|
+
+ # Truly no page tabs — create one instead of attaching to omnibox
|
|
21
|
+
+ # popup. background=True: a foreground Target.createTarget raises
|
|
22
|
+
+ # the Chrome window and steals macOS app focus (2026-07-15 7/7
|
|
23
|
+
+ # same-second correlation); a re-attach must never pop a window.
|
|
24
|
+
+ tid = (await self.cdp.send_raw(
|
|
25
|
+
+ "Target.createTarget", {"url": "about:blank", "background": True}
|
|
26
|
+
+ ))["targetId"]
|
|
27
|
+
+ log(f"no page tabs at all, created background about:blank ({tid})")
|
|
28
|
+
pages = [{"targetId": tid, "url": "about:blank", "type": "page"}]
|
|
29
|
+
self.session = (await self.cdp.send_raw(
|
|
30
|
+
"Target.attachToTarget", {"targetId": pages[0]["targetId"], "flatten": True}
|
|
@@ -114,7 +114,10 @@ def _reddit_page(pw):
|
|
|
114
114
|
if page is None and ctx.pages:
|
|
115
115
|
page = ctx.pages[0]
|
|
116
116
|
if page is None:
|
|
117
|
-
|
|
117
|
+
# Zero pages: create in the BACKGROUND (2026-08-04). A plain
|
|
118
|
+
# new_page() is a foreground Target.createTarget = focus steal.
|
|
119
|
+
from browser_lifecycle import background_new_page
|
|
120
|
+
page = background_new_page(browser, ctx)
|
|
118
121
|
page.set_default_timeout(20000)
|
|
119
122
|
return browser, page
|
|
120
123
|
except Exception:
|