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 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. SIGTERM-with-grace lets Chrome persist cookies to disk."""
316
- targets: list[int] = []
317
- pid = _read_pid()
318
- if pid and _pid_alive(pid):
319
- targets.append(pid)
320
- for owner in _port_owner_pids():
321
- if owner not in targets and _pid_alive(owner):
322
- targets.append(owner)
323
-
324
- for t in targets:
325
- _terminate(t)
326
-
327
- try:
328
- PID_FILE.unlink()
329
- except FileNotFoundError:
330
- pass
331
- for stale in ("/tmp/bu-default.sock", "/tmp/bu-default.pid"):
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
- os.unlink(stale)
383
+ stale.unlink()
334
384
  except FileNotFoundError:
335
385
  pass
336
- return {"status": "stopped", "reaped": targets, "tracked_pid": pid}
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.31",
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
- # Upstream browser-harness dropped `-c <script>` for stdin-heredoc; pipe via
69
- # an unquoted heredoc so $REPO_DIR / $QUERIES_JSON still expand.
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" <<PY 2>&1
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
- PY
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
- # Upstream browser-harness dropped `-c <script>` in favor of stdin-heredoc;
1000
- # pipe via an unquoted heredoc so $REPO_DIR / $QUERIES_TMP still expand.
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" <<PY 2>&1 | tee -a "$LOG_FILE"
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
- PY
1077
+ " 2>&1 | tee -a "$LOG_FILE"
1033
1078
  fi
1034
1079
  rm -f "$QUERIES_TMP"
1035
1080
 
@@ -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)"