@m13v/s4l 1.7.6-rc.7 → 1.7.6-rc.9

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.6-rc.7",
3
- "installedAt": "2026-07-17T17:41:44.516Z"
2
+ "version": "1.7.6-rc.9",
3
+ "installedAt": "2026-07-17T17:54:17.719Z"
4
4
  }
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.6-rc.7",
5
+ "version": "1.7.6-rc.9",
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": {
@@ -2938,11 +2938,14 @@ class S4LMenuBar(rumps.App):
2938
2938
  flight, and not snoozed (the snooze label and gate take precedence)."""
2939
2939
  if pending_count <= 0 or self._review_active:
2940
2940
  return 0.0
2941
- # The reveal cadence is a cards-mode concept (its menu control is
2942
- # hidden in canvas mode, 2026-07-16); never hold canvas presentations
2943
- # on a stale cadence value the user can no longer see or change.
2941
+ # Canvas never auto-pops (2026-07-17, user direction): the explicit
2942
+ # "Review N pending drafts" click is the ONLY way to open it. Report
2943
+ # the same indefinite hold as the cards-mode "Never" preset so the
2944
+ # menu shows the courtesy line. The reveal cadence itself remains a
2945
+ # cards-mode concept (its menu control is hidden in canvas mode,
2946
+ # 2026-07-16), so a stale cadence value is never consulted here.
2944
2947
  if st.read_review_layout() != "cards":
2945
- return 0.0
2948
+ return -1.0
2946
2949
  now = time.time()
2947
2950
  if now < self._review_snooze_until:
2948
2951
  return 0.0
@@ -3063,12 +3066,20 @@ class S4LMenuBar(rumps.App):
3063
3066
  # card keeps growing live) and skipped for focus=True (the menu's
3064
3067
  # "Review N pending drafts" is an explicit ask). _last_review_sig is NOT
3065
3068
  # stamped on a hold, so the same set still presents fresh when due.
3066
- # Cards mode only (2026-07-16): the cadence control is hidden while
3067
- # View Mode is Canvas, so a stale value must not silently pace or
3068
- # suppress canvas presentations. No panel is open past the
3069
- # _review_active branch above, so the preference IS the layout that
3070
- # is about to present.
3071
- if not focus and st.read_review_layout() == "cards":
3069
+ # No panel is open past the _review_active branch above, so the
3070
+ # preference IS the layout that is about to present.
3071
+ if not focus:
3072
+ if st.read_review_layout() != "cards":
3073
+ # Canvas never auto-pops (2026-07-17, user direction): the
3074
+ # explicit "Review N pending drafts" click (focus=True) is the
3075
+ # ONLY way to open the canvas. The extend-open branch above
3076
+ # still grows an already-open canvas live; _last_review_sig is
3077
+ # NOT stamped here, so the whole backlog presents fresh on the
3078
+ # click.
3079
+ return
3080
+ # Cards mode: pace fresh pop-ups by the reveal cadence. The
3081
+ # cadence control is a cards-only concept (hidden while View Mode
3082
+ # is Canvas, 2026-07-16), so it is never consulted for canvas.
3072
3083
  cadence = st.read_reveal_cadence()
3073
3084
  if cadence < 0:
3074
3085
  # "Never" preset: cards never auto-pop, only an explicit
@@ -4069,7 +4080,12 @@ class S4LMenuBar(rumps.App):
4069
4080
  # now regardless.
4070
4081
  hold_until = self._reveal_hold_until(pending_count)
4071
4082
  if hold_until < 0:
4072
- items.append(self._label("Cards hidden — see count in the menu bar"))
4083
+ if st.read_review_layout() == "cards":
4084
+ items.append(self._label("Cards hidden — see count in the menu bar"))
4085
+ else:
4086
+ # Canvas mode: it never auto-opens; the review item above
4087
+ # is the one way in.
4088
+ items.append(self._label("Canvas opens from the review item above"))
4073
4089
  elif hold_until:
4074
4090
  items.append(
4075
4091
  self._label(
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.6-rc.7",
3
+ "version": "1.7.6-rc.9",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.6-rc.7",
3
+ "version": "1.7.6-rc.9",
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",
@@ -1,44 +1,61 @@
1
1
  #!/usr/bin/env python3
2
- """Real CDP readiness probe for the harness Chrome.
2
+ """Raw-websocket CDP readiness probe for the harness Chrome.
3
3
 
4
- Exit 0 when a full Playwright connect_over_cdp handshake completes against the
5
- given CDP URL, 1 when it does not. /json/version alone is a LIVENESS check: a
6
- wedged Chrome (process alive, HTTP answering, websocket upgrade completing,
7
- but the browser loop never servicing the CDP session) passes it, and every
8
- downstream attach then eats Playwright's 180s default timeout while holding
9
- the browser lock (S4L-4H, Karol 2026-07-11; identical wedge locally 2026-07-09,
10
- twice, same Chrome instance both times).
4
+ Exit 0 when a real DevTools websocket handshake completes against the given
5
+ CDP URL (browser-level connect + Browser.getVersion round-trip), 1 when it
6
+ does not. /json/version alone is a LIVENESS check: a Chrome whose ws accept
7
+ path died still passes it; this probe exercises the actual session dispatch.
11
8
 
12
- Usage: cdp_ready_check.py [CDP_URL] [TIMEOUT_MS]
9
+ HISTORY — why raw websocket and NOT Playwright (2026-07-17): the original
10
+ probe used sync_playwright().connect_over_cdp. Wedge-diag captures
11
+ (skill/logs/wedge-diag/, v3 discriminator) proved that during every observed
12
+ "wedge" the raw ws handshake completed in <5s while the Playwright connect
13
+ timed out at 20s — with Chrome's DevToolsHandlerThread idle in kevent64 the
14
+ whole time. The stall lives in Playwright's node driver under machine load,
15
+ NOT in Chrome; the Playwright-based verdict was executing a healthy browser
16
+ roughly hourly for days (every cdp_wedge relaunch 07-14..07-17). A probe must
17
+ not depend on machinery heavier than the thing it probes. The S4L-4H /
18
+ Karol-box incidents that motivated the deep probe are equally covered by the
19
+ ws round-trip below (and in hindsight may have been this same client-side
20
+ stall).
21
+
22
+ Also preserves the renderer-liveness sweep (2026-07-14): a tab whose RENDERER
23
+ crashed ("Aw, Snap") keeps its title/url in every listing while sitting dead;
24
+ probe each page with a trivial Runtime.evaluate and reload it IN PLACE on
25
+ failure — fresh renderer, no kill, no new window, no focus change.
13
26
 
14
- Prints a one-line JSON verdict to stdout so the caller can persist it
15
- (twitter-backend.sh writes it to skill/logs/cdp-health.json, which
16
- memory_snapshot.py carries onto the per-minute heartbeat sample).
27
+ Usage: cdp_ready_check.py [CDP_URL] [TIMEOUT_MS]
17
28
 
18
- Falls back to an HTTP-only probe when playwright is not importable under the
19
- invoking interpreter, so a bare python3 caller degrades to the legacy
20
- liveness behavior instead of hard-failing.
29
+ Prints a one-line JSON verdict to stdout (same shape/keys as before; mode is
30
+ now "raw_ws"). Falls back to an HTTP-only probe when websocket-client is not
31
+ importable, so a bare python3 caller degrades to liveness instead of failing.
21
32
  """
22
33
  import json
23
34
  import sys
24
35
  import time
36
+ import urllib.request
37
+
38
+ # ProxyHandler({}): loopback CDP must never route through a proxy. macOS
39
+ # system proxy settings leak into urllib's default opener, and a box-wide
40
+ # forwarder 403s 127.0.0.1 probes (2026-07-13 root cause of a "wedged
41
+ # Chrome" misdiagnosis).
42
+ _OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
43
+
44
+
45
+ def _get_json(url: str, timeout: float):
46
+ with _OPENER.open(url, timeout=timeout) as r:
47
+ return json.loads(r.read())
25
48
 
26
49
 
27
50
  def main() -> int:
28
51
  url = (sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9555").rstrip("/")
29
- timeout_ms = int(sys.argv[2]) if len(sys.argv) > 2 else 8000
52
+ timeout_s = (int(sys.argv[2]) if len(sys.argv) > 2 else 20000) / 1000.0
30
53
  t0 = time.time()
31
54
  try:
32
- from playwright.sync_api import sync_playwright
55
+ import websocket # websocket-client, present in the owned runtime venv
33
56
  except Exception:
34
- import urllib.request
35
57
  try:
36
- # ProxyHandler({}): loopback CDP must never route through a proxy.
37
- # macOS system proxy settings leak into urllib's default opener, and
38
- # a box-wide forwarder 403s 127.0.0.1 probes (2026-07-13 root cause
39
- # of the "wedged Chrome" misdiagnosis).
40
- opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
41
- opener.open(f"{url}/json/version", timeout=3)
58
+ _OPENER.open(f"{url}/json/version", timeout=3)
42
59
  print(json.dumps({"ready": True, "mode": "http-only"}))
43
60
  return 0
44
61
  except Exception as e:
@@ -46,35 +63,49 @@ def main() -> int:
46
63
  "ready": False, "mode": "http-only", "error": str(e)[:120],
47
64
  }))
48
65
  return 1
66
+
67
+ def _ws_call(ws_url: str, method: str, timeout: float, **params):
68
+ # suppress_origin: Chrome 111+ rejects ws clients whose Origin header
69
+ # is not in --remote-allow-origins (same pattern as the tab parker).
70
+ ws = websocket.create_connection(ws_url, timeout=timeout, suppress_origin=True)
71
+ try:
72
+ ws.send(json.dumps({"id": 1, "method": method, "params": params}))
73
+ deadline = time.time() + timeout
74
+ while time.time() < deadline:
75
+ msg = json.loads(ws.recv())
76
+ if msg.get("id") == 1:
77
+ return msg
78
+ raise TimeoutError(f"no reply to {method}")
79
+ finally:
80
+ ws.close()
81
+
49
82
  try:
50
- with sync_playwright() as p:
51
- browser = p.chromium.connect_over_cdp(url, timeout=timeout_ms)
52
- n_contexts = len(browser.contexts)
53
- # Renderer-liveness sweep (2026-07-14). A tab whose RENDERER
54
- # crashed ("Aw, Snap", error code 5) keeps its normal title/url in
55
- # every CDP listing and the browser-level handshake stays green,
56
- # so it sat visibly dead for 20+ minutes with nothing entitled to
57
- # touch it — below the wedge detector (browser-level) and the
58
- # stall guard (scan-progress-level). Probe each page with a
59
- # trivial evaluate; on failure reload the tab IN PLACE, which
60
- # spawns a fresh renderer. No kill, no new window, no focus
61
- # change. Best-effort: revival must never fail the readiness
62
- # verdict the wedge gate depends on.
63
- revived = 0
64
- for ctx in browser.contexts:
65
- for page in ctx.pages:
83
+ # Browser-level handshake: the readiness verdict.
84
+ info = _get_json(f"{url}/json/version", timeout=3)
85
+ _ws_call(info["webSocketDebuggerUrl"], "Browser.getVersion", timeout_s)
86
+
87
+ # Renderer-liveness sweep: revive crashed tabs in place, best-effort;
88
+ # revival must never fail the readiness verdict the wedge gate uses.
89
+ revived = 0
90
+ pages = []
91
+ try:
92
+ pages = [t for t in _get_json(f"{url}/json/list", timeout=3)
93
+ if t.get("type") == "page" and t.get("webSocketDebuggerUrl")]
94
+ for t in pages:
95
+ try:
96
+ _ws_call(t["webSocketDebuggerUrl"], "Runtime.evaluate",
97
+ 4.0, expression="1")
98
+ except Exception:
66
99
  try:
67
- page.set_default_timeout(4000)
68
- page.evaluate("1")
100
+ _ws_call(t["webSocketDebuggerUrl"], "Page.reload", 8.0)
101
+ revived += 1
69
102
  except Exception:
70
- try:
71
- page.reload(timeout=15000, wait_until="commit")
72
- revived += 1
73
- except Exception:
74
- pass
75
- browser.close()
103
+ pass
104
+ except Exception:
105
+ pass
106
+
76
107
  out = {
77
- "ready": True, "mode": "cdp", "contexts": n_contexts,
108
+ "ready": True, "mode": "raw_ws", "contexts": len(pages),
78
109
  "elapsed_s": round(time.time() - t0, 2),
79
110
  }
80
111
  if revived:
@@ -83,7 +114,7 @@ def main() -> int:
83
114
  return 0
84
115
  except Exception as e:
85
116
  print(json.dumps({
86
- "ready": False, "mode": "cdp",
117
+ "ready": False, "mode": "raw_ws",
87
118
  "elapsed_s": round(time.time() - t0, 2),
88
119
  "error": str(e)[:200].replace("\n", " "),
89
120
  }))