@m13v/s4l 1.7.6 → 1.7.7-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 (41) hide show
  1. package/bin/cli.js +12 -0
  2. package/mcp/dist/index.js +25 -70
  3. package/mcp/dist/repo.js +15 -1
  4. package/mcp/dist/runtime.js +103 -55
  5. package/mcp/dist/version.js +49 -2
  6. package/mcp/dist/version.json +2 -2
  7. package/mcp/manifest.json +1 -1
  8. package/mcp/menubar/s4l_browser_foreground.py +54 -0
  9. package/mcp/menubar/s4l_card.py +19 -20
  10. package/mcp/menubar/s4l_card_canvas.py +29 -26
  11. package/mcp/menubar/s4l_menubar.py +38 -0
  12. package/mcp/menubar/s4l_state.py +118 -5
  13. package/mcp/package.json +1 -1
  14. package/package.json +1 -1
  15. package/scripts/_pa_compact.py +15 -0
  16. package/scripts/_pa_q.py +14 -0
  17. package/scripts/_pa_run.py +20 -0
  18. package/scripts/browser_lifecycle.py +48 -0
  19. package/scripts/claude_job.py +14 -0
  20. package/scripts/cleanup_harness_tabs.py +74 -11
  21. package/scripts/context_mining.py +664 -0
  22. package/scripts/copy_browser_cookies.py +3 -1
  23. package/scripts/harness_target_monitor.py +105 -0
  24. package/scripts/linkedin_session_watch.py +196 -0
  25. package/scripts/merge_review_queue.py +3 -1
  26. package/scripts/patches/browser-harness-daemon-blank-tab-attach.patch +30 -0
  27. package/scripts/post_reddit.py +1 -1
  28. package/scripts/reddit_ban_check.py +19 -1
  29. package/scripts/reddit_browser.py +9 -3
  30. package/scripts/reddit_browser_fetch.py +5 -1
  31. package/scripts/reddit_tools.py +50 -98
  32. package/scripts/s4l_box_update.sh +35 -5
  33. package/scripts/snapshot.py +53 -2
  34. package/scripts/stats.py +26 -53
  35. package/scripts/tlh_freshness_284.py +134 -0
  36. package/scripts/twitter_browser.py +208 -27
  37. package/skill/engage-dm-replies.sh +13 -3
  38. package/skill/lib/harness-common.sh +32 -3
  39. package/skill/lib/linkedin-backend.sh +0 -1
  40. package/skill/lib/reddit-backend.sh +0 -1
  41. package/skill/lib/twitter-backend.sh +0 -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/index.js CHANGED
@@ -131,10 +131,13 @@ const STALL_WATCH_INTERVAL_SECS = 120;
131
131
  // S4L_OVERLAY_WATCH=0.
132
132
  const OVERLAY_WATCH_LABEL = "com.m13v.social-overlay-watch";
133
133
  const OVERLAY_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${OVERLAY_WATCH_LABEL}.plist`);
134
- // Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
135
- // install keeps itself current the interactive `runtime` tool (action:'update')
136
- // only helps when
137
- // a human-facing agent session is open, which an autopilot box never has.
134
+ // Legacy daily self-updater launchd job. BANNED (user decision 2026-07-31):
135
+ // it ran s4l_box_update.sh unattended, which kills and relaunches Claude
136
+ // Desktop with no human in the loop. Updates are user-initiated only (menubar
137
+ // "Update now & restart Claude Desktop", the panel button, `runtime`
138
+ // action:'update', or an operator running s4l_box_update.sh explicitly). The
139
+ // label/plist constants survive so ensureUpdaterRemoved() can clean the job
140
+ // off every box that got it from v1.7.1..v1.7.7-rc.4.
138
141
  const UPDATER_LABEL = "com.m13v.social-autoposter-update";
139
142
  const UPDATER_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${UPDATER_LABEL}.plist`);
140
143
  // A sane PATH for launchd jobs (launchd starts with a bare PATH). Include the
@@ -4874,68 +4877,21 @@ async function ensureOverlayWatchInstalled() {
4874
4877
  return { ok: false, detail: e?.message || String(e) };
4875
4878
  }
4876
4879
  }
4877
- // Install/refresh the daily self-updater launchd job. This used to be bundled
4878
- // into the now-deleted `autopilot` MCP tool (removed 2026-06-19, 88bd1cb9):
4879
- // calling `autopilot enable` installed this job as a side effect, so removing
4880
- // the tool silently orphaned itUPDATER_LABEL/UPDATER_PLIST and the
4881
- // auto_update_on status check survived (buildSnapshot still reports them), but
4882
- // nothing has installed the plist since, on ANY box provisioned after that
4883
- // commit (auto_update_on reads false forever). Restored here as its own
4884
- // deterministic boot-time job, same pattern as its five siblings above.
4885
- //
4886
- // Points at scripts/s4l_box_update.sh, NOT skill/social-autoposter-update.sh:
4887
- // the latter is the npm-lane updater (npm view + npx update) and is a silent
4888
- // no-op on a .mcpb box, which has no npm/npx on PATH (see version.ts). The
4889
- // .mcpb-lane equivalent downloads the .mcpb directly from the channel-resolved
4890
- // GitHub release and unpacks it over the extension dir — see that script's own
4891
- // header for the channel/no-downgrade/retry guards. Default mode there
4892
- // downloads + unpacks + restarts Claude Desktop with NO human in the loop,
4893
- // matching the original bundled updater's intent ("keeps a headless install
4894
- // current"); RunAtLoad so a box that boots already-behind checks promptly.
4895
- async function ensureUpdaterInstalled() {
4880
+ // Remove the legacy daily self-updater launchd job (see UPDATER_LABEL's
4881
+ // comment). Boot-time, idempotent, best-effort: unload the job if launchd has
4882
+ // it and delete the plist so the daily silent Claude Desktop restart can never
4883
+ // fire again. Do NOT reintroduce an install path for this job any future
4884
+ // auto-update mechanism must be user-visible and user-confirmed.
4885
+ async function ensureUpdaterRemoved() {
4896
4886
  try {
4897
4887
  if (process.platform !== "darwin")
4898
- return { ok: false, detail: "not macOS" };
4899
- if ((process.env.S4L_AUTO_UPDATE) === "0")
4900
- return { ok: false, detail: "disabled (S4L_AUTO_UPDATE=0)" };
4901
- const logDir = path.join(repoDir(), "skill", "logs");
4902
- try {
4903
- fs.mkdirSync(logDir, { recursive: true });
4904
- }
4905
- catch {
4906
- /* best-effort */
4907
- }
4908
- const xml = plistXml({
4909
- label: UPDATER_LABEL,
4910
- programArgs: ["/bin/bash", path.join(repoDir(), "scripts", "s4l_box_update.sh")],
4911
- intervalSecs: 86_400,
4912
- runAtLoad: true,
4913
- stdoutLog: path.join(logDir, "launchd-self-update-stdout.log"),
4914
- stderrLog: path.join(logDir, "launchd-self-update-stderr.log"),
4915
- });
4888
+ return { ok: true, detail: "not macOS" };
4916
4889
  const uid = process.getuid ? process.getuid() : 0;
4917
- let cur = null;
4918
- try {
4919
- cur = fs.readFileSync(UPDATER_PLIST, "utf-8");
4920
- }
4921
- catch {
4922
- cur = null;
4923
- }
4924
- let detail;
4925
- if (cur === xml) {
4926
- const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4927
- detail = `current (load rc=${res.code})`;
4928
- }
4929
- else {
4930
- if (cur !== null) {
4931
- await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4932
- }
4933
- fs.mkdirSync(path.dirname(UPDATER_PLIST), { recursive: true });
4934
- fs.writeFileSync(UPDATER_PLIST, xml, "utf-8");
4935
- const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4936
- detail = cur === null ? "installed + loaded" : `rewritten + reloaded (rc=${res.code})`;
4937
- }
4938
- return { ok: true, detail };
4890
+ const existed = fs.existsSync(UPDATER_PLIST);
4891
+ await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4892
+ if (existed)
4893
+ fs.rmSync(UPDATER_PLIST, { force: true });
4894
+ return { ok: true, detail: existed ? "removed" : "absent" };
4939
4895
  }
4940
4896
  catch (e) {
4941
4897
  return { ok: false, detail: e?.message || String(e) };
@@ -6134,13 +6090,12 @@ async function main() {
6134
6090
  void ensureOverlayWatchInstalled()
6135
6091
  .then((r) => console.error(`[overlay-watch] launchd supervisor: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6136
6092
  .catch((e) => console.error("[overlay-watch] supervisor install failed:", e?.message || e));
6137
- // Daily self-updater: restored 2026-07-08 after 88bd1cb9 ("Remove autopilot
6138
- // tool") silently dropped its only install path (it used to be bundled into
6139
- // the deleted `autopilot enable` action). Best-effort; never blocks boot.
6140
- // Disable with S4L_AUTO_UPDATE=0.
6141
- void ensureUpdaterInstalled()
6142
- .then((r) => console.error(`[self-update] launchd updater: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6143
- .catch((e) => console.error("[self-update] updater install failed:", e?.message || e));
6093
+ // Silent daily self-updates are banned (user decision 2026-07-31): the job
6094
+ // restarted Claude Desktop unattended. Clean it off every box that got it
6095
+ // from v1.7.1..v1.7.7-rc.4. Best-effort; never blocks boot.
6096
+ void ensureUpdaterRemoved()
6097
+ .then((r) => console.error(`[self-update] legacy launchd updater: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6098
+ .catch((e) => console.error("[self-update] updater removal failed:", e?.message || e));
6144
6099
  // Heal installs onboarded before short_links_live defaulted to false: such a
6145
6100
  // project wraps short links against the customer's own domain, which has no
6146
6101
  // /r/[code] resolver, so every minted link 404s. Re-point them at the s4l.ai
package/mcp/dist/repo.js CHANGED
@@ -71,6 +71,18 @@ export function run(cmd, args, opts = {}) {
71
71
  catch {
72
72
  /* a spawn observer must never break the run */
73
73
  }
74
+ if (opts.stdin != null) {
75
+ // Swallow async pipe errors (e.g. EPIPE when the child exits before
76
+ // reading) — an unhandled stream error would crash the whole process.
77
+ child.stdin.on("error", () => { });
78
+ try {
79
+ child.stdin.write(opts.stdin);
80
+ child.stdin.end();
81
+ }
82
+ catch {
83
+ /* a closed stdin must never break the run */
84
+ }
85
+ }
74
86
  let stdout = "";
75
87
  let stderr = "";
76
88
  // Per-stream partial-line buffers so onLine fires on whole lines only,
@@ -199,7 +211,9 @@ export function readPlan(batchId) {
199
211
  }
200
212
  }
201
213
  export function writePlan(batchId, plan) {
202
- fs.writeFileSync(planPath(batchId), JSON.stringify(plan, null, 2), "utf-8");
214
+ // Compact (no indent): the review store reached 65 MB with pretty-printing
215
+ // (2026-08-02 lag incident) and every reader pays the parse.
216
+ fs.writeFileSync(planPath(batchId), JSON.stringify(plan), "utf-8");
203
217
  }
204
218
  // Find the newest plan file when no batch id is supplied.
205
219
  export function latestBatchId() {
@@ -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], {
@@ -195,16 +195,63 @@ async function latestFromGithubRedirect() {
195
195
  return null;
196
196
  }
197
197
  }
198
+ // Optional GitHub token (2026-07-30): authenticated probes get 5000/h instead
199
+ // of the anonymous 60/h-per-IP quota that silenced the staging update banner
200
+ // on 2026-07-13 and again on 2026-07-30. Sources, in order: GITHUB_TOKEN /
201
+ // GH_TOKEN env, then `gh auth token` when the gh CLI exists (dev/operator
202
+ // machines; .mcpb boxes have neither and resolve to null instantly). The token
203
+ // is only ever sent to api.github.com, always via stdin (curl -H @-) so it
204
+ // never appears in argv/`ps`, and the `gh auth token` shell-out is noTee so it
205
+ // never reaches the telemetry relay. Keep in lockstep with
206
+ // scripts/snapshot.py::_github_token.
207
+ let ghTokCache = { at: 0, tok: null };
208
+ const GH_TOK_TTL_MS = 900_000;
209
+ async function githubToken() {
210
+ const now = Date.now();
211
+ if (ghTokCache.at && now - ghTokCache.at < GH_TOK_TTL_MS)
212
+ return ghTokCache.tok;
213
+ let tok = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null;
214
+ if (!tok) {
215
+ for (const gh of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "gh"]) {
216
+ const res = await run(gh, ["auth", "token"], { timeoutMs: 5000, noTee: true });
217
+ if (res.code === -1)
218
+ continue; // not spawnable at this path; try the next
219
+ const cand = (res.stdout || "").trim();
220
+ if (res.code === 0 && cand)
221
+ tok = cand;
222
+ break;
223
+ }
224
+ }
225
+ ghTokCache = { at: now, tok };
226
+ return tok;
227
+ }
198
228
  // Conditional-request state lives in the SHARED cache file (latest-release.json)
199
229
  // so the ETag survives process boundaries: short-lived MCP respawns used to pay
200
230
  // a full 200 per process; now every probe sends If-None-Match and gets a free
201
- // 304 between releases.
231
+ // 304 between releases. Probes authenticate when a GitHub token is available;
232
+ // a 401 (revoked/expired token) retries anonymously so a bad token is never
233
+ // worse than no token.
202
234
  async function curlConditional(url, etag) {
235
+ const tok = await githubToken();
236
+ const first = await curlOnce(url, etag, tok);
237
+ if (first.status === 401 && tok) {
238
+ ghTokCache = { at: Date.now(), tok: null }; // drop the dead token
239
+ return curlOnce(url, etag, null);
240
+ }
241
+ return first;
242
+ }
243
+ async function curlOnce(url, etag, token) {
203
244
  const args = ["-sS", "-m", "10", "-H", "Accept: application/vnd.github+json"];
245
+ if (token)
246
+ args.push("-H", "@-"); // Authorization arrives via stdin, never argv
204
247
  if (etag)
205
248
  args.push("-H", `If-None-Match: ${etag}`);
206
249
  args.push("-w", "\n__CURL_STATUS__:%{http_code}\n__CURL_ETAG__:%header{etag}", url);
207
- const res = await run("curl", args, { timeoutMs: 12000, noTee: true });
250
+ const res = await run("curl", args, {
251
+ timeoutMs: 12000,
252
+ noTee: true,
253
+ stdin: token ? `Authorization: Bearer ${token}` : undefined,
254
+ });
208
255
  let status = 0;
209
256
  let newEtag = null;
210
257
  const body = [];
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.6",
3
- "installedAt": "2026-07-18T17:32:42.014Z"
2
+ "version": "1.7.7-rc.10",
3
+ "installedAt": "2026-08-04T02:28:00.288Z"
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.6",
5
+ "version": "1.7.7-rc.10",
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": {
@@ -79,6 +79,7 @@ class _Worker(threading.Thread):
79
79
  super().__init__(daemon=True, name="s4l-browser-foreground")
80
80
  self._pid_cache = {} # pid -> (is_harness, details dict)
81
81
  self._prev_app = None # last non-harness frontmost app name
82
+ self._prev_app_pid = None # ...and its pid, for precise re-activation
82
83
  self._last_key = None # (cause, pid) of last emitted event
83
84
  self._last_emit_at = 0.0
84
85
  self._suppressed = 0
@@ -108,13 +109,19 @@ class _Worker(threading.Thread):
108
109
  if "chrome" not in low and "chromium" not in low:
109
110
  if cause == "activated" and name:
110
111
  self._prev_app = name
112
+ self._prev_app_pid = pid
111
113
  return
112
114
  is_harness, details = self._classify(pid)
113
115
  if not is_harness:
114
116
  # The user's own Chrome counts as their workspace too.
115
117
  if cause == "activated" and name:
116
118
  self._prev_app = name
119
+ self._prev_app_pid = pid
117
120
  return
121
+ # Suppress focus on EVERY harness activation, BEFORE the telemetry
122
+ # dedupe — the log is rate-limited, but hiding must fire every time or
123
+ # a fast pop-burst would leave later pops sitting in the foreground.
124
+ self._suppress_focus(pid, details)
118
125
  now = time.time()
119
126
  key = (cause, pid)
120
127
  if key == self._last_key and now - self._last_emit_at < _DEDUPE_SECONDS:
@@ -137,6 +144,53 @@ class _Worker(threading.Thread):
137
144
  context="browser-foreground",
138
145
  )
139
146
 
147
+ def _suppress_focus(self, pid, details):
148
+ # OS-LEVEL FOCUS SUPPRESSION (2026-07-30). The per-path code fixes
149
+ # (bh-harness activate suppression, tab reuse/park) each cover ONE way
150
+ # of driving Chrome; the reddit pipeline reaches it via a DIFFERENT
151
+ # path (reddit_browser.py Playwright connect/new_page) that bypasses
152
+ # them, and a raw external Playwright connect bypasses everything. This
153
+ # acts on the OS window event itself, so it covers EVERY connector:
154
+ # when an OFFSCREEN automation harness (window parked at negative Y —
155
+ # twitter/reddit/linkedin; NOT the onscreen setup-login window) grabs
156
+ # the foreground, hide that specific Chrome process by pid, then force
157
+ # focus back to the app the user was in. Screenshots/clicks are
158
+ # unaffected (CDP is offscreen-raster + synthetic input; the occlusion
159
+ # flags keep hidden tabs painting).
160
+ # DEFAULT OFF (user decision 2026-07-30): only the telemetry above runs
161
+ # unless S4L_HARNESS_HIDE=1 opts in. S4L_NO_HARNESS_HIDE force-disables
162
+ # even when the opt-in is set.
163
+ if os.environ.get("S4L_NO_HARNESS_HIDE"):
164
+ return
165
+ if os.environ.get("S4L_HARNESS_HIDE") != "1":
166
+ return
167
+ pos = details.get("window_position") or ""
168
+ try:
169
+ offscreen = any(int(float(v)) < 0 for v in pos.split(",")[:2])
170
+ except Exception:
171
+ offscreen = False
172
+ if not offscreen:
173
+ return # onscreen (e.g. setup login) — leave it visible
174
+ try:
175
+ from AppKit import NSRunningApplication
176
+
177
+ ra = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
178
+ if ra is not None:
179
+ ra.hide() # order the harness window out
180
+ # hide() alone does NOT reliably return focus here: all three
181
+ # harnesses share the "Google Chrome Beta" bundle, so hiding one
182
+ # process can leave the app-level active state on a sibling. Force
183
+ # focus back to the app the user was actually in.
184
+ if self._prev_app_pid and self._prev_app_pid != pid:
185
+ prev = NSRunningApplication.runningApplicationWithProcessIdentifier_(
186
+ self._prev_app_pid
187
+ )
188
+ # NSApplicationActivateIgnoringOtherApps = 1 << 1
189
+ if prev is not None and not prev.isTerminated():
190
+ prev.activateWithOptions_(1 << 1)
191
+ except Exception:
192
+ pass
193
+
140
194
  def run(self):
141
195
  while True:
142
196
  try:
@@ -464,12 +464,12 @@ def _compact_arm(variant):
464
464
 
465
465
 
466
466
  def _reply_heading_suffix(d):
467
- """Concise 'project/lane · viral N · t v4' readout appended straight onto the
468
- 'Reply (editable):' heading (2026-07-07), so the two facts a reviewer
469
- checks first don't require opening the details eye. Pulled from the same
467
+ """Concise 'project · viral N · t v4' readout appended straight onto the
468
+ 'Reply (editable):' heading (2026-07-07), so the facts a reviewer checks
469
+ first don't require opening the details eye. Pulled from the same
470
470
  fields _details_lines used to render as 'Project:'/'Lane:'/'Virality
471
- score:' rows; those three are omitted from the popover now to avoid
472
- showing the same numbers twice. Empty string when neither is known."""
471
+ score:' rows; those are omitted from the popover now to avoid showing
472
+ the same numbers twice. Empty string when neither is known."""
473
473
  d = d or {}
474
474
  project = (d.get("project") or "").strip()
475
475
  lane = ((d.get("experiments") or {}).get("lane") or "").strip()
@@ -480,20 +480,11 @@ def _reply_heading_suffix(d):
480
480
  # approve_() blocks posting on these and the reviewer should know why
481
481
  # before clicking rather than after.
482
482
  bits.append("⚠ SANDBOX — not postable")
483
- # Project and lane are independent concepts most of the time (project
484
- # "fazm" drafted under lane "personal_brand"), but the operator's own
485
- # PersonalBrand project IS the personal_brand lane, so project=="PersonalBrand"
486
- # + lane=="personal_brand" is the SAME fact twice, not two facts — compare
487
- # with punctuation/case stripped so "PersonalBrand" vs "personal_brand"
488
- # collapses to one token instead of "PersonalBrand/personal_brand".
489
- same = project and lane and re.sub(r"[^a-z0-9]", "", project.lower()) == re.sub(
490
- r"[^a-z0-9]", "", lane.lower()
491
- )
492
- if same:
493
- bits.append(project)
494
- elif project and lane:
495
- bits.append(f"{project}/{lane}")
496
- elif project:
483
+ # Heading shows the project/lane name only, never a combined
484
+ # "project/lane" string (2026-07-31, user call: the "/promotion" suffix
485
+ # on cards was confusing). Lane is still stamped on the candidate and
486
+ # readable in the details popover; it's just not concatenated here.
487
+ if project:
497
488
  bits.append(project)
498
489
  elif lane:
499
490
  bits.append(lane)
@@ -1973,7 +1964,12 @@ class _ReviewController(NSObject):
1973
1964
  """NSTimer target (2026-07-15): re-renders the header's age/expiry
1974
1965
  label every second so its countdown visibly counts down without
1975
1966
  needing hover. Not a python_method -- NSTimer invokes this through
1976
- the ObjC runtime."""
1967
+ the ObjC runtime. No-op when the rendered text is unchanged: the
1968
+ label usually shows a coarse "3h"-style value that only changes
1969
+ every few minutes, and unconditionally re-styling it dirtied one
1970
+ layer per tile per second -- with a 130-tile canvas that was a
1971
+ constant CoreAnimation commit churn keeping the app at ~15% CPU
1972
+ while idle (2026-08-02 lag incident)."""
1977
1973
  if self._age_expiry_label is None:
1978
1974
  return
1979
1975
  try:
@@ -1984,6 +1980,9 @@ class _ReviewController(NSObject):
1984
1980
  )
1985
1981
  if not text:
1986
1982
  return
1983
+ if (text, urgent) == getattr(self, "_age_expiry_last", None):
1984
+ return
1985
+ self._age_expiry_last = (text, urgent)
1987
1986
  self._age_expiry_label.setStringValue_(text)
1988
1987
  self._age_expiry_label.setFont_(_font(11, urgent))
1989
1988
  self._age_expiry_label.setTextColor_(