social-autoposter 1.6.62 → 1.6.64
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 +16 -0
- package/mcp/dist/index.js +36 -0
- package/mcp/dist/version.json +2 -2
- package/package.json +1 -1
- package/scripts/harness_overlay.py +26 -4
- package/skill/lib/linkedin-backend.sh +36 -3
- package/skill/run-overlay-watch.sh +79 -0
package/bin/cli.js
CHANGED
|
@@ -705,6 +705,22 @@ function generatePlists() {
|
|
|
705
705
|
stdoutLog: `${DEST}/skill/logs/launchd-self-update-stdout.log`,
|
|
706
706
|
stderrLog: `${DEST}/skill/logs/launchd-self-update-stderr.log`,
|
|
707
707
|
},
|
|
708
|
+
{
|
|
709
|
+
// On-screen overlay watcher supervisor. The overlay (harness status banner
|
|
710
|
+
// + interactive draft sidebar) only renders WHILE harness_overlay.py watch
|
|
711
|
+
// runs. The supervisor is idempotent (pgrep guard), so a 60s StartInterval
|
|
712
|
+
// is a no-op while the watcher is up and re-spawns it within a minute if it
|
|
713
|
+
// ever dies. RunAtLoad starts it right after install. This is what makes the
|
|
714
|
+
// overlay appear on headless / remote installs (Lane A); the MCP covers the
|
|
715
|
+
// pure-.mcpb lane by calling the same script on draft_cycle / autopilot.
|
|
716
|
+
file: 'com.m13v.social-overlay-watch.plist',
|
|
717
|
+
label: 'com.m13v.social-overlay-watch',
|
|
718
|
+
script: `${DEST}/skill/run-overlay-watch.sh`,
|
|
719
|
+
interval: 60,
|
|
720
|
+
runAtLoad: true,
|
|
721
|
+
stdoutLog: `${DEST}/skill/logs/launchd-overlay-watch-stdout.log`,
|
|
722
|
+
stderrLog: `${DEST}/skill/logs/launchd-overlay-watch-stderr.log`,
|
|
723
|
+
},
|
|
708
724
|
];
|
|
709
725
|
|
|
710
726
|
const driver = scheduler.driverFor();
|
package/mcp/dist/index.js
CHANGED
|
@@ -222,6 +222,32 @@ function cycleProgressMessage(line) {
|
|
|
222
222
|
return `Cycle stopped (${m[1]}).`;
|
|
223
223
|
return null;
|
|
224
224
|
}
|
|
225
|
+
// Start the twitter-harness on-screen overlay watcher if it isn't already up.
|
|
226
|
+
// The overlay (status banner + interactive draft sidebar) only renders WHILE
|
|
227
|
+
// `harness_overlay.py watch` runs. The supervisor script is idempotent (pgrep
|
|
228
|
+
// guard), so calling this on every draft_cycle / autopilot-enable / show-browser
|
|
229
|
+
// is safe: it spawns at most one detached watcher and is a fast no-op otherwise.
|
|
230
|
+
//
|
|
231
|
+
// We thread SAPS_PYTHON (the owned uv runtime, so the watcher resolves a
|
|
232
|
+
// playwright-capable interpreter on Lane B / .mcpb installs that have no system
|
|
233
|
+
// python) and SAPS_LOG_DIR (the materialized repo's skill/logs, so the watcher
|
|
234
|
+
// reads the SAME cycle logs this run writes to decide busy/idle). Fire-and-forget:
|
|
235
|
+
// a failure here must never break the cycle it's decorating.
|
|
236
|
+
async function ensureOverlayWatch() {
|
|
237
|
+
try {
|
|
238
|
+
await run("bash", ["skill/run-overlay-watch.sh"], {
|
|
239
|
+
timeoutMs: 20_000,
|
|
240
|
+
env: {
|
|
241
|
+
SAPS_PYTHON: resolvePython(),
|
|
242
|
+
SAPS_LOG_DIR: path.join(repoDir(), "skill", "logs"),
|
|
243
|
+
TWITTER_CDP_URL: process.env.TWITTER_CDP_URL || "http://127.0.0.1:9555",
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* best-effort: the overlay is a nicety, never a blocker */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
225
251
|
async function produceDrafts(project, onProgress) {
|
|
226
252
|
// Run the real pipeline in DRAFT_ONLY mode: scan -> score -> draft -> link-gen,
|
|
227
253
|
// then STOP before posting. The script prints `DRAFT_ONLY_PLAN=<path>` and
|
|
@@ -240,6 +266,9 @@ async function produceDrafts(project, onProgress) {
|
|
|
240
266
|
};
|
|
241
267
|
if (project)
|
|
242
268
|
env.SAPS_FORCE_PROJECT = project;
|
|
269
|
+
// Bring the on-screen overlay up alongside the live harness window so the user
|
|
270
|
+
// watching the scan/scrape sees status + queued drafts. Idempotent + detached.
|
|
271
|
+
await ensureOverlayWatch();
|
|
243
272
|
let step = 0;
|
|
244
273
|
let lastMsg = "";
|
|
245
274
|
// ONE predictable, host-independent place to watch a draft_cycle run, so any
|
|
@@ -971,6 +1000,9 @@ tool("autopilot", {
|
|
|
971
1000
|
});
|
|
972
1001
|
}
|
|
973
1002
|
if (action === "enable") {
|
|
1003
|
+
// Bring up the on-screen overlay watcher so background autopilot cycles
|
|
1004
|
+
// still paint the harness status/sidebar. Idempotent + detached.
|
|
1005
|
+
await ensureOverlayWatch();
|
|
974
1006
|
// 1) Cycle plist. Write one pointing at the self-update guard ONLY if no
|
|
975
1007
|
// plist exists yet; never overwrite a hand-tuned/dev plist.
|
|
976
1008
|
const createdCycle = ensurePlist(TWITTER_AUTOPILOT_PLIST, plistXml({
|
|
@@ -1480,6 +1512,10 @@ tool("show_browser_to_user", {
|
|
|
1480
1512
|
}
|
|
1481
1513
|
return jsonContent({ ok: true, brought_to_front: true, port: res.port });
|
|
1482
1514
|
}
|
|
1515
|
+
// If the user is about to watch the live browser, make sure the on-screen
|
|
1516
|
+
// overlay watcher is up too so the harness window carries status + drafts.
|
|
1517
|
+
if (action === "start")
|
|
1518
|
+
await ensureOverlayWatch();
|
|
1483
1519
|
const ensured = await screencast.ensure(typeof args?.port === "number" ? args.port : undefined);
|
|
1484
1520
|
if (!ensured.ok) {
|
|
1485
1521
|
const message = ensured.error === "no_browser"
|
package/mcp/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -517,12 +517,29 @@ def _fetch_drafts(limit: int = 40) -> list:
|
|
|
517
517
|
|
|
518
518
|
# --- cycle-log -> friendly status -------------------------------------------
|
|
519
519
|
|
|
520
|
+
def _safe_mtime(p: str) -> float:
|
|
521
|
+
"""getmtime that tolerates the file vanishing mid-scan (log rotation race).
|
|
522
|
+
|
|
523
|
+
The watch loop runs forever while cycles rotate/delete logs underneath it.
|
|
524
|
+
A bare os.path.getmtime on a path that disappeared between the glob and the
|
|
525
|
+
stat raises FileNotFoundError and (previously) killed the whole watcher,
|
|
526
|
+
dropping the overlay until something restarted it. Treat a gone file as
|
|
527
|
+
infinitely old so it just loses the max() race instead of crashing.
|
|
528
|
+
"""
|
|
529
|
+
try:
|
|
530
|
+
return os.path.getmtime(p)
|
|
531
|
+
except OSError:
|
|
532
|
+
return 0.0
|
|
533
|
+
|
|
534
|
+
|
|
520
535
|
def _latest_cycle_log() -> Path | None:
|
|
521
536
|
files = glob.glob(str(LOG_DIR / "twitter-cycle-*.log"))
|
|
522
537
|
if not files:
|
|
523
538
|
return None
|
|
524
|
-
newest = max(files, key=
|
|
525
|
-
|
|
539
|
+
newest = max(files, key=_safe_mtime)
|
|
540
|
+
# The winner could STILL have been deleted between selection and use; the
|
|
541
|
+
# caller (_current_status) stats it again, so hand back None if it's gone.
|
|
542
|
+
return Path(newest) if os.path.exists(newest) else None
|
|
526
543
|
|
|
527
544
|
|
|
528
545
|
_RE_SCAN = re.compile(r"project='([^']+)'\s+q=(['\"])(.*?)\2\s+kept=(\d+)")
|
|
@@ -575,7 +592,7 @@ def _current_status() -> str:
|
|
|
575
592
|
log = _latest_cycle_log()
|
|
576
593
|
if not log:
|
|
577
594
|
return "Idle \u2014 waiting for the next cycle\u2026"
|
|
578
|
-
age = time.time() -
|
|
595
|
+
age = time.time() - _safe_mtime(str(log))
|
|
579
596
|
if age > IDLE_AFTER_SEC:
|
|
580
597
|
return "Idle \u2014 waiting for the next cycle\u2026"
|
|
581
598
|
return _tail_last_meaningful(log) or "Working\u2026"
|
|
@@ -638,7 +655,12 @@ def cmd_watch(interval: float = 2.0) -> int:
|
|
|
638
655
|
last_sb_sig = None
|
|
639
656
|
try:
|
|
640
657
|
while True:
|
|
641
|
-
status
|
|
658
|
+
# Never let status computation (log globbing/stat, all racing against
|
|
659
|
+
# live log rotation) kill the watcher; fall back to a neutral status.
|
|
660
|
+
try:
|
|
661
|
+
status = _current_status()
|
|
662
|
+
except Exception:
|
|
663
|
+
status = "Working\u2026"
|
|
642
664
|
try:
|
|
643
665
|
if h is None:
|
|
644
666
|
h = Harness().__enter__()
|
|
@@ -192,24 +192,57 @@ _acquire_linkedin_pipeline_lock() {
|
|
|
192
192
|
return 0
|
|
193
193
|
fi
|
|
194
194
|
local _who="${SAPS_PIPELINE_NAME:-$(basename "${0:-linkedin-pipeline}")}"
|
|
195
|
+
# WAIT, don't bail. Aligned 2026-06-04 with Twitter's skill/lock.sh::acquire_lock
|
|
196
|
+
# so an overlapping LinkedIn fire (e.g. stats-linkedin's :23 slot landing on
|
|
197
|
+
# top of run-linkedin's :14 post still finishing) queues for the 9556 Chrome
|
|
198
|
+
# instead of `exit 0`-skipping. Previously the live-holder branch bailed, which
|
|
199
|
+
# systematically starved stats-linkedin every time its slot collided with the
|
|
200
|
+
# ~9min post pipeline, so comment-stats never refreshed. Bounded by a timeout
|
|
201
|
+
# (default 1h) and a 3h lock-age safety net, matching lock.sh.
|
|
202
|
+
local _waited=0
|
|
203
|
+
local _timeout="${SAPS_LINKEDIN_PIPELINE_LOCK_TIMEOUT:-3600}"
|
|
204
|
+
local _logged_wait=false
|
|
195
205
|
while : ; do
|
|
196
206
|
if mkdir "$_LI_PIPELINE_LOCK_DIR" 2>/dev/null; then
|
|
197
207
|
echo "$$" > "$_LI_PIPELINE_LOCK_DIR/pid"
|
|
198
208
|
echo "$_who" > "$_LI_PIPELINE_LOCK_DIR/holder"
|
|
199
209
|
export _LI_PIPELINE_LOCK_HELD=1
|
|
200
|
-
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock ACQUIRED by $_who (pid $$)" >&2
|
|
210
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock ACQUIRED by $_who (pid $$) waited=${_waited}s" >&2
|
|
201
211
|
return 0
|
|
202
212
|
fi
|
|
203
213
|
local _h_pid _h_who
|
|
204
214
|
_h_pid="$(cat "$_LI_PIPELINE_LOCK_DIR/pid" 2>/dev/null || echo "")"
|
|
205
215
|
_h_who="$(cat "$_LI_PIPELINE_LOCK_DIR/holder" 2>/dev/null || echo "?")"
|
|
216
|
+
# Reclaim a stale lock left by a dead holder (no release trap by design).
|
|
206
217
|
if [ -z "$_h_pid" ] || ! kill -0 "$_h_pid" 2>/dev/null; then
|
|
207
218
|
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: reclaiming stale lock (dead holder ${_h_who} pid ${_h_pid:-unknown})" >&2
|
|
208
219
|
rm -rf "$_LI_PIPELINE_LOCK_DIR"
|
|
209
220
|
continue
|
|
210
221
|
fi
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
# Safety net: reclaim any lock older than 3h regardless of holder liveness.
|
|
223
|
+
# watchdog_hung_runs.py SIGTERMs a hung holder long before this fires;
|
|
224
|
+
# mirrors the 10800s ceiling in lock.sh::acquire_lock.
|
|
225
|
+
if [ -d "$_LI_PIPELINE_LOCK_DIR" ]; then
|
|
226
|
+
local _lock_age
|
|
227
|
+
_lock_age=$(( $(date +%s) - $(stat -f %m "$_LI_PIPELINE_LOCK_DIR" 2>/dev/null || stat -c %Y "$_LI_PIPELINE_LOCK_DIR" 2>/dev/null || date +%s) ))
|
|
228
|
+
if [ "$_lock_age" -gt 10800 ]; then
|
|
229
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: removing lock older than 3h (age ${_lock_age}s, holder ${_h_who})" >&2
|
|
230
|
+
rm -rf "$_LI_PIPELINE_LOCK_DIR"
|
|
231
|
+
continue
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
# Timed out waiting -> skip this fire (launchd will re-fire next slot).
|
|
235
|
+
if [ "$_waited" -ge "$_timeout" ]; then
|
|
236
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: still held by ${_h_who} (pid ${_h_pid}) after $((_timeout/60))min; ${_who} skipping this fire" >&2
|
|
237
|
+
exit 0
|
|
238
|
+
fi
|
|
239
|
+
# Surface the holder once when we first start waiting.
|
|
240
|
+
if [ "$_logged_wait" = "false" ]; then
|
|
241
|
+
echo "[$(date +%H:%M:%S)] linkedin-pipeline lock: held by ${_h_who} (pid ${_h_pid}); ${_who} waiting (timeout $((_timeout/60))min)..." >&2
|
|
242
|
+
_logged_wait=true
|
|
243
|
+
fi
|
|
244
|
+
sleep 2
|
|
245
|
+
_waited=$((_waited + 2))
|
|
213
246
|
done
|
|
214
247
|
}
|
|
215
248
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Idempotent supervisor for the twitter-harness on-screen overlay watcher.
|
|
3
|
+
#
|
|
4
|
+
# WHAT: keeps exactly ONE `harness_overlay.py watch` process alive. That watcher
|
|
5
|
+
# injects the status overlay + interactive draft sidebar into the twitter-harness
|
|
6
|
+
# Chrome window so a human watching the harness sees what the pipeline is doing
|
|
7
|
+
# and can preview queued drafts.
|
|
8
|
+
#
|
|
9
|
+
# WHY a supervisor: the overlay only renders WHILE the watch process runs. It was
|
|
10
|
+
# previously a manual, local-only process, so it never appeared on headless /
|
|
11
|
+
# remote installs. This script makes it self-starting from BOTH install lanes:
|
|
12
|
+
# - Lane A (npm/cli): launchd job `com.m13v.social-overlay-watch` (StartInterval
|
|
13
|
+
# 60 + RunAtLoad) re-invokes this script every minute; the pgrep guard makes
|
|
14
|
+
# re-invocation a no-op while the watcher is already up.
|
|
15
|
+
# - Lane B (.mcpb / pure MCP): the MCP calls this script on draft_cycle /
|
|
16
|
+
# autopilot-enable / show_browser_to_user, threading SAPS_PYTHON + SAPS_LOG_DIR.
|
|
17
|
+
#
|
|
18
|
+
# IDEMPOTENT: safe to call on a 60s timer. If a watcher is already running it
|
|
19
|
+
# exits 0 immediately. Otherwise it spawns one detached (nohup, own session) and
|
|
20
|
+
# returns; the spawned watcher then runs until the machine/MCP tears it down.
|
|
21
|
+
#
|
|
22
|
+
# This script is intentionally NOT locked: the overlay UX is expected to evolve.
|
|
23
|
+
|
|
24
|
+
set -u
|
|
25
|
+
|
|
26
|
+
# --- resolve the repo this script lives in (works from launchd + MCP cwd) -----
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
29
|
+
OVERLAY_PY="${REPO_DIR}/scripts/harness_overlay.py"
|
|
30
|
+
|
|
31
|
+
if [ ! -f "${OVERLAY_PY}" ]; then
|
|
32
|
+
echo "[overlay-watch] harness_overlay.py not found at ${OVERLAY_PY}; nothing to do" >&2
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# --- idempotency guard: at most one watcher, ever ----------------------------
|
|
37
|
+
# Match the full `harness_overlay.py watch` invocation (NOT a bare `grep -r`, so
|
|
38
|
+
# this never trips the BSD-grep-on-FIFO hang noted in the repo CLAUDE.md).
|
|
39
|
+
if pgrep -f "harness_overlay.py watch" >/dev/null 2>&1; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# --- resolve a python interpreter --------------------------------------------
|
|
44
|
+
# harness_overlay.py self-heals to a playwright-capable interpreter on its own
|
|
45
|
+
# (see _ensure_playwright_interpreter), so any python3 that exists is fine to
|
|
46
|
+
# launch with. Prefer the MCP-threaded SAPS_PYTHON (owned uv runtime), then the
|
|
47
|
+
# usual suspects.
|
|
48
|
+
PYBIN=""
|
|
49
|
+
for _cand in \
|
|
50
|
+
"${SAPS_PYTHON:-}" \
|
|
51
|
+
"/opt/homebrew/bin/python3.11" \
|
|
52
|
+
"/usr/bin/python3" \
|
|
53
|
+
"/opt/homebrew/bin/python3"
|
|
54
|
+
do
|
|
55
|
+
if [ -n "${_cand}" ] && [ -x "${_cand}" ]; then PYBIN="${_cand}"; break; fi
|
|
56
|
+
done
|
|
57
|
+
if [ -z "${PYBIN}" ]; then
|
|
58
|
+
PYBIN="$(command -v python3 2>/dev/null || true)"
|
|
59
|
+
fi
|
|
60
|
+
if [ -z "${PYBIN}" ]; then
|
|
61
|
+
echo "[overlay-watch] no python3 found; cannot start overlay watcher" >&2
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# --- env the watcher needs ---------------------------------------------------
|
|
66
|
+
# SAPS_LOG_DIR: where harness_overlay.py reads cycle logs to decide busy/idle.
|
|
67
|
+
# Default to this repo's skill/logs (MCP overrides it to the materialized repo).
|
|
68
|
+
export SAPS_LOG_DIR="${SAPS_LOG_DIR:-${REPO_DIR}/skill/logs}"
|
|
69
|
+
# CDP target for the twitter harness Chrome (honor BYO-Chrome installs).
|
|
70
|
+
export TWITTER_CDP_URL="${TWITTER_CDP_URL:-http://127.0.0.1:9555}"
|
|
71
|
+
mkdir -p "${SAPS_LOG_DIR}" 2>/dev/null || true
|
|
72
|
+
WATCH_LOG="${SAPS_LOG_DIR}/overlay-watch.log"
|
|
73
|
+
|
|
74
|
+
# --- spawn detached ----------------------------------------------------------
|
|
75
|
+
cd "${REPO_DIR}" || exit 0
|
|
76
|
+
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
|
|
77
|
+
nohup "${PYBIN}" "${OVERLAY_PY}" watch >>"${WATCH_LOG}" 2>&1 &
|
|
78
|
+
disown 2>/dev/null || true
|
|
79
|
+
exit 0
|