@m13v/s4l 1.7.4-rc.26 → 1.7.4-rc.28

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
@@ -22,7 +22,7 @@ import fs from "node:fs";
22
22
  import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
23
23
  import { applySetup, resolveProject, hasReadyProject, personaReady, listManagedProjectStatus, listProjectSettings, ensureShortLinksDefault, ensurePersonaProject, findPersonaProject, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, normalizeStringList, } from "./setup.js";
24
24
  import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
25
- import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, retryProvisionIfStalled, } from "./runtime.js";
25
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, ensureHarnessPatched, retryProvisionIfStalled, } from "./runtime.js";
26
26
  import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
27
27
  import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
28
28
  import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, captureMessage, flushSentry, startLogStreaming, flushLogs, logLine, checkVersionChange } from "./telemetry.js";
@@ -5107,6 +5107,17 @@ async function main() {
5107
5107
  if (ensureRuntimeProvisioned()) {
5108
5108
  console.error("[social-autoposter-mcp] owned runtime not ready; provisioning on boot");
5109
5109
  }
5110
+ // Vendored harness patch for ALREADY-ready runtimes: provision() is skipped
5111
+ // above when the runtime is ready, so a patch shipped in an rc would never
5112
+ // reach an existing install's harness checkout without this. Idempotent
5113
+ // (marker check) and best-effort; never blocks boot.
5114
+ void ensureHarnessPatched()
5115
+ .then((r) => {
5116
+ if (!(r.ok && r.detail === "already patched")) {
5117
+ console.error(`[social-autoposter-mcp] harness patch: ${r.ok ? "ok" : "skip"} (${r.detail})`);
5118
+ }
5119
+ })
5120
+ .catch(() => { });
5110
5121
  // Queue-backed drafting (2026-06-23): keep the two worker-task prompts current,
5111
5122
  // pre-approve their tools EAGERLY (before onboarding even creates the tasks, so
5112
5123
  // the first unattended fire can't stall), and (re)install the launchd kicker
@@ -589,6 +589,42 @@ export function ensureRuntimeProvisioned() {
589
589
  return false; // best-effort; a boot provision must never break startup
590
590
  }
591
591
  }
592
+ // Boot hook closing the provisioning gap for ALREADY-ready runtimes: provision()
593
+ // skips when runtimeReady(), so a vendored-patch update shipped in an rc never
594
+ // reaches an existing install's harness checkout (proved on the QA box: rc.26
595
+ // delivered scripts/patches/ but the checkout stayed unpatched, 2026-07-13).
596
+ // This runs on every MCP boot: if the fix marker is absent and the patch
597
+ // applies, apply it and reload the harness daemon so the running process picks
598
+ // up the new source (the uv tool install is EDITABLE, so patched files are
599
+ // live without a reinstall). Best-effort; never blocks startup.
600
+ export async function ensureHarnessPatched() {
601
+ try {
602
+ const adminPy = path.join(HARNESS_DIR, "src", "browser_harness", "admin.py");
603
+ if (!fs.existsSync(adminPy))
604
+ return { ok: false, detail: "harness not installed yet" };
605
+ if (fs.readFileSync(adminPy, "utf-8").includes("cdp_urlopen")) {
606
+ return { ok: true, detail: "already patched" };
607
+ }
608
+ const harnessPatch = path.join(MATERIALIZED_REPO, "scripts", "patches", "browser-harness-loopback-cdp-proxy.patch");
609
+ if (!fs.existsSync(harnessPatch))
610
+ return { ok: false, detail: "patch file missing from package" };
611
+ const chk = await sh("git", ["-C", HARNESS_DIR, "apply", "--check", harnessPatch], {
612
+ timeoutMs: 30000,
613
+ });
614
+ if (chk.code !== 0) {
615
+ return { ok: false, detail: "patch does not apply (dirty or diverged checkout)" };
616
+ }
617
+ const app = await sh("git", ["-C", HARNESS_DIR, "apply", harnessPatch], { timeoutMs: 30000 });
618
+ if (app.code !== 0)
619
+ return { ok: false, detail: `git apply failed (exit ${app.code})` };
620
+ // Reload the daemon so the long-lived process re-imports the patched source.
621
+ await sh(HARNESS_BIN, ["--reload"], { timeoutMs: 30000 });
622
+ return { ok: true, detail: "patched + daemon reloaded" };
623
+ }
624
+ catch (e) {
625
+ return { ok: false, detail: String(e?.message || e) };
626
+ }
627
+ }
592
628
  async function provision(progress) {
593
629
  const setStep = (id, status, detail) => {
594
630
  const st = progress.steps.find((s) => s.id === id);
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.4-rc.26",
3
- "installedAt": "2026-07-13T16:27:04.389Z"
2
+ "version": "1.7.4-rc.28",
3
+ "installedAt": "2026-07-13T21:52:37.483Z"
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.4-rc.26",
5
+ "version": "1.7.4-rc.28",
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.4-rc.26",
3
+ "version": "1.7.4-rc.28",
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.4-rc.26",
3
+ "version": "1.7.4-rc.28",
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",
@@ -1750,7 +1750,7 @@ def _log_dm_outbound(chat_url, content, dm_id=None, minted_codes=None):
1750
1750
  return False
1751
1751
 
1752
1752
 
1753
- def send_dm(chat_url, message, dm_id=None):
1753
+ def send_dm(chat_url, message, dm_id=None, apply_campaigns=True):
1754
1754
  """Send a message in a Reddit chat or PM thread.
1755
1755
 
1756
1756
  For chat URLs (reddit.com/chat/...), navigates to the chat room and
@@ -1792,8 +1792,12 @@ def send_dm(chat_url, message, dm_id=None):
1792
1792
  minted_link_codes = wrap_res.get("minted_codes", [])
1793
1793
 
1794
1794
  # Tool-level campaign suffix injection (guaranteed delivery of literal text).
1795
+ # apply_campaigns=False (via S4L_SKIP_CAMPAIGN_SUFFIX on the send-dm CLI)
1796
+ # opts this DM out — set ONLY for human-drafted escalation replies
1797
+ # (engage-dm-replies Phase 0); see twitter_browser.py send_dm for the
1798
+ # incident rationale. The autopilot never sets it.
1795
1799
  applied_campaigns = []
1796
- for cid, suffix, sample_rate in _load_active_reddit_campaigns_for_dm():
1800
+ for cid, suffix, sample_rate in (_load_active_reddit_campaigns_for_dm() if apply_campaigns else []):
1797
1801
  if random.random() < sample_rate:
1798
1802
  message = message + suffix
1799
1803
  applied_campaigns.append(cid)
@@ -2507,7 +2511,11 @@ def main():
2507
2511
  dm_id_arg = int(sys.argv[4])
2508
2512
  except ValueError:
2509
2513
  print(f"[send-dm] ignoring non-int dm_id arg: {sys.argv[4]!r}", file=sys.stderr)
2510
- result = send_dm(sys.argv[2], sys.argv[3], dm_id=dm_id_arg)
2514
+ # S4L_SKIP_CAMPAIGN_SUFFIX=1 opts this DM out of campaign suffixes.
2515
+ # Set ONLY by engage-dm-replies Phase 0 when delivering a human-drafted
2516
+ # escalation reply — see send_dm's apply_campaigns comment.
2517
+ _skip_camp = os.environ.get("S4L_SKIP_CAMPAIGN_SUFFIX", "").strip().lower() in ("1", "true", "yes")
2518
+ result = send_dm(sys.argv[2], sys.argv[3], dm_id=dm_id_arg, apply_campaigns=not _skip_camp)
2511
2519
  print(json.dumps(result, indent=2))
2512
2520
 
2513
2521
  elif cmd == "compose-dm":
@@ -2148,7 +2148,7 @@ def read_conversation(thread_url, max_messages=20):
2148
2148
  browser.close()
2149
2149
 
2150
2150
 
2151
- def send_dm(thread_url, message, dm_id=None):
2151
+ def send_dm(thread_url, message, dm_id=None, apply_campaigns=True):
2152
2152
  """Send a message in a Twitter/X DM conversation.
2153
2153
 
2154
2154
  Navigates to the thread URL, types the message in the compose box,
@@ -2186,8 +2186,16 @@ def send_dm(thread_url, message, dm_id=None):
2186
2186
  message = wrap_res["text"]
2187
2187
  minted_link_codes = wrap_res.get("minted_codes", [])
2188
2188
 
2189
+ # apply_campaigns=False (via S4L_SKIP_CAMPAIGN_SUFFIX on the send-dm CLI)
2190
+ # opts this DM out of active-campaign suffixes. Set ONLY when delivering a
2191
+ # HUMAN-drafted escalation reply (engage-dm-replies Phase 0): a human
2192
+ # answering an escalation — often an AI-callout — must not get a bot tell
2193
+ # coin-flipped onto their words (2026-07-13: the ' written with ai' suffix
2194
+ # landing on a hand-approved recovery DM to ngeloxyz helped burn the lead).
2195
+ # The autopilot never sets it, so the A/B experiment continues everywhere
2196
+ # else. Mirrors reply_to_tweet's apply_campaigns plumbing.
2189
2197
  applied_campaigns = []
2190
- for cid, suffix, sample_rate in _load_active_twitter_campaigns():
2198
+ for cid, suffix, sample_rate in (_load_active_twitter_campaigns() if apply_campaigns else []):
2191
2199
  if random.random() < sample_rate:
2192
2200
  # Wrap any URLs in the suffix through dm_short_links (DM rail) so
2193
2201
  # clicks attribute to this DM. Falls back to raw suffix if dm_id
@@ -2927,7 +2935,11 @@ def main():
2927
2935
  except ValueError:
2928
2936
  print(f"send-dm: dm_id must be int, got {sys.argv[4]!r}", file=sys.stderr)
2929
2937
  sys.exit(1)
2930
- result = send_dm(sys.argv[2], sys.argv[3], dm_id=dm_id_arg)
2938
+ # S4L_SKIP_CAMPAIGN_SUFFIX=1 opts this DM out of campaign suffixes.
2939
+ # Set ONLY by engage-dm-replies Phase 0 when delivering a human-drafted
2940
+ # escalation reply — see send_dm's apply_campaigns comment.
2941
+ _skip_camp = os.environ.get("S4L_SKIP_CAMPAIGN_SUFFIX", "").strip().lower() in ("1", "true", "yes")
2942
+ result = send_dm(sys.argv[2], sys.argv[3], dm_id=dm_id_arg, apply_campaigns=not _skip_camp)
2931
2943
  print(json.dumps(result, indent=2))
2932
2944
 
2933
2945
  elif cmd == "notifications":
@@ -302,12 +302,25 @@ The \`public_target_url\` field is THEIR public comment that originally led to t
302
302
 
303
303
  ### Step C. If \`reply_channel\` is \`dm\` or \`both\`: deliver the DM
304
304
 
305
+ **NO CAMPAIGN SUFFIX ON HUMAN REPLIES.** A row in this queue is a HUMAN answering
306
+ an escalation (often an AI-callout), so the campaign suffix (e.g. ' written with
307
+ ai') must NEVER be coin-flipped onto it — it re-burns the exact lead the human is
308
+ trying to recover (2026-07-13: the suffix landed on the hand-approved ngeloxyz
309
+ recovery DM and helped kill the thread). When sending via the python CLIs, ALWAYS
310
+ prefix the env opt-out:
311
+ \`\`\`bash
312
+ S4L_SKIP_CAMPAIGN_SUFFIX=1 python3 scripts/twitter_browser.py send-dm THREAD_URL "MESSAGE" DM_ID
313
+ S4L_SKIP_CAMPAIGN_SUFFIX=1 python3 scripts/reddit_browser.py send-dm CHAT_URL "MESSAGE" DM_ID
314
+ \`\`\`
315
+ This applies ONLY inside Phase 0 (human replies). Auto-replies in the later phases
316
+ keep the normal campaign behavior — do NOT carry this env var forward.
317
+
305
318
  1. Craft a natural DM based on the human's instructions and the conversation context.
306
319
  2. Navigate to the conversation on the correct platform using \`chat_url\` (or find the conversation with their_author).
307
320
  - **Reddit Chat** (mcp__reddit-harness__bh_run tool, CDP-driven real Chrome on port 9557)
308
321
  - **LinkedIn Messages** (mcp__linkedin-harness__bh_run tool, CDP-driven real Chrome on port 9556)
309
322
  - **X/Twitter DMs** (mcp__twitter-harness__bh_run tool), if encrypted DM passcode dialog appears, enter: $TWITTER_DM_PASSCODE
310
- 3. Type and send the crafted DM.
323
+ 3. Type and send the crafted DM (with the suffix opt-out env var when using the python send-dm CLIs, per the rule above).
311
324
  4. Log the outbound message (log what you ACTUALLY SENT, not the human's instructions). Pass --verified ONLY when the browser tool returned verified=true. If verification failed, log nothing and let the next cycle retry; never pass --verified speculatively:
312
325
  \`\`\`bash
313
326
  cd ~/social-autoposter && python3 scripts/dm_conversation.py log-outbound --dm-id DM_ID --content "THE_CRAFTED_DM_YOU_SENT" --verified