@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 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);
@@ -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 marker of the CURRENT vendored patch: a symbol that exists only
737
- // in its newest revision (v2 added the daemon.py websocket-proxy
738
- // neutralization after v1's cdp_urlopen alone proved insufficient the
739
- // WS dial still routed through the macOS system proxy and died with
740
- // "proxy rejected connection: HTTP 503"). Bump this string whenever the
741
- // patch gains a new hunk, or upgraded installs will silently keep the
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
- if (fs.readFileSync(daemonPy, "utf-8").includes("s4l_no_proxy_ws")) {
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
- const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
750
- if (!fs.existsSync(harnessPatch))
751
- return { ok: false, detail: "patch file missing from package" };
752
- // Discard any PRIOR vendored-patch revision (e.g. v1) so the cumulative
753
- // patch applies onto pristine pinned sources. Only src/ is touched; the
754
- // checkout is a managed artifact, never a place for local edits.
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 chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
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: "patched + daemon reloaded" };
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 fix on top of the pin: loopback CDP requests must never route
1015
- // through a proxy. macOS system proxy settings leak into urllib's default
1016
- // opener, and a box-wide forwarder 403s every 127.0.0.1 probe, so Chrome
1017
- // reads as "wedged"/logged-out while it is actually fine (2026-07-13).
1018
- // Upstream doesn't carry the fix and we don't control that repo, so apply
1019
- // our patch at install time, on every machine. If it stops applying
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], {
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.7-rc.8",
3
- "installedAt": "2026-08-03T22:41:58.148Z"
2
+ "version": "1.7.7",
3
+ "installedAt": "2026-08-04T18:33:55.672Z"
4
4
  }
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-rc.8",
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-rc.8",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.7-rc.8",
3
+ "version": "1.7.7",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",
@@ -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
- r = _send("Target.createTarget", {"url": "about:blank"})
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
- page = ctx.new_page()
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: