@m13v/s4l 1.7.5-rc.30 → 1.7.5-rc.32
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 +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/merge_review_queue.py +11 -0
- package/skill/lib/harness-common.sh +19 -1
- package/skill/run-reddit-search.sh +17 -2
- package/skill/run-reddit-threads.sh +16 -2
- package/skill/run-twitter-cycle.sh +47 -29
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.7.5-rc.
|
|
5
|
+
"version": "1.7.5-rc.32",
|
|
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.5-rc.
|
|
3
|
+
"version": "1.7.5-rc.32",
|
|
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
|
@@ -107,6 +107,17 @@ def _atomic_write(path: str, obj) -> None:
|
|
|
107
107
|
|
|
108
108
|
def _dedup_key(c: dict) -> str:
|
|
109
109
|
"""Match submit_drafts: dedup by the thread/candidate URL, else candidate_id."""
|
|
110
|
+
# Sandbox cards (experiments.sandbox=true) replay real historical threads
|
|
111
|
+
# ON PURPOSE, including ones already dealt with for real -- that's the
|
|
112
|
+
# whole point of testing against past data. Namespace their key so they
|
|
113
|
+
# never collide with (and get silently swallowed by) a real card for the
|
|
114
|
+
# same URL, and keep the reply text IN the key so two different prompt
|
|
115
|
+
# variants drafting the same thread both surface as distinct cards
|
|
116
|
+
# instead of the second dropping as "already seen" (identical reruns
|
|
117
|
+
# still naturally dedupe against each other, which is fine).
|
|
118
|
+
if (c.get("experiments") or {}).get("sandbox"):
|
|
119
|
+
url = c.get("candidate_url") or c.get("tweet_url") or c.get("thread_url") or ""
|
|
120
|
+
return f"sandbox:{url}:{(c.get('reply_text') or '')[:80]}"
|
|
110
121
|
for k in ("candidate_url", "tweet_url", "thread_url", "candidate_id"):
|
|
111
122
|
v = c.get(k)
|
|
112
123
|
if v:
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
# harness-common.sh - the shared engine behind every per-platform browser
|
|
3
3
|
# backend (twitter-backend.sh, reddit-backend.sh, linkedin-backend.sh).
|
|
4
4
|
#
|
|
5
|
+
# LOCAL OVERRIDES: ~/.social-autoposter-mcp/harness-local.env is sourced (when
|
|
6
|
+
# present) before any launch decision. It is machine-local (never shipped),
|
|
7
|
+
# for per-box experiments without a release: e.g. pointing one operator box's
|
|
8
|
+
# harness at Chrome Beta (BH_CHROME_BIN=...) or adding launch flags
|
|
9
|
+
# (HC_EXTRA_FLAGS="--no-startup-window") while every other install keeps
|
|
10
|
+
# stock behavior. Values here are plain shell assignments; keep it tiny.
|
|
11
|
+
# shellcheck disable=SC1090
|
|
12
|
+
[ -f "$HOME/.social-autoposter-mcp/harness-local.env" ] && . "$HOME/.social-autoposter-mcp/harness-local.env" 2>/dev/null || true
|
|
13
|
+
#
|
|
5
14
|
# Born 2026-07-14 from the three near-duplicate backends. Every hard-won fix
|
|
6
15
|
# used to land in ONE backend and drift: the CDP wedge detector, the two-strike
|
|
7
16
|
# tolerance, the focus-safe launcher, the occlusion flags, and the repo-relative
|
|
@@ -287,6 +296,15 @@ hc_ensure_browser() {
|
|
|
287
296
|
# 2026-07-14/15 EXC_BREAKPOINT (deliberate internal abort) crash dumps
|
|
288
297
|
# arriving ~once per cycle with zero jetsam/OOM evidence. Playwright
|
|
289
298
|
# and Puppeteer both launch with it for exactly this reason.
|
|
299
|
+
# With --no-startup-window (focus-steal prevention: Chrome creates NO
|
|
300
|
+
# window at launch, so macOS has nothing to activate; the first
|
|
301
|
+
# pipeline op mints its tab via the background Target.createTarget
|
|
302
|
+
# path) a positional URL argument would force a window and defeat the
|
|
303
|
+
# flag — drop the launch URL in that mode.
|
|
304
|
+
local _launch_url="${HC_LAUNCH_URL:-about:blank}"
|
|
305
|
+
case " ${HC_EXTRA_FLAGS:-} " in
|
|
306
|
+
*" --no-startup-window "*) _launch_url="" ;;
|
|
307
|
+
esac
|
|
290
308
|
launch_harness_chrome "$_chrome_bin" "$_prof" \
|
|
291
309
|
--remote-debugging-port="$_port" \
|
|
292
310
|
--user-data-dir="$_prof" \
|
|
@@ -296,7 +314,7 @@ hc_ensure_browser() {
|
|
|
296
314
|
--disable-backgrounding-occluded-windows \
|
|
297
315
|
${HC_EXTRA_FLAGS:-} \
|
|
298
316
|
${_os_extra} \
|
|
299
|
-
|
|
317
|
+
${_launch_url}
|
|
300
318
|
for _i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
|
301
319
|
curl -sf --max-time 2 -o /dev/null "${_def_url}/json/version" 2>/dev/null && break
|
|
302
320
|
sleep 1
|
|
@@ -284,8 +284,23 @@ python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 60 --ttl 30
|
|
|
284
284
|
# old ensure_browser_healthy "reddit" ps-scan-for-MCP-Chrome path; the harness is
|
|
285
285
|
# the single browser the whole Reddit pipeline now rides (REDDIT_CDP_URL points
|
|
286
286
|
# every child Python proc at it). Falls back to ensure_browser_healthy on failure.
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE" || true
|
|
288
|
+
_ensure_rc="${PIPESTATUS[0]}"
|
|
289
|
+
if [ "$_ensure_rc" = "78" ]; then
|
|
290
|
+
# Reserved skip code (harness-common.sh:182-185): the pre-launch hook
|
|
291
|
+
# deliberately deferred because a poster is mid-drain on the ONE shared
|
|
292
|
+
# tab. This is NOT a bootstrap failure — falling back to
|
|
293
|
+
# ensure_browser_healthy here (as this used to, treating ANY nonzero rc
|
|
294
|
+
# as broken) is wrong on two counts: it misreports a clean defer as a
|
|
295
|
+
# warning, and ensure_browser_healthy checks the pre-harness
|
|
296
|
+
# browser-profiles/reddit path (dead since the 2026-05-29 migration), so
|
|
297
|
+
# it is pure wasted work every time cycles overlap around an active post
|
|
298
|
+
# (routine — see the "double-fork wrapper" note above BATCH_ID).
|
|
299
|
+
log "reddit-harness bootstrap deferred (rc=78, poster active elsewhere); skipping this cycle so the poster keeps the tab."
|
|
300
|
+
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
301
|
+
exit 0
|
|
302
|
+
elif [ "$_ensure_rc" != "0" ]; then
|
|
303
|
+
log "WARNING: reddit-harness bootstrap failed (rc=$_ensure_rc); falling back to ensure_browser_healthy reddit"
|
|
289
304
|
ensure_browser_healthy "reddit"
|
|
290
305
|
fi
|
|
291
306
|
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" release 2>/dev/null || true
|
|
@@ -256,8 +256,22 @@ export CLAUDE_SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
|
|
|
256
256
|
log "Acquiring reddit-browser lease (TTL 90s, MCP-proxy heartbeated)..."
|
|
257
257
|
python3 "$REPO_DIR/scripts/reddit_browser_lock.py" acquire --timeout 600 --ttl 90 2>&1 | tee -a "$LOG_FILE" || \
|
|
258
258
|
log "WARNING: reddit_browser_lock.py acquire failed; proceeding without lease (peer pipelines may collide)."
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
ensure_reddit_browser_for_backend 2>&1 | tee -a "$LOG_FILE" || true
|
|
260
|
+
_ensure_rc="${PIPESTATUS[0]}"
|
|
261
|
+
if [ "$_ensure_rc" = "78" ]; then
|
|
262
|
+
# Reserved skip code (harness-common.sh:182-185): the pre-launch hook
|
|
263
|
+
# deliberately deferred because a poster is mid-drain on the ONE shared
|
|
264
|
+
# tab. This is NOT a bootstrap failure — falling back to the legacy
|
|
265
|
+
# ensure_browser_healthy here (as this used to, treating ANY nonzero rc
|
|
266
|
+
# as broken) is wrong on two counts: it misreports a clean defer as a
|
|
267
|
+
# warning, and ensure_browser_healthy checks the pre-harness
|
|
268
|
+
# browser-profiles/reddit path (dead since the 2026-05-29 migration), so
|
|
269
|
+
# it is pure wasted work every time cycles overlap around an active post
|
|
270
|
+
# (routine, see the "double-fork wrapper" note in run-reddit-search.sh).
|
|
271
|
+
log "reddit-harness bootstrap deferred (rc=78, poster active elsewhere); skipping this run so the poster keeps the tab."
|
|
272
|
+
exit 0
|
|
273
|
+
elif [ "$_ensure_rc" != "0" ]; then
|
|
274
|
+
log "WARNING: reddit-harness bootstrap failed (rc=$_ensure_rc); falling back to ensure_browser_healthy reddit"
|
|
261
275
|
ensure_browser_healthy "reddit"
|
|
262
276
|
fi
|
|
263
277
|
|
|
@@ -2205,23 +2205,29 @@ SKIP_FILE="/tmp/twitter_cycle_skips_${BATCH_ID}.json"
|
|
|
2205
2205
|
if [ -z "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
|
|
2206
2206
|
python3 "$REPO_DIR/scripts/twitter_batch_phase.py" advance "$BATCH_ID" --phase phase2b-prep 2>&1 | tee -a "$LOG_FILE" || true
|
|
2207
2207
|
fi
|
|
2208
|
-
log "Re-acquiring twitter-browser lock for Phase 2b-prep (read+draft only)..."
|
|
2209
|
-
acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
|
|
2210
|
-
log "twitter-browser lock held (pid=$$) Phase 2b-prep"
|
|
2211
|
-
# Drop stale singleton locks (see clean_stale_singleton.sh, also called in Phase 1).
|
|
2212
|
-
ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
2213
|
-
|
|
2214
2208
|
# Thread-media capture (2026-06-03, gated by S4L_TWITTER_CAPTURE_MEDIA, default
|
|
2215
|
-
# OFF).
|
|
2216
|
-
#
|
|
2217
|
-
# (images/videos/GIFs/link-cards), persist each into
|
|
2209
|
+
# OFF). When enabled, does ONE cheap deterministic pass over every candidate
|
|
2210
|
+
# thread to pull its media (images/videos/GIFs/link-cards), persist each into
|
|
2218
2211
|
# twitter_candidates.thread_media, and build a MEDIA CONTEXT block injected into
|
|
2219
2212
|
# the prep prompt so the reply-writer can react to what the tweet visually shows
|
|
2220
2213
|
# instead of replying text-blind. Must be deterministic (Python pre-fetch) because
|
|
2221
2214
|
# the prep prompt forbids the model from calling twitter_browser.py. Entirely
|
|
2222
2215
|
# best-effort: any failure leaves MEDIA_BLOCK empty and the cycle proceeds.
|
|
2216
|
+
#
|
|
2217
|
+
# The twitter-browser lock exists SOLELY to guard this capture step, so it is
|
|
2218
|
+
# now gated by the same flag (2026-07-16): acquiring it unconditionally meant
|
|
2219
|
+
# every cycle -- sandbox or live -- contended for the browser lock even when
|
|
2220
|
+
# nothing in this phase touches the browser, which is the default state (media
|
|
2221
|
+
# capture off). Found live: a sandbox test run waited ~6 minutes in the FIFO
|
|
2222
|
+
# lock queue behind a real cycle for a lock it never ended up needing.
|
|
2223
2223
|
MEDIA_BLOCK=""
|
|
2224
2224
|
if [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA:-}" = "true" ]; then
|
|
2225
|
+
log "Re-acquiring twitter-browser lock for Phase 2b-prep (read+draft only)..."
|
|
2226
|
+
acquire_lock "twitter-browser" 3600 2>>"$LOG_FILE"
|
|
2227
|
+
log "twitter-browser lock held (pid=$$) Phase 2b-prep"
|
|
2228
|
+
# Drop stale singleton locks (see clean_stale_singleton.sh, also called in Phase 1).
|
|
2229
|
+
ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
2230
|
+
|
|
2225
2231
|
if [ -s "$MEDIA_URLS_FILE" ]; then
|
|
2226
2232
|
log "Phase 2b-prep: capturing thread media for $(wc -l < "$MEDIA_URLS_FILE" | tr -d ' ') candidate(s)..."
|
|
2227
2233
|
MEDIA_BLOCK=$(python3 "$REPO_DIR/scripts/capture_thread_media.py" --urls-file "$MEDIA_URLS_FILE" --scroll 1 2>>"$LOG_FILE" || true)
|
|
@@ -2231,28 +2237,29 @@ if [ "${S4L_TWITTER_CAPTURE_MEDIA:-0}" = "1" ] || [ "${S4L_TWITTER_CAPTURE_MEDIA
|
|
|
2231
2237
|
log "Phase 2b-prep: no media captured (none found or capture skipped)."
|
|
2232
2238
|
fi
|
|
2233
2239
|
fi
|
|
2240
|
+
rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
|
|
2241
|
+
|
|
2242
|
+
# Release the twitter-browser lock now. Thread-media capture above was the
|
|
2243
|
+
# ONLY browser-touching step in Phase 2b-prep; the Claude drafting call below
|
|
2244
|
+
# is architecturally browser-free (--strict-mcp-config omits the
|
|
2245
|
+
# twitter-harness MCP, so the model can never reach the CDP Chrome even if it
|
|
2246
|
+
# tried) and, since the 2026-06-23 queue migration, "run-twitter-cycle-prep"
|
|
2247
|
+
# routes through claude_job.py to an independent worker process that can
|
|
2248
|
+
# block for up to S4L_CLAUDE_QUEUE_TIMEOUT (1800s default). Holding the
|
|
2249
|
+
# browser lock across that wait made this process a preemption target for
|
|
2250
|
+
# any post that needed the browser: the post-vs-scan hijack fix SIGKILLs
|
|
2251
|
+
# whoever holds the lock (see docs/twitter_browser_lock.md), which killed
|
|
2252
|
+
# this process mid-wait while the worker kept drafting in the background,
|
|
2253
|
+
# stranding the finished result until the salvage reconciler's 35-min window
|
|
2254
|
+
# (scripts/salvage_orphaned_prep_results.py) picked it up. Releasing here
|
|
2255
|
+
# removes this whole phase from being a SIGKILL target for the rest of the
|
|
2256
|
+
# drafting wait. Phase 2b-post re-acquires unconditionally below.
|
|
2257
|
+
log "Releasing twitter-browser lock before Claude drafting (drafting never touches the browser)..."
|
|
2258
|
+
release_lock "twitter-browser" 2>>"$LOG_FILE"
|
|
2234
2259
|
else
|
|
2235
|
-
log "Phase 2b-prep: thread-media capture disabled (S4L_TWITTER_CAPTURE_MEDIA not set)."
|
|
2260
|
+
log "Phase 2b-prep: thread-media capture disabled (S4L_TWITTER_CAPTURE_MEDIA not set); skipping the twitter-browser lock entirely."
|
|
2261
|
+
rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
|
|
2236
2262
|
fi
|
|
2237
|
-
rm -f "$MEDIA_URLS_FILE" 2>/dev/null || true
|
|
2238
|
-
|
|
2239
|
-
# Release the twitter-browser lock now. Thread-media capture above was the
|
|
2240
|
-
# ONLY browser-touching step in Phase 2b-prep; the Claude drafting call below
|
|
2241
|
-
# is architecturally browser-free (--strict-mcp-config omits the
|
|
2242
|
-
# twitter-harness MCP, so the model can never reach the CDP Chrome even if it
|
|
2243
|
-
# tried) and, since the 2026-06-23 queue migration, "run-twitter-cycle-prep"
|
|
2244
|
-
# routes through claude_job.py to an independent worker process that can
|
|
2245
|
-
# block for up to S4L_CLAUDE_QUEUE_TIMEOUT (1800s default). Holding the
|
|
2246
|
-
# browser lock across that wait made this process a preemption target for
|
|
2247
|
-
# any post that needed the browser: the post-vs-scan hijack fix SIGKILLs
|
|
2248
|
-
# whoever holds the lock (see docs/twitter_browser_lock.md), which killed
|
|
2249
|
-
# this process mid-wait while the worker kept drafting in the background,
|
|
2250
|
-
# stranding the finished result until the salvage reconciler's 35-min window
|
|
2251
|
-
# (scripts/salvage_orphaned_prep_results.py) picked it up. Releasing here
|
|
2252
|
-
# removes this whole phase from being a SIGKILL target for the rest of the
|
|
2253
|
-
# drafting wait. Phase 2b-post re-acquires unconditionally below.
|
|
2254
|
-
log "Releasing twitter-browser lock before Claude drafting (drafting never touches the browser)..."
|
|
2255
|
-
release_lock "twitter-browser" 2>>"$LOG_FILE"
|
|
2256
2263
|
|
|
2257
2264
|
# --- ACCOUNT VOICE CORPUS injection (both lanes, 2026-07-15) -----------------
|
|
2258
2265
|
# build_persona.py apply / voice_exemplars.py write a raw first-hand corpus
|
|
@@ -2824,6 +2831,17 @@ if [ "${DRAFT_ONLY:-0}" = "1" ]; then
|
|
|
2824
2831
|
--tweets-pulled "${TWEETS_PULLED:-0}" --candidates "${BATCH_COUNT:-0}" --above-floor "${HIGH_DELTA_COUNT:-0}" \
|
|
2825
2832
|
--cost "$_COST" --elapsed $(( $(date +%s) - RUN_START )) 2>/dev/null || true
|
|
2826
2833
|
_SA_RUN_SUMMARY_EMITTED=1
|
|
2834
|
+
# Sandbox runs have no MCP wrapper watching stdout for this marker to hand
|
|
2835
|
+
# the plan off to merge_review_queue.py (that handoff normally happens
|
|
2836
|
+
# outside this script), so a sandbox plan would otherwise just rot on
|
|
2837
|
+
# disk -- found live 2026-07-15, had to hand-reconstruct one after the
|
|
2838
|
+
# fact. $PLAN_FILE already carries the fully-transformed candidates
|
|
2839
|
+
# (reply_text/drafts/experiments.sandbox stamp, same shape any real draft
|
|
2840
|
+
# gets), so this is a straight, no-transform call into the same merge
|
|
2841
|
+
# step everything else uses.
|
|
2842
|
+
if [ -n "${S4L_SANDBOX_CANDIDATES_FILE:-}" ]; then
|
|
2843
|
+
python3 "$REPO_DIR/scripts/merge_review_queue.py" --plan "$PLAN_FILE" 2>&1 | tee -a "$LOG_FILE"
|
|
2844
|
+
fi
|
|
2827
2845
|
# IMPORTANT: do NOT rm -f "$PLAN_FILE" here; the reviewer needs it. Print a
|
|
2828
2846
|
# machine-readable marker so the MCP wrapper can locate the plan deterministically.
|
|
2829
2847
|
echo "DRAFT_ONLY_PLAN=$PLAN_FILE"
|