@m13v/s4l 1.7.7-rc.4 → 1.7.7-rc.6

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/index.js CHANGED
@@ -131,10 +131,13 @@ const STALL_WATCH_INTERVAL_SECS = 120;
131
131
  // S4L_OVERLAY_WATCH=0.
132
132
  const OVERLAY_WATCH_LABEL = "com.m13v.social-overlay-watch";
133
133
  const OVERLAY_WATCH_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${OVERLAY_WATCH_LABEL}.plist`);
134
- // Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
135
- // install keeps itself current — the interactive `runtime` tool (action:'update')
136
- // only helps when
137
- // a human-facing agent session is open, which an autopilot box never has.
134
+ // Legacy daily self-updater launchd job. BANNED (user decision 2026-07-31):
135
+ // it ran s4l_box_update.sh unattended, which kills and relaunches Claude
136
+ // Desktop with no human in the loop. Updates are user-initiated only (menubar
137
+ // "Update now & restart Claude Desktop", the panel button, `runtime`
138
+ // action:'update', or an operator running s4l_box_update.sh explicitly). The
139
+ // label/plist constants survive so ensureUpdaterRemoved() can clean the job
140
+ // off every box that got it from v1.7.1..v1.7.7-rc.4.
138
141
  const UPDATER_LABEL = "com.m13v.social-autoposter-update";
139
142
  const UPDATER_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${UPDATER_LABEL}.plist`);
140
143
  // A sane PATH for launchd jobs (launchd starts with a bare PATH). Include the
@@ -4874,68 +4877,21 @@ async function ensureOverlayWatchInstalled() {
4874
4877
  return { ok: false, detail: e?.message || String(e) };
4875
4878
  }
4876
4879
  }
4877
- // Install/refresh the daily self-updater launchd job. This used to be bundled
4878
- // into the now-deleted `autopilot` MCP tool (removed 2026-06-19, 88bd1cb9):
4879
- // calling `autopilot enable` installed this job as a side effect, so removing
4880
- // the tool silently orphaned it — UPDATER_LABEL/UPDATER_PLIST and the
4881
- // auto_update_on status check survived (buildSnapshot still reports them), but
4882
- // nothing has installed the plist since, on ANY box provisioned after that
4883
- // commit (auto_update_on reads false forever). Restored here as its own
4884
- // deterministic boot-time job, same pattern as its five siblings above.
4885
- //
4886
- // Points at scripts/s4l_box_update.sh, NOT skill/social-autoposter-update.sh:
4887
- // the latter is the npm-lane updater (npm view + npx update) and is a silent
4888
- // no-op on a .mcpb box, which has no npm/npx on PATH (see version.ts). The
4889
- // .mcpb-lane equivalent downloads the .mcpb directly from the channel-resolved
4890
- // GitHub release and unpacks it over the extension dir — see that script's own
4891
- // header for the channel/no-downgrade/retry guards. Default mode there
4892
- // downloads + unpacks + restarts Claude Desktop with NO human in the loop,
4893
- // matching the original bundled updater's intent ("keeps a headless install
4894
- // current"); RunAtLoad so a box that boots already-behind checks promptly.
4895
- async function ensureUpdaterInstalled() {
4880
+ // Remove the legacy daily self-updater launchd job (see UPDATER_LABEL's
4881
+ // comment). Boot-time, idempotent, best-effort: unload the job if launchd has
4882
+ // it and delete the plist so the daily silent Claude Desktop restart can never
4883
+ // fire again. Do NOT reintroduce an install path for this job — any future
4884
+ // auto-update mechanism must be user-visible and user-confirmed.
4885
+ async function ensureUpdaterRemoved() {
4896
4886
  try {
4897
4887
  if (process.platform !== "darwin")
4898
- return { ok: false, detail: "not macOS" };
4899
- if ((process.env.S4L_AUTO_UPDATE) === "0")
4900
- return { ok: false, detail: "disabled (S4L_AUTO_UPDATE=0)" };
4901
- const logDir = path.join(repoDir(), "skill", "logs");
4902
- try {
4903
- fs.mkdirSync(logDir, { recursive: true });
4904
- }
4905
- catch {
4906
- /* best-effort */
4907
- }
4908
- const xml = plistXml({
4909
- label: UPDATER_LABEL,
4910
- programArgs: ["/bin/bash", path.join(repoDir(), "scripts", "s4l_box_update.sh")],
4911
- intervalSecs: 86_400,
4912
- runAtLoad: true,
4913
- stdoutLog: path.join(logDir, "launchd-self-update-stdout.log"),
4914
- stderrLog: path.join(logDir, "launchd-self-update-stderr.log"),
4915
- });
4888
+ return { ok: true, detail: "not macOS" };
4916
4889
  const uid = process.getuid ? process.getuid() : 0;
4917
- let cur = null;
4918
- try {
4919
- cur = fs.readFileSync(UPDATER_PLIST, "utf-8");
4920
- }
4921
- catch {
4922
- cur = null;
4923
- }
4924
- let detail;
4925
- if (cur === xml) {
4926
- const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4927
- detail = `current (load rc=${res.code})`;
4928
- }
4929
- else {
4930
- if (cur !== null) {
4931
- await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4932
- }
4933
- fs.mkdirSync(path.dirname(UPDATER_PLIST), { recursive: true });
4934
- fs.writeFileSync(UPDATER_PLIST, xml, "utf-8");
4935
- const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4936
- detail = cur === null ? "installed + loaded" : `rewritten + reloaded (rc=${res.code})`;
4937
- }
4938
- return { ok: true, detail };
4890
+ const existed = fs.existsSync(UPDATER_PLIST);
4891
+ await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
4892
+ if (existed)
4893
+ fs.rmSync(UPDATER_PLIST, { force: true });
4894
+ return { ok: true, detail: existed ? "removed" : "absent" };
4939
4895
  }
4940
4896
  catch (e) {
4941
4897
  return { ok: false, detail: e?.message || String(e) };
@@ -6134,13 +6090,12 @@ async function main() {
6134
6090
  void ensureOverlayWatchInstalled()
6135
6091
  .then((r) => console.error(`[overlay-watch] launchd supervisor: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6136
6092
  .catch((e) => console.error("[overlay-watch] supervisor install failed:", e?.message || e));
6137
- // Daily self-updater: restored 2026-07-08 after 88bd1cb9 ("Remove autopilot
6138
- // tool") silently dropped its only install path (it used to be bundled into
6139
- // the deleted `autopilot enable` action). Best-effort; never blocks boot.
6140
- // Disable with S4L_AUTO_UPDATE=0.
6141
- void ensureUpdaterInstalled()
6142
- .then((r) => console.error(`[self-update] launchd updater: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6143
- .catch((e) => console.error("[self-update] updater install failed:", e?.message || e));
6093
+ // Silent daily self-updates are banned (user decision 2026-07-31): the job
6094
+ // restarted Claude Desktop unattended. Clean it off every box that got it
6095
+ // from v1.7.1..v1.7.7-rc.4. Best-effort; never blocks boot.
6096
+ void ensureUpdaterRemoved()
6097
+ .then((r) => console.error(`[self-update] legacy launchd updater: ${r.ok ? "ok" : "skip"} (${r.detail})`))
6098
+ .catch((e) => console.error("[self-update] updater removal failed:", e?.message || e));
6144
6099
  // Heal installs onboarded before short_links_live defaulted to false: such a
6145
6100
  // project wraps short links against the customer's own domain, which has no
6146
6101
  // /r/[code] resolver, so every minted link 404s. Re-point them at the s4l.ai
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.7-rc.4",
3
- "installedAt": "2026-07-30T23:35:31.407Z"
2
+ "version": "1.7.7-rc.6",
3
+ "installedAt": "2026-07-31T23:52:05.555Z"
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.7-rc.4",
5
+ "version": "1.7.7-rc.6",
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": {
@@ -156,9 +156,14 @@ class _Worker(threading.Thread):
156
156
  # the foreground, hide that specific Chrome process by pid, then force
157
157
  # focus back to the app the user was in. Screenshots/clicks are
158
158
  # unaffected (CDP is offscreen-raster + synthetic input; the occlusion
159
- # flags keep hidden tabs painting). Escape hatch: S4L_NO_HARNESS_HIDE.
159
+ # flags keep hidden tabs painting).
160
+ # DEFAULT OFF (user decision 2026-07-30): only the telemetry above runs
161
+ # unless S4L_HARNESS_HIDE=1 opts in. S4L_NO_HARNESS_HIDE force-disables
162
+ # even when the opt-in is set.
160
163
  if os.environ.get("S4L_NO_HARNESS_HIDE"):
161
164
  return
165
+ if os.environ.get("S4L_HARNESS_HIDE") != "1":
166
+ return
162
167
  pos = details.get("window_position") or ""
163
168
  try:
164
169
  offscreen = any(int(float(v)) < 0 for v in pos.split(",")[:2])
@@ -464,12 +464,12 @@ def _compact_arm(variant):
464
464
 
465
465
 
466
466
  def _reply_heading_suffix(d):
467
- """Concise 'project/lane · viral N · t v4' readout appended straight onto the
468
- 'Reply (editable):' heading (2026-07-07), so the two facts a reviewer
469
- checks first don't require opening the details eye. Pulled from the same
467
+ """Concise 'project · viral N · t v4' readout appended straight onto the
468
+ 'Reply (editable):' heading (2026-07-07), so the facts a reviewer checks
469
+ first don't require opening the details eye. Pulled from the same
470
470
  fields _details_lines used to render as 'Project:'/'Lane:'/'Virality
471
- score:' rows; those three are omitted from the popover now to avoid
472
- showing the same numbers twice. Empty string when neither is known."""
471
+ score:' rows; those are omitted from the popover now to avoid showing
472
+ the same numbers twice. Empty string when neither is known."""
473
473
  d = d or {}
474
474
  project = (d.get("project") or "").strip()
475
475
  lane = ((d.get("experiments") or {}).get("lane") or "").strip()
@@ -480,20 +480,11 @@ def _reply_heading_suffix(d):
480
480
  # approve_() blocks posting on these and the reviewer should know why
481
481
  # before clicking rather than after.
482
482
  bits.append("⚠ SANDBOX — not postable")
483
- # Project and lane are independent concepts most of the time (project
484
- # "fazm" drafted under lane "personal_brand"), but the operator's own
485
- # PersonalBrand project IS the personal_brand lane, so project=="PersonalBrand"
486
- # + lane=="personal_brand" is the SAME fact twice, not two facts — compare
487
- # with punctuation/case stripped so "PersonalBrand" vs "personal_brand"
488
- # collapses to one token instead of "PersonalBrand/personal_brand".
489
- same = project and lane and re.sub(r"[^a-z0-9]", "", project.lower()) == re.sub(
490
- r"[^a-z0-9]", "", lane.lower()
491
- )
492
- if same:
493
- bits.append(project)
494
- elif project and lane:
495
- bits.append(f"{project}/{lane}")
496
- elif project:
483
+ # Heading shows the project/lane name only, never a combined
484
+ # "project/lane" string (2026-07-31, user call: the "/promotion" suffix
485
+ # on cards was confusing). Lane is still stamped on the candidate and
486
+ # readable in the details popover; it's just not concatenated here.
487
+ if project:
497
488
  bits.append(project)
498
489
  elif lane:
499
490
  bits.append(lane)
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.7-rc.4",
3
+ "version": "1.7.7-rc.6",
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.7-rc.4",
3
+ "version": "1.7.7-rc.6",
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",
@@ -76,6 +76,12 @@ TAG_TO_TYPE = {
76
76
  # Python and inlined; no Bash tools), mirroring the twitter Phase 2b
77
77
  # tool-free conversion, so it rides the same universal worker.
78
78
  "post-reddit-draft": "reddit-draft",
79
+ # Context mining (2026-07-31): scripts/context_mining.py inlines compacted
80
+ # transcripts + the corpus into one pure text->JSON turn. Execution
81
+ # guidance lives at the top of the prompt itself (not only in
82
+ # TYPE_TO_WORKER_NOTES) because an installed-package worker claims with
83
+ # ITS copy of this map, which may predate this type.
84
+ "context-mining": "context-mining",
79
85
  }
80
86
 
81
87
  # queue type -> (activity state, label) the menu bar shows while the job is in
@@ -92,6 +98,7 @@ TYPE_TO_ACTIVITY = {
92
98
  # must stay concise (user rule 2026-07-14); the platform shows on the
93
99
  # review card itself, not in the tray.
94
100
  "reddit-draft": ("drafting", "draft"),
101
+ "context-mining": ("learning", "mining context"),
95
102
  }
96
103
 
97
104
  # queue type -> execution notes PREPENDED to the prompt sidecar at claim time.
@@ -140,6 +147,13 @@ TYPE_TO_WORKER_NOTES = {
140
147
  "absent from posts (add a rejects entry only for the structural "
141
148
  "false-positive cases the prompt describes)."
142
149
  ),
150
+ "context-mining": (
151
+ "WORKER EXECUTION NOTES (queue metadata; follow while executing the "
152
+ "prompt below): single-turn, tool-free job. Everything you need is "
153
+ "inlined in the prompt (transcripts, corpus, prior decisions). Never "
154
+ "use tools. Read, then submit ONE result object matching the schema: "
155
+ "{\"proposals\": [...]}. An empty proposals list is a valid answer."
156
+ ),
143
157
  }
144
158
 
145
159
 
@@ -1,11 +1,21 @@
1
1
  #!/usr/bin/env python3
2
- """Close every CDP "page" tab in harness Chrome except one.
2
+ """Close every CDP "page" tab in harness Chrome except one, and keep that one
3
+ tab REAL (http/https).
3
4
 
4
- Called from skill/lib/twitter-backend.sh::ensure_twitter_browser_for_backend
5
- and skill/engage-twitter.sh's inline harness branch as part of pre-flight.
6
- Safe to call any time: exits 0 silently when harness Chrome is down. Workers
7
- and iframe targets are left alone; they auto-clean when their parent page
8
- closes.
5
+ Called from skill/lib/harness-common.sh::hc_cleanup_tabs (all three platform
6
+ backends) as part of pre-flight. Safe to call any time: exits 0 silently when
7
+ harness Chrome is down. Workers and iframe targets are left alone; they
8
+ auto-clean when their parent page closes.
9
+
10
+ Revive (2026-07-31): when BH_REVIVE_URL is set to an http(s) url, a Chrome
11
+ whose only tab is about:blank gets that tab navigated to the revive url, and
12
+ a completely tabless Chrome (--no-startup-window launch) gets ONE background
13
+ tab created there. This matters because the harness daemon's is_real_page()
14
+ refuses about:/chrome: tabs — a blank-only browser used to trap it in a
15
+ "stale session -> foreground createTarget(about:blank) -> still not real"
16
+ loop, one macOS focus steal per iteration (30/day on reddit-harness).
17
+ Both revive paths need websocket-client (present in the owned runtime venv,
18
+ S4L_PYTHON); without it the script degrades to close-only, as before.
9
19
 
10
20
  The standalone-script form (vs an inline heredoc) is required because bash
11
21
  3.2 on macOS cannot parse a nested heredoc inside a function body inside a
@@ -24,6 +34,56 @@ import urllib.request
24
34
  # its own harness Chrome on 9556. Default 9555 keeps Twitter callers unchanged.
25
35
  CDP_PORT = int(os.environ.get("BH_CLEANUP_PORT", "9555"))
26
36
  CDP_URL = f"http://127.0.0.1:{CDP_PORT}"
37
+ REVIVE_URL = os.environ.get("BH_REVIVE_URL", "")
38
+
39
+
40
+ def _is_real(t) -> bool:
41
+ return (t.get("url") or "").startswith(("http://", "https://"))
42
+
43
+
44
+ def _ws_call(ws_url: str, method: str, timeout: float = 8.0, **params):
45
+ # suppress_origin: Chrome 111+ rejects ws clients whose Origin header is
46
+ # not in --remote-allow-origins (same pattern as cdp_ready_check.py).
47
+ import websocket # websocket-client; ImportError handled by callers
48
+
49
+ ws = websocket.create_connection(ws_url, timeout=timeout, suppress_origin=True)
50
+ try:
51
+ ws.send(json.dumps({"id": 1, "method": method, "params": params}))
52
+ import time
53
+
54
+ deadline = time.time() + timeout
55
+ while time.time() < deadline:
56
+ msg = json.loads(ws.recv())
57
+ if msg.get("id") == 1:
58
+ return msg
59
+ raise TimeoutError(f"no reply to {method}")
60
+ finally:
61
+ ws.close()
62
+
63
+
64
+ def _revive(keep) -> None:
65
+ """Ensure one REAL tab exists: navigate a lone non-real tab (keep) to
66
+ REVIVE_URL, or create a background tab there when keep is None (tabless
67
+ Chrome). Best-effort; cleanup's exit code never depends on it."""
68
+ if not REVIVE_URL.startswith(("http://", "https://")):
69
+ return
70
+ try:
71
+ if keep is None:
72
+ # background=True: a foreground Target.createTarget raises the
73
+ # Chrome window and steals macOS app focus; revive must be silent.
74
+ info = json.loads(
75
+ urllib.request.urlopen(f"{CDP_URL}/json/version", timeout=2).read()
76
+ )
77
+ _ws_call(
78
+ info["webSocketDebuggerUrl"], "Target.createTarget",
79
+ url=REVIVE_URL, background=True,
80
+ )
81
+ print(f"[cleanup_harness_tabs] created background tab on {REVIVE_URL}")
82
+ elif not _is_real(keep) and keep.get("webSocketDebuggerUrl"):
83
+ _ws_call(keep["webSocketDebuggerUrl"], "Page.navigate", url=REVIVE_URL)
84
+ print(f"[cleanup_harness_tabs] revived blank tab -> {REVIVE_URL}")
85
+ except Exception as e:
86
+ print(f"[cleanup_harness_tabs] revive skipped ({type(e).__name__}: {e})")
27
87
 
28
88
 
29
89
  def main() -> int:
@@ -33,8 +93,13 @@ def main() -> int:
33
93
  except Exception:
34
94
  return 0
35
95
  pages = [t for t in tabs if t.get("type") == "page"]
36
- if len(pages) <= 1:
37
- print(f"[cleanup_harness_tabs] {len(pages)} page tab(s), no cleanup needed")
96
+ if not pages:
97
+ print("[cleanup_harness_tabs] 0 page tab(s)")
98
+ _revive(None)
99
+ return 0
100
+ if len(pages) == 1:
101
+ print("[cleanup_harness_tabs] 1 page tab(s), no cleanup needed")
102
+ _revive(pages[0])
38
103
  return 0
39
104
  # Keep a REAL (http/https) tab when one exists, not blindly pages[0]. The
40
105
  # /json order is roughly most-recently-active first, so a freshly-spawned
@@ -43,9 +108,6 @@ def main() -> int:
43
108
  # daemon's tab forces it to re-attach and re-spawn another about:blank, which
44
109
  # is exactly the orphan-tab churn this script is meant to clean up. Falling
45
110
  # back to pages[0] preserves the prior behavior when every tab is blank.
46
- def _is_real(t):
47
- return (t.get("url") or "").startswith(("http://", "https://"))
48
-
49
111
  keep = next((t for t in pages if _is_real(t)), pages[0])
50
112
  closed = 0
51
113
  for t in pages:
@@ -61,6 +123,7 @@ def main() -> int:
61
123
  pass
62
124
  kept_kind = "1 real" if _is_real(keep) else "1"
63
125
  print(f"[cleanup_harness_tabs] closed {closed}/{len(pages) - 1} extra page tabs (kept {kept_kind})")
126
+ _revive(keep)
64
127
  return 0
65
128
 
66
129