social-autoposter 1.6.165 → 1.6.166

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/index.js CHANGED
@@ -82,17 +82,21 @@ const MEMORY_SNAPSHOT_INTERVAL_SECS = 60;
82
82
  const STALL_WATCH_LABEL = "com.m13v.social-autopilot-stall-watch";
83
83
  const STALL_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${STALL_WATCH_LABEL}.plist`);
84
84
  const STALL_WATCH_INTERVAL_SECS = 120;
85
- // On-screen overlay watcher supervisor. The harness status overlay ("S4L
86
- // running" / idle banner) only renders WHILE `harness_overlay.py watch` is
87
- // alive. That watcher is fire-and-forget with no supervisor of its own, so when
88
- // it dies (or the harness Chrome restarts) nothing brings it back and the
89
- // overlay silently disappears. Promote it to a first-class launchd job like its
90
- // siblings: RunAtLoad starts it at boot, the 60s StartInterval re-invokes the
91
- // idempotent supervisor script (pgrep guard => no-op while up) so a dead watcher
92
- // is respawned within a minute. Disable with SAPS_OVERLAY_WATCH=0.
85
+ // On-screen overlay watcher. The harness status overlay ("S4L running" / idle
86
+ // banner) only renders WHILE `harness_overlay.py watch` is alive. That watcher
87
+ // is fire-and-forget with no supervisor of its own, so when it dies (or the
88
+ // harness Chrome restarts) nothing brings it back and the overlay silently
89
+ // disappears. Promote it to a first-class launchd job, but run the long-lived
90
+ // watcher in the FOREGROUND under KeepAlive (NOT a StartInterval that re-invokes
91
+ // a spawn-and-exit supervisor). The supervisor pattern races launchd on macOS:
92
+ // the instant the kicker shell exits, launchd SIGKILLs the whole job process
93
+ // group and reaps the just-spawned watcher before it can detach. Running the
94
+ // watcher AS the job's main process makes launchd supervise it directly:
95
+ // RunAtLoad starts it at boot, KeepAlive restarts it if it ever exits, and on
96
+ // unload its SIGTERM handler clears the overlay cleanly. Disable with
97
+ // SAPS_OVERLAY_WATCH=0.
93
98
  const OVERLAY_WATCH_LABEL = "com.m13v.social-overlay-watch";
94
99
  const OVERLAY_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${OVERLAY_WATCH_LABEL}.plist`);
95
- const OVERLAY_WATCH_INTERVAL_SECS = 60;
96
100
  // Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
97
101
  // install keeps itself current — the interactive `runtime` tool (action:'update')
98
102
  // only helps when
@@ -134,6 +138,9 @@ function launchdPath() {
134
138
  }
135
139
  function plistXml(opts) {
136
140
  const args = opts.programArgs.map((a) => `\t\t<string>${a}</string>`).join("\n");
141
+ const schedule = opts.keepAlive
142
+ ? `\t<key>KeepAlive</key>\n\t<true/>`
143
+ : `\t<key>StartInterval</key>\n\t<integer>${opts.intervalSecs}</integer>`;
137
144
  // Background (cron/autopilot) runs get the same Chrome the interactive cycle
138
145
  // uses, so a no-sudo ~/Applications install (which the shell's own resolver
139
146
  // doesn't scan) is still found off-screen. Omitted when Chrome resolves via
@@ -159,8 +166,7 @@ function plistXml(opts) {
159
166
  \t<array>
160
167
  ${args}
161
168
  \t</array>
162
- \t<key>StartInterval</key>
163
- \t<integer>${opts.intervalSecs}</integer>
169
+ ${schedule}
164
170
  \t<key>StandardOutPath</key>
165
171
  \t<string>${opts.stdoutLog}</string>
166
172
  \t<key>StandardErrorPath</key>
@@ -2695,15 +2701,19 @@ async function ensureMemorySnapshotInstalled() {
2695
2701
  return { ok: false, detail: e?.message || String(e) };
2696
2702
  }
2697
2703
  }
2698
- // Install/refresh the on-screen overlay watcher launchd supervisor. Promotes the
2704
+ // Install/refresh the on-screen overlay watcher launchd job. Promotes the
2699
2705
  // harness status overlay from a best-effort, fired-from-other-tools nicety to a
2700
- // first-class self-healing job (RunAtLoad + 60s StartInterval). The script it
2701
- // drives (skill/run-overlay-watch.sh) is idempotent via a pgrep guard, so the
2702
- // 60s re-invocation is a fast no-op while the watcher is up and respawns it
2703
- // within a minute if it ever dies or the harness Chrome restarts. SAPS_PYTHON is
2704
- // baked by plistXml; we add SAPS_LOG_DIR (so the watcher reads the same cycle
2705
- // logs to decide busy/idle) and the harness CDP URL. Disable with
2706
- // SAPS_OVERLAY_WATCH=0.
2706
+ // first-class self-healing job. We run `harness_overlay.py watch` directly in
2707
+ // the FOREGROUND under KeepAlive (RunAtLoad starts it at boot; launchd restarts
2708
+ // it if it ever exits) rather than a StartInterval that re-fires a spawn-and-exit
2709
+ // supervisor: on macOS that supervisor races launchd, which SIGKILLs the job's
2710
+ // process group the instant the kicker shell exits and reaps the just-spawned
2711
+ // watcher before it can detach (verified on the box: the watcher caught the
2712
+ // group SIGTERM and cleared the overlay every cycle). harness_overlay.py holds a
2713
+ // singleton flock so the MCP's best-effort run-overlay-watch.sh lane can never
2714
+ // double-paint. SAPS_PYTHON is baked by plistXml; we add SAPS_LOG_DIR (so the
2715
+ // watcher reads the same cycle logs to decide busy/idle) and the harness CDP
2716
+ // URL. Disable with SAPS_OVERLAY_WATCH=0.
2707
2717
  async function ensureOverlayWatchInstalled() {
2708
2718
  try {
2709
2719
  if (process.platform !== "darwin")
@@ -2719,8 +2729,9 @@ async function ensureOverlayWatchInstalled() {
2719
2729
  }
2720
2730
  const xml = plistXml({
2721
2731
  label: OVERLAY_WATCH_LABEL,
2722
- programArgs: ["/bin/bash", path.join(repoDir(), "skill", "run-overlay-watch.sh")],
2723
- intervalSecs: OVERLAY_WATCH_INTERVAL_SECS,
2732
+ programArgs: [resolvePython(), path.join(repoDir(), "scripts", "harness_overlay.py"), "watch"],
2733
+ intervalSecs: 0,
2734
+ keepAlive: true,
2724
2735
  runAtLoad: true,
2725
2736
  stdoutLog: path.join(logDir, "launchd-overlay-watch-stdout.log"),
2726
2737
  stderrLog: path.join(logDir, "launchd-overlay-watch-stderr.log"),
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.165",
3
- "installedAt": "2026-06-30T02:01:48.183Z"
2
+ "version": "1.6.166",
3
+ "installedAt": "2026-06-30T02:20:22.438Z"
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.6.165",
5
+ "version": "1.6.166",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.165",
3
+ "version": "1.6.166",
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": "social-autoposter",
3
- "version": "1.6.165",
3
+ "version": "1.6.166",
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"
@@ -36,6 +36,7 @@ down it sleeps and retries; it never crashes the pipeline.
36
36
 
37
37
  from __future__ import annotations
38
38
 
39
+ import fcntl
39
40
  import glob
40
41
  import json
41
42
  import os
@@ -454,6 +455,20 @@ def cmd_watch(interval: float = 2.0) -> int:
454
455
  holds ONE CDP connection open across ticks (light, and friendly to the
455
456
  poster's concurrent CDP session) and only reconnects when the harness Chrome
456
457
  comes/goes. Never raises into the pipeline."""
458
+ # Singleton guard: there must be exactly ONE watcher painting at a time, or
459
+ # two loops fight over the same overlay (double heartbeat, flicker). Two start
460
+ # lanes can race to spawn this: the MCP's foreground KeepAlive launchd job and
461
+ # the best-effort run-overlay-watch.sh supervisor. Hold an exclusive,
462
+ # non-blocking flock for the life of the process; if another watcher already
463
+ # holds it, exit 0 quietly and let that one own the overlay. The lock fd is
464
+ # intentionally leaked (kept open) until the process dies so the OS releases
465
+ # it automatically on exit/kill.
466
+ try:
467
+ _lock_fd = os.open("/tmp/saps_overlay_watch.lock", os.O_CREAT | os.O_RDWR, 0o644)
468
+ fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
469
+ except OSError:
470
+ print("another overlay watcher already running; exiting", file=sys.stderr)
471
+ return 0
457
472
  print(f"watching {LOG_DIR}/twitter-cycle-*.log -> overlay on {CDP_URL} (Ctrl-C to stop)")
458
473
  # Treat SIGTERM (launchd unload, `kill`) like Ctrl-C so the overlay is
459
474
  # cleared on the way out instead of lingering until the next navigation.
@@ -73,6 +73,20 @@ WATCH_LOG="${SAPS_LOG_DIR}/overlay-watch.log"
73
73
  # --- spawn detached ----------------------------------------------------------
74
74
  cd "${REPO_DIR}" || exit 0
75
75
  echo "[overlay-watch] $(date '+%Y-%m-%d %H:%M:%S') starting watcher py=${PYBIN} cdp=${TWITTER_CDP_URL} log=${SAPS_LOG_DIR}" >>"${WATCH_LOG}" 2>&1
76
- nohup "${PYBIN}" "${OVERLAY_PY}" watch >>"${WATCH_LOG}" 2>&1 &
76
+ # Spawn the watcher in a NEW SESSION so it outlives this supervisor.
77
+ # When launchd fires this script (StartInterval 60), the script is the job's
78
+ # main process; the moment it exits, launchd reaps the WHOLE job process group.
79
+ # `nohup` only blocks SIGHUP, not that group SIGKILL, so a plain `nohup ... &`
80
+ # child dies the instant we `exit 0` (it survives only when this runs from an
81
+ # interactive shell, which is NOT a launchd job). macOS ships no setsid(1), so
82
+ # use the python we already resolved to os.setsid() off the launchd group, then
83
+ # exec the real watcher. The watcher's own interpreter self-heal (os.execv)
84
+ # keeps the new session.
85
+ nohup "${PYBIN}" -c 'import os, sys
86
+ try:
87
+ os.setsid()
88
+ except OSError:
89
+ pass
90
+ os.execv(sys.argv[1], sys.argv[1:])' "${PYBIN}" "${OVERLAY_PY}" watch >>"${WATCH_LOG}" 2>&1 &
77
91
  disown 2>/dev/null || true
78
92
  exit 0