social-autoposter 1.6.154 → 1.6.156
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/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.6.
|
|
5
|
+
"version": "1.6.156",
|
|
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 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/menubar/s4l_state.py
CHANGED
|
@@ -22,6 +22,7 @@ import os
|
|
|
22
22
|
import subprocess
|
|
23
23
|
import sys
|
|
24
24
|
import threading
|
|
25
|
+
import time
|
|
25
26
|
import urllib.request
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
@@ -195,32 +196,58 @@ def loopback_tool(name: str, args=None, timeout: float = 20.0):
|
|
|
195
196
|
|
|
196
197
|
|
|
197
198
|
# ---- the snapshot the menu bar renders ------------------------------------
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
199
|
+
# Background snapshot cache. scripts/snapshot.py reads files but may spawn the
|
|
200
|
+
# X-status subprocess (setup_twitter_auth.py -> CDP to Chrome), which must NEVER
|
|
201
|
+
# run on the menu bar's UI thread — a hung Chrome would freeze the menu. So a
|
|
202
|
+
# daemon thread recomputes and snapshot() returns the last cached value INSTANTLY.
|
|
203
|
+
_snap_cache = {"val": None, "at": 0.0}
|
|
204
|
+
_snap_lock = threading.Lock()
|
|
205
|
+
_snap_refreshing = [False]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _compute_snapshot_full():
|
|
209
|
+
repo = os.environ.get("SAPS_REPO_DIR") or str(Path.home() / "social-autoposter")
|
|
210
|
+
scripts = os.path.join(repo, "scripts")
|
|
211
|
+
if scripts not in sys.path:
|
|
212
|
+
sys.path.insert(0, scripts)
|
|
213
|
+
import snapshot as _snapshot_mod # scripts/snapshot.py
|
|
214
|
+
return _snapshot_mod.compute()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _refresh_snapshot_bg():
|
|
212
218
|
try:
|
|
213
|
-
|
|
214
|
-
scripts = os.path.join(repo, "scripts")
|
|
215
|
-
if scripts not in sys.path:
|
|
216
|
-
sys.path.insert(0, scripts)
|
|
217
|
-
import snapshot as _snapshot_mod # scripts/snapshot.py
|
|
218
|
-
snap = _snapshot_mod.compute()
|
|
219
|
+
snap = _compute_snapshot_full()
|
|
219
220
|
if isinstance(snap, dict) and "projects_total" in snap:
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
with _snap_lock:
|
|
222
|
+
_snap_cache["val"] = snap
|
|
223
|
+
_snap_cache["at"] = time.time()
|
|
222
224
|
except Exception:
|
|
223
225
|
pass
|
|
226
|
+
finally:
|
|
227
|
+
_snap_refreshing[0] = False
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def snapshot():
|
|
231
|
+
"""Full snapshot computed DIRECTLY from the stateful files via
|
|
232
|
+
scripts/snapshot.py — the SAME single-source module the MCP shells out to, so
|
|
233
|
+
the two surfaces can't diverge. NO loopback / MCP dependency, so a restarting
|
|
234
|
+
or closed Claude can't freeze or stale the menu (the old tier-1 `loopback_tool`
|
|
235
|
+
blocked the UI thread up to 20s and was the freeze). The heavy compute runs on
|
|
236
|
+
a BACKGROUND thread; this returns the last cached result instantly.
|
|
237
|
+
|
|
238
|
+
Tiers: (1) the background-computed local snapshot; (2) the server's last
|
|
239
|
+
persisted `status-summary.json`; (3) the onboarding ledger."""
|
|
240
|
+
now = time.time()
|
|
241
|
+
with _snap_lock:
|
|
242
|
+
cached = _snap_cache["val"]
|
|
243
|
+
age = now - _snap_cache["at"]
|
|
244
|
+
if (cached is None or age > 4.0) and not _snap_refreshing[0]:
|
|
245
|
+
_snap_refreshing[0] = True
|
|
246
|
+
threading.Thread(target=_refresh_snapshot_bg, daemon=True).start()
|
|
247
|
+
if cached is not None:
|
|
248
|
+
out = dict(cached)
|
|
249
|
+
out["_live"] = True
|
|
250
|
+
return out
|
|
224
251
|
summ = read_json("status-summary.json")
|
|
225
252
|
if isinstance(summ, dict) and "projects_total" in summ:
|
|
226
253
|
summ["_live"] = False
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/social-autoposter-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.156",
|
|
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
|
@@ -249,14 +249,29 @@ def main() -> int:
|
|
|
249
249
|
dry = "--dry-run" in sys.argv
|
|
250
250
|
max_age = _env_int("SAPS_REAPER_MAX_AGE_SEC", DEFAULT_MAX_AGE_SEC)
|
|
251
251
|
# (1) Queue-correlated reaping knobs.
|
|
252
|
-
# grace: how long an UNCLAIMED session may live before it
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
|
|
252
|
+
# grace: how long an UNCLAIMED session may live before its AGE makes it reapable.
|
|
253
|
+
#
|
|
254
|
+
# 2026-06-29, second pass: this used to default to 90s, and the queue-readable
|
|
255
|
+
# branch below used it as the ONLY age gate -- which meant a separate, short,
|
|
256
|
+
# activity-BLIND timer governed normal operation and silently overrode the 35-min
|
|
257
|
+
# `max_age` ceiling that the whole top-of-file rationale is built around (producer
|
|
258
|
+
# deadline 1800s + margin). An actively-DRAFTING session ages out of the
|
|
259
|
+
# "inflight+margin newest" window after ~2 min as fresh empty workers spawn on top
|
|
260
|
+
# of it, then got SIGTERMed at 90s mid-draft -> the mysterious "~120s code-143
|
|
261
|
+
# kill". Raising it to 300s only moved the cliff.
|
|
262
|
+
#
|
|
263
|
+
# The fix is to stop having two timers. There is now ONE overall age ceiling
|
|
264
|
+
# (`max_age`, 35 min = producer deadline + margin): past it the producer has
|
|
265
|
+
# already discarded the worker's result, so the session is provably useless and
|
|
266
|
+
# safe to reap whether or not it ever claimed. `grace` therefore defaults to
|
|
267
|
+
# `max_age` -- the queue-readable branch is no longer allowed to preempt a session
|
|
268
|
+
# earlier than the producer's own deadline. What keeps MEMORY bounded instead of a
|
|
269
|
+
# short timer is (a) claim-holders are spared outright via running_claim_pids() and
|
|
270
|
+
# (b) the count-cap (max_group) reaps the oldest-beyond-N by COUNT, regardless of
|
|
271
|
+
# age, and never touches a claim-holder. Override only to deliberately re-introduce
|
|
272
|
+
# an earlier age gate (not recommended); it can never exceed max_age in effect.
|
|
273
|
+
grace = _env_int("SAPS_REAPER_GRACE_SEC", DEFAULT_MAX_AGE_SEC) # = max_age: one ceiling
|
|
274
|
+
grace = min(grace, max_age) # an override may only shorten, never outlive the ceiling
|
|
260
275
|
keep_margin = _env_int("SAPS_REAPER_KEEP_MARGIN", 1) # extra newest spared beyond busy set
|
|
261
276
|
# (2) Count-cap backstop: never let one uuid group hold more than this many live
|
|
262
277
|
# workers, regardless of queue state. 0 disables. The default rarely fires once
|
|
@@ -21,15 +21,32 @@ HB_PID="" # scan-phase heartbeat (started below); torn down by the EXIT trap
|
|
|
21
21
|
# the cycle's exit code.
|
|
22
22
|
trap 'kill "$HB_PID" 2>/dev/null || true; rm -f "$OUT"; "$PY" "$REPO_DIR/scripts/saps_activity.py" clear 2>/dev/null || true' EXIT
|
|
23
23
|
|
|
24
|
-
# Narrate the scan phase. The CDP scan runs inside the (locked)
|
|
25
|
-
# which has no activity writer; this covers that window until
|
|
26
|
-
# flips the label to "finding threads"/"drafting replies"
|
|
24
|
+
# Narrate the scan phase, GRANULARLY. The CDP scan runs inside the (locked)
|
|
25
|
+
# run-twitter-cycle.sh which has no activity writer; this covers that window until
|
|
26
|
+
# the queue provider flips the label to "finding threads"/"drafting replies".
|
|
27
|
+
# Instead of a frozen "scanning X for threads" for the whole multi-minute scan,
|
|
28
|
+
# each heartbeat recomputes elapsed and scrapes THIS cycle's own stdout ($OUT, the
|
|
29
|
+
# tee target below) for live progress — queries run, and candidates found once
|
|
30
|
+
# Phase 1 reports them — so the menu bar actually moves. Reads $OUT only; never
|
|
31
|
+
# touches the locked cycle. heartbeat() re-stamps ONLY while the state is still
|
|
32
|
+
# "scanning", so once the provider advances the phase it goes quiet (no flicker).
|
|
27
33
|
"$PY" "$REPO_DIR/scripts/saps_activity.py" write scanning "scanning X for threads" 2>/dev/null || true
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
(
|
|
34
|
+
SCAN_T0=$(date +%s)
|
|
35
|
+
(
|
|
36
|
+
while true; do
|
|
37
|
+
sleep 20
|
|
38
|
+
_el=$(( $(date +%s) - SCAN_T0 ))
|
|
39
|
+
if [ "$_el" -lt 60 ]; then _dur="${_el}s"; else _dur="$(( _el / 60 ))m"; fi
|
|
40
|
+
_q=$(grep -c "kept=" "$OUT" 2>/dev/null || true); _q=${_q:-0}
|
|
41
|
+
_found=$(grep -oE "Batch has [0-9]+" "$OUT" 2>/dev/null | tail -1 | grep -oE "[0-9]+" | tail -1 || true)
|
|
42
|
+
if [ -n "$_found" ]; then
|
|
43
|
+
_lbl="scanning X for threads (${_dur} · ${_q} queries, ${_found} found)"
|
|
44
|
+
else
|
|
45
|
+
_lbl="scanning X for threads (${_dur} · ${_q} queries)"
|
|
46
|
+
fi
|
|
47
|
+
"$PY" "$REPO_DIR/scripts/saps_activity.py" heartbeat scanning "$_lbl" 2>/dev/null || true
|
|
48
|
+
done
|
|
49
|
+
) &
|
|
33
50
|
HB_PID=$!
|
|
34
51
|
|
|
35
52
|
# Engagement mode (2026-06-26). The menu-bar toggle writes mode.json; this reads
|