@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 +12 -1
- package/mcp/dist/runtime.js +36 -0
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/reddit_browser.py +11 -3
- package/scripts/twitter_browser.py +15 -3
- package/skill/engage-dm-replies.sh +14 -1
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
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -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);
|
package/mcp/dist/version.json
CHANGED
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.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|