social-autoposter 1.6.31 → 1.6.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/manifest.json +2 -1
- package/mcp-servers/browser-harness/server.py +81 -19
- package/package.json +5 -2
- package/skill/invent-supply-test.sh +5 -4
- package/skill/run-twitter-cycle.sh +49 -4
- package/mcp-servers/browser-harness/__pycache__/server.cpython-311.pyc +0 -0
- package/mcp-servers/browser-harness/__pycache__/server.cpython-314.pyc +0 -0
- package/mcp-servers/browser-harness/__pycache__/server.cpython-39.pyc +0 -0
- package/scripts/_dm_icp_batch.sh +0 -32
package/mcp/manifest.json
CHANGED
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"type": "directory",
|
|
51
51
|
"title": "Social Autoposter repo path",
|
|
52
52
|
"description": "Absolute path to your social-autoposter repo clone (the folder that contains config.json, scripts/, skill/). The MCP shells out to the pipeline scripts in this folder.",
|
|
53
|
-
"required": true
|
|
53
|
+
"required": true,
|
|
54
|
+
"default": "${HOME}/social-autoposter"
|
|
54
55
|
},
|
|
55
56
|
"saps_python": {
|
|
56
57
|
"type": "string",
|
|
@@ -309,31 +309,93 @@ def _terminate(pid: int, grace: float = 5.0) -> None:
|
|
|
309
309
|
pass
|
|
310
310
|
|
|
311
311
|
|
|
312
|
+
def _cdp_browser_close() -> bool:
|
|
313
|
+
"""Ask Chrome to quit via CDP Browser.close. This is Chrome's own
|
|
314
|
+
graceful-shutdown RPC: it tears down renderers in order and flushes the
|
|
315
|
+
cookie store before exiting, which signal-based termination does not
|
|
316
|
+
reliably guarantee. Returns True if the RPC was issued; False if the
|
|
317
|
+
browser-harness CLI was missing, CDP was unreachable, or the call errored.
|
|
318
|
+
Issuing the RPC does NOT mean Chrome has exited yet — poll the pid."""
|
|
319
|
+
if not shutil.which(BROWSER_HARNESS_BIN) and not Path(BROWSER_HARNESS_BIN).exists():
|
|
320
|
+
return False
|
|
321
|
+
env = os.environ.copy()
|
|
322
|
+
env["BU_CDP_URL"] = CDP_URL
|
|
323
|
+
env["PATH"] = f"{Path.home()}/.local/bin:" + env.get("PATH", "")
|
|
324
|
+
try:
|
|
325
|
+
proc = subprocess.run(
|
|
326
|
+
[BROWSER_HARNESS_BIN],
|
|
327
|
+
input="cdp('Browser.close')\n",
|
|
328
|
+
env=env,
|
|
329
|
+
capture_output=True,
|
|
330
|
+
text=True,
|
|
331
|
+
timeout=10,
|
|
332
|
+
)
|
|
333
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
334
|
+
return False
|
|
335
|
+
return proc.returncode == 0
|
|
336
|
+
|
|
337
|
+
|
|
312
338
|
def stop_chrome() -> dict:
|
|
313
339
|
"""Gracefully stop the managed Chrome — the tracked process AND any orphan
|
|
314
340
|
still LISTENing on the debug port — so connect_x-launched Chromes can't
|
|
315
|
-
strand the port.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
341
|
+
strand the port.
|
|
342
|
+
|
|
343
|
+
Two-stage shutdown: first ask Chrome to quit itself via CDP `Browser.close`
|
|
344
|
+
(its own graceful-quit RPC, which flushes the cookie SQLite synchronously
|
|
345
|
+
before exit). If the process exits within `CDP_QUIT_DEADLINE_SEC`, we're
|
|
346
|
+
done — cookies are durable. Only if CDP refuses or the process doesn't
|
|
347
|
+
exit in time do we fall back to SIGTERM-with-grace, then SIGKILL. The old
|
|
348
|
+
SIGTERM+5s+SIGKILL path lost cookies because Chrome's shutdown sequence
|
|
349
|
+
sometimes outlasts the 5s window; CDP-first removes that race."""
|
|
350
|
+
CDP_QUIT_DEADLINE_SEC = 20.0
|
|
351
|
+
POLL_INTERVAL_SEC = 0.5
|
|
352
|
+
|
|
353
|
+
tracked_pid = _read_pid()
|
|
354
|
+
initial_owners = _port_owner_pids()
|
|
355
|
+
initial_targets: list[int] = []
|
|
356
|
+
if tracked_pid and _pid_alive(tracked_pid):
|
|
357
|
+
initial_targets.append(tracked_pid)
|
|
358
|
+
for owner in initial_owners:
|
|
359
|
+
if owner not in initial_targets and _pid_alive(owner):
|
|
360
|
+
initial_targets.append(owner)
|
|
361
|
+
|
|
362
|
+
cdp_attempted = False
|
|
363
|
+
cdp_issued = False
|
|
364
|
+
if initial_targets and _cdp_alive():
|
|
365
|
+
cdp_attempted = True
|
|
366
|
+
cdp_issued = _cdp_browser_close()
|
|
367
|
+
if cdp_issued:
|
|
368
|
+
deadline = time.time() + CDP_QUIT_DEADLINE_SEC
|
|
369
|
+
while time.time() < deadline:
|
|
370
|
+
still_alive = [p for p in initial_targets if _pid_alive(p)]
|
|
371
|
+
if not still_alive:
|
|
372
|
+
break
|
|
373
|
+
time.sleep(POLL_INTERVAL_SEC)
|
|
374
|
+
|
|
375
|
+
reaped: list[int] = []
|
|
376
|
+
survivors = [p for p in initial_targets if _pid_alive(p)]
|
|
377
|
+
for p in survivors:
|
|
378
|
+
_terminate(p, grace=15.0)
|
|
379
|
+
reaped.append(p)
|
|
380
|
+
|
|
381
|
+
for stale in (PID_FILE, Path("/tmp/bu-default.sock"), Path("/tmp/bu-default.pid")):
|
|
332
382
|
try:
|
|
333
|
-
|
|
383
|
+
stale.unlink()
|
|
334
384
|
except FileNotFoundError:
|
|
335
385
|
pass
|
|
336
|
-
|
|
386
|
+
|
|
387
|
+
via = "cdp_browser_close" if (cdp_issued and not survivors) else (
|
|
388
|
+
"sigterm_fallback" if cdp_attempted else "sigterm"
|
|
389
|
+
)
|
|
390
|
+
return {
|
|
391
|
+
"status": "stopped",
|
|
392
|
+
"via": via,
|
|
393
|
+
"tracked_pid": tracked_pid,
|
|
394
|
+
"initial_targets": initial_targets,
|
|
395
|
+
"cdp_attempted": cdp_attempted,
|
|
396
|
+
"cdp_issued": cdp_issued,
|
|
397
|
+
"sigterm_reaped": reaped,
|
|
398
|
+
}
|
|
337
399
|
|
|
338
400
|
|
|
339
401
|
# --- browser-harness exec wrapper ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-autoposter",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.32",
|
|
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"
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
"!scripts/test_own_reply_dedup.py",
|
|
107
107
|
"!scripts/twitter_compose_dm.py",
|
|
108
108
|
"scripts/*.sh",
|
|
109
|
+
"!scripts/_dm_icp_batch.sh",
|
|
109
110
|
"config.example.json",
|
|
110
111
|
"requirements.txt",
|
|
111
112
|
"SKILL.md",
|
|
@@ -117,7 +118,9 @@
|
|
|
117
118
|
"mcp/dist/",
|
|
118
119
|
"mcp/package.json",
|
|
119
120
|
"mcp/manifest.json",
|
|
120
|
-
"mcp/install.mjs"
|
|
121
|
+
"mcp/install.mjs",
|
|
122
|
+
"!**/__pycache__/**",
|
|
123
|
+
"!**/*.pyc"
|
|
121
124
|
],
|
|
122
125
|
"keywords": [
|
|
123
126
|
"social-media",
|
|
@@ -65,13 +65,14 @@ echo "[invent-supply-test] twitter-browser lock held (pid=$$)" >&2
|
|
|
65
65
|
|
|
66
66
|
# One harness invocation handles every query so we pay the CLI startup once.
|
|
67
67
|
# Each scan() call appends a JSONL record to SCAN_TWEETS_FILE=$SCAN_OUT.
|
|
68
|
-
#
|
|
69
|
-
#
|
|
68
|
+
# Installed browser-harness (v0.1.0) only accepts `-c "<script>"`; it does NOT
|
|
69
|
+
# read from stdin (a heredoc just prints usage and exits with 0 tweets). Use
|
|
70
|
+
# double-quoted -c so $REPO_DIR / $QUERIES_JSON still expand.
|
|
70
71
|
BU_NAME=twitter-harness BU_CDP_URL=http://127.0.0.1:9555 \
|
|
71
72
|
SCAN_TWEETS_FILE="$SCAN_OUT" \
|
|
72
73
|
BATCH_ID="${BATCH_ID:-}" \
|
|
73
74
|
FRESHNESS_HOURS_DISCOVER="$FRESHNESS_HOURS" \
|
|
74
|
-
"$HARNESS_BIN"
|
|
75
|
+
"$HARNESS_BIN" -c "
|
|
75
76
|
import sys, json, os, time
|
|
76
77
|
sys.path.insert(0, '$REPO_DIR/scripts')
|
|
77
78
|
from twitter_scan import scan
|
|
@@ -90,7 +91,7 @@ for q in queries:
|
|
|
90
91
|
except Exception as e:
|
|
91
92
|
dt = time.time() - t0
|
|
92
93
|
print(f' err project={project!r} q={query[:50]!r} in {dt:.1f}s {type(e).__name__}: {e}', flush=True)
|
|
93
|
-
|
|
94
|
+
" 2>&1
|
|
94
95
|
|
|
95
96
|
release_lock "twitter-browser"
|
|
96
97
|
echo "[invent-supply-test] done; results in $SCAN_OUT" >&2
|
|
@@ -690,6 +690,49 @@ log "twitter-browser lock held (pid=$$) Phase 1"
|
|
|
690
690
|
# refuses to clean if the lock PID is alive.
|
|
691
691
|
ensure_twitter_browser_for_backend 2>&1 | tee -a "$LOG_FILE"
|
|
692
692
|
|
|
693
|
+
# --- Pre-flight: live X session probe (added 2026-06-02) --------------------
|
|
694
|
+
# Before drafting/scraping anything, confirm the harness Chrome actually has a
|
|
695
|
+
# valid x.com session. One CDP Network.getCookies call (<1s) catches the
|
|
696
|
+
# "import never ran, evaporated after a hard restart, or auth_token expired"
|
|
697
|
+
# cases that previously surfaced as "Phase 1 returned 0 tweets" mysteries.
|
|
698
|
+
# Failing fast here turns a wasted ~7-minute scan + Claude bill into a clear
|
|
699
|
+
# "reconnect X" message in the log.
|
|
700
|
+
log "Pre-flight: probing harness Chrome for a live x.com auth_token..."
|
|
701
|
+
_PREFLIGHT_OUT=$(BU_NAME=twitter-harness BU_CDP_URL=http://127.0.0.1:9555 \
|
|
702
|
+
"$HOME/.local/bin/browser-harness" -c "
|
|
703
|
+
import sys, time
|
|
704
|
+
try:
|
|
705
|
+
raw = cdp('Network.getCookies', urls=['https://x.com/', 'https://twitter.com/'])
|
|
706
|
+
except Exception as e:
|
|
707
|
+
print('PREFLIGHT_CDP_ERROR ' + type(e).__name__ + ': ' + str(e))
|
|
708
|
+
sys.exit(0)
|
|
709
|
+
ck = raw.get('cookies', [])
|
|
710
|
+
auth = [c for c in ck if c.get('name') == 'auth_token']
|
|
711
|
+
if not auth:
|
|
712
|
+
print('PREFLIGHT_FAIL no_auth_token cookies_total=' + str(len(ck)))
|
|
713
|
+
sys.exit(0)
|
|
714
|
+
exp = auth[0].get('expires')
|
|
715
|
+
domain = auth[0].get('domain', '?')
|
|
716
|
+
if exp in (None, -1, 0):
|
|
717
|
+
print('PREFLIGHT_OK session domain=' + domain)
|
|
718
|
+
else:
|
|
719
|
+
now = time.time()
|
|
720
|
+
if exp < now:
|
|
721
|
+
print('PREFLIGHT_FAIL auth_token_expired exp=' + str(int(exp)) + ' now=' + str(int(now)))
|
|
722
|
+
sys.exit(0)
|
|
723
|
+
print('PREFLIGHT_OK exp=' + str(int(exp)) + ' domain=' + domain)
|
|
724
|
+
" 2>&1)
|
|
725
|
+
if printf '%s\n' "$_PREFLIGHT_OUT" | grep -q '^PREFLIGHT_OK'; then
|
|
726
|
+
log " Pre-flight OK: $(printf '%s\n' "$_PREFLIGHT_OUT" | grep '^PREFLIGHT_OK' | head -1)"
|
|
727
|
+
else
|
|
728
|
+
log " Pre-flight FAILED. The harness Chrome has no live X session."
|
|
729
|
+
log " Details: $(printf '%s\n' "$_PREFLIGHT_OUT" | tail -3 | tr '\n' '|')"
|
|
730
|
+
log " Action: run \`python3 scripts/setup_twitter_auth.py connect\` (or call the connect_x MCP tool) to import a fresh X session from your everyday browser, then re-run the cycle. If the import fails with 'access denied', unlock the macOS keychain first: \`security unlock-keychain ~/Library/Keychains/login.keychain-db\`."
|
|
731
|
+
echo "twitter_batches: ended $BATCH_ID"
|
|
732
|
+
release_lock "twitter-browser" 2>/dev/null || true
|
|
733
|
+
exit 1
|
|
734
|
+
fi
|
|
735
|
+
|
|
693
736
|
# --- Phase 1 retry loop (2026-05-27) ----------------------------------------
|
|
694
737
|
# When a single scan produces fewer than RETRY_TARGET candidates that survive
|
|
695
738
|
# all Phase 1 filters (harness age gate, scorer stale_age cutoff, already-
|
|
@@ -996,15 +1039,17 @@ except Exception: print(0)
|
|
|
996
1039
|
# $SCAN_TWEETS_FILE, which the existing shell-side parse below consumes.
|
|
997
1040
|
if [ "$QUERIES_COUNT" -gt 0 ]; then
|
|
998
1041
|
log "Lean Phase 1: executing $QUERIES_COUNT queries via browser-harness CDP"
|
|
999
|
-
#
|
|
1000
|
-
#
|
|
1042
|
+
# Installed browser-harness (v0.1.0) only accepts `-c "<script>"`; it does NOT
|
|
1043
|
+
# read a script from stdin (a heredoc just makes it print its usage line and
|
|
1044
|
+
# exit, producing 0 tweets). Use double-quoted -c so $REPO_DIR / $QUERIES_TMP
|
|
1045
|
+
# still expand; the Python body uses single quotes internally so it nests fine.
|
|
1001
1046
|
BU_NAME=twitter-harness BU_CDP_URL=http://127.0.0.1:9555 \
|
|
1002
1047
|
SCAN_TWEETS_FILE="$SCAN_TWEETS_FILE" \
|
|
1003
1048
|
BATCH_ID="$BATCH_ID" \
|
|
1004
1049
|
TWITTER_CYCLE_VARIANT="$TWITTER_CYCLE_VARIANT" \
|
|
1005
1050
|
FRESHNESS_HOURS_DISCOVER="$FRESHNESS_HOURS_DISCOVER" \
|
|
1006
1051
|
ENGAGED_TWEET_IDS="$ENGAGED_TWEET_IDS" \
|
|
1007
|
-
"$HOME/.local/bin/browser-harness"
|
|
1052
|
+
"$HOME/.local/bin/browser-harness" -c "
|
|
1008
1053
|
import sys, json, os, time
|
|
1009
1054
|
sys.path.insert(0, '$REPO_DIR/scripts')
|
|
1010
1055
|
from twitter_scan import scan
|
|
@@ -1029,7 +1074,7 @@ for q in queries:
|
|
|
1029
1074
|
except Exception as e:
|
|
1030
1075
|
dt = time.time() - t0
|
|
1031
1076
|
print(f' err project={project!r} q={query[:50]!r} in {dt:.1f}s {type(e).__name__}: {e}', flush=True)
|
|
1032
|
-
|
|
1077
|
+
" 2>&1 | tee -a "$LOG_FILE"
|
|
1033
1078
|
fi
|
|
1034
1079
|
rm -f "$QUERIES_TMP"
|
|
1035
1080
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/_dm_icp_batch.sh
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Usage: _dm_icp_batch.sh DM_ID DEFAULT_LABEL DEFAULT_NOTES [override:project=label:notes ...]
|
|
3
|
-
# Sets default label/notes on all 19 projects, then applies overrides
|
|
4
|
-
set -e
|
|
5
|
-
DM_ID=$1
|
|
6
|
-
DEFAULT_LABEL=$2
|
|
7
|
-
DEFAULT_NOTES=$3
|
|
8
|
-
shift 3
|
|
9
|
-
|
|
10
|
-
declare -A LABELS NOTES
|
|
11
|
-
PROJECTS=(fazm Terminator "macOS MCP" Vipassana S4L "AI Browser Profile" "WhatsApp MCP" "macOS Session Replay" Cyrano Assrt PieLine Clone mk0r fde10x claude-meter c0nsl tenxats paperback-expert studyly)
|
|
12
|
-
for p in "${PROJECTS[@]}"; do
|
|
13
|
-
LABELS["$p"]="$DEFAULT_LABEL"
|
|
14
|
-
NOTES["$p"]="$DEFAULT_NOTES"
|
|
15
|
-
done
|
|
16
|
-
|
|
17
|
-
for arg in "$@"; do
|
|
18
|
-
proj="${arg%%=*}"
|
|
19
|
-
rest="${arg#*=}"
|
|
20
|
-
lbl="${rest%%:*}"
|
|
21
|
-
note="${rest#*:}"
|
|
22
|
-
LABELS["$proj"]="$lbl"
|
|
23
|
-
NOTES["$proj"]="$note"
|
|
24
|
-
done
|
|
25
|
-
|
|
26
|
-
cd /Users/matthewdi/social-autoposter
|
|
27
|
-
for p in "${PROJECTS[@]}"; do
|
|
28
|
-
python3 scripts/dm_conversation.py set-icp-precheck \
|
|
29
|
-
--dm-id "$DM_ID" --project "$p" \
|
|
30
|
-
--label "${LABELS[$p]}" --notes "${NOTES[$p]}" >/dev/null
|
|
31
|
-
done
|
|
32
|
-
echo "ICP prechecks set for DM $DM_ID (default=$DEFAULT_LABEL, ${#@} overrides)"
|