@m13v/s4l 1.7.4-rc.4 → 1.7.4-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.
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +2 -2
- package/scripts/scan_x_profile.py +25 -7
- package/scripts/top_performers.py +34 -0
- package/skill/run-twitter-cycle.sh +28 -1
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.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": {
|
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.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.4-rc.
|
|
3
|
+
"version": "1.7.4-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",
|
|
@@ -160,4 +160,4 @@
|
|
|
160
160
|
"pg": "^8.20.0",
|
|
161
161
|
"ws": "^8.0.0"
|
|
162
162
|
}
|
|
163
|
-
}
|
|
163
|
+
}
|
|
@@ -324,13 +324,31 @@ def _own_posted_ids() -> set:
|
|
|
324
324
|
from http_api import api_get # noqa: PLC0415
|
|
325
325
|
import re
|
|
326
326
|
ids: set = set()
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
# The route caps limit at 500, so walk the FULL posting history with a
|
|
328
|
+
# forward posted_at cursor (order_dir=asc + since>=). On prolific
|
|
329
|
+
# accounts a single newest-500 page misses older S4L posts that the
|
|
330
|
+
# profile scan still reaches (bit the operator account: 12.8k posted
|
|
331
|
+
# statuses, 2 of 5 picked exemplars were S4L's own). The since filter
|
|
332
|
+
# is inclusive, so overlap rows dedupe via the set; a page whose max
|
|
333
|
+
# posted_at equals the cursor would loop and breaks instead.
|
|
334
|
+
cursor = None
|
|
335
|
+
for _ in range(80): # hard cap 40k statuses
|
|
336
|
+
q = {"platform": "twitter", "has_our_url": "true", "limit": 500,
|
|
337
|
+
"order_by": "posted_at", "order_dir": "asc"}
|
|
338
|
+
if cursor:
|
|
339
|
+
q["since"] = cursor
|
|
340
|
+
resp = api_get("/api/v1/posts", q)
|
|
341
|
+
rows = ((resp or {}).get("data") or {}).get("posts") or []
|
|
342
|
+
for r in rows:
|
|
343
|
+
m = re.search(r"/status/(\d+)", str(r.get("our_url") or ""))
|
|
344
|
+
if m:
|
|
345
|
+
ids.add(m.group(1))
|
|
346
|
+
if len(rows) < 500:
|
|
347
|
+
break
|
|
348
|
+
page_max = max((r.get("posted_at") or "" for r in rows), default="")
|
|
349
|
+
if not page_max or page_max == cursor:
|
|
350
|
+
break
|
|
351
|
+
cursor = page_max
|
|
334
352
|
return ids
|
|
335
353
|
except Exception as e:
|
|
336
354
|
print(f"[scan_x_profile] own-posts exclusion unavailable: {e}", file=sys.stderr)
|
|
@@ -567,9 +567,43 @@ def main():
|
|
|
567
567
|
"summary, no bottom posts). This is the lean "
|
|
568
568
|
"on-demand shape the drafting session calls "
|
|
569
569
|
"after routing a candidate to a project."))
|
|
570
|
+
parser.add_argument("--invoked-by", default=None,
|
|
571
|
+
help=("Caller tag recorded in the on-demand invocation "
|
|
572
|
+
"ledger (the drafting prompt passes the cycle's "
|
|
573
|
+
"batch_id). Tracking only; no behavior change."))
|
|
570
574
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
571
575
|
args = parser.parse_args()
|
|
572
576
|
|
|
577
|
+
# On-demand invocation ledger (2026-07-10). Any --project call is the
|
|
578
|
+
# on-demand per-project winners lookup the draft prompt offers; record it
|
|
579
|
+
# at the TOOL level (model self-reports are unreliable) so we can measure
|
|
580
|
+
# whether drafting sessions actually use the query. One JSON line per
|
|
581
|
+
# call in $S4L_STATE_DIR/top-performers-invocations.jsonl; the cycle
|
|
582
|
+
# counts lines for its batch_id after prep and logs a
|
|
583
|
+
# [project_top_performers] marker. Best-effort: never block the report.
|
|
584
|
+
if args.project:
|
|
585
|
+
try:
|
|
586
|
+
import datetime
|
|
587
|
+
state_dir = os.environ.get(
|
|
588
|
+
"S4L_STATE_DIR",
|
|
589
|
+
os.path.expanduser("~/.social-autoposter-mcp"))
|
|
590
|
+
os.makedirs(state_dir, exist_ok=True)
|
|
591
|
+
with open(os.path.join(state_dir,
|
|
592
|
+
"top-performers-invocations.jsonl"),
|
|
593
|
+
"a") as fh:
|
|
594
|
+
fh.write(json.dumps({
|
|
595
|
+
"ts": datetime.datetime.now(
|
|
596
|
+
datetime.timezone.utc).isoformat(),
|
|
597
|
+
"project": args.project,
|
|
598
|
+
"platform": args.platform,
|
|
599
|
+
"brief": bool(args.brief),
|
|
600
|
+
"top": args.top,
|
|
601
|
+
"invoked_by": args.invoked_by,
|
|
602
|
+
}) + "\n")
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
print(f"[top_performers] invocation ledger write failed: {exc!r}",
|
|
605
|
+
file=sys.stderr)
|
|
606
|
+
|
|
573
607
|
(summary, style_perf, top, bottom, fallback_top,
|
|
574
608
|
top_by_group, top_by_style) = _fetch_report_via_api(
|
|
575
609
|
platform=args.platform, project=args.project, top=args.top, bottom=args.bottom,
|
|
@@ -2047,7 +2047,7 @@ All project configs: $ALL_PROJECTS_JSON
|
|
|
2047
2047
|
|
|
2048
2048
|
## PROJECT TOP PERFORMERS (query on demand, do NOT skip routing first)
|
|
2049
2049
|
The feedback reports below carry a per-style exemplar only; project winners are no longer bulk-injected. AFTER you have decided which project a candidate's draft is for, you MAY pull that project's own recent winners (last 30 days, ranked by real click rate) when you are unsure how this product converts in replies:
|
|
2050
|
-
python3 $REPO_DIR/scripts/top_performers.py --platform twitter --project 'PROJECT_NAME' --top 3 --brief
|
|
2050
|
+
python3 $REPO_DIR/scripts/top_performers.py --platform twitter --project 'PROJECT_NAME' --top 3 --brief --invoked-by '$BATCH_ID'
|
|
2051
2051
|
(PROJECT_NAME exactly as it appears in the candidate's 'Project match' / config.json.) Treat the results as evidence of which CLAIMS and ANGLES landed for that product, never as structural templates: do not copy their sentence shape, opener, or pivot wording. One call per project at most; skip the call entirely for projects you already queried this session.
|
|
2052
2052
|
|
|
2053
2053
|
$RECENT_SELF_BLOCK
|
|
@@ -2364,6 +2364,33 @@ if [ "$PREP_PARSE_EXIT" -eq 0 ] && [ -f "$PLAN_FILE" ]; then
|
|
|
2364
2364
|
fi
|
|
2365
2365
|
log "Phase 2b-prep complete. plan_count=$PLAN_COUNT"
|
|
2366
2366
|
|
|
2367
|
+
# On-demand project-winners usage marker (2026-07-10). top_performers.py
|
|
2368
|
+
# appends a JSON line to the state-dir ledger on every --project call (the
|
|
2369
|
+
# draft prompt tells the model to pass --invoked-by "$BATCH_ID"). Count this
|
|
2370
|
+
# batch's lines and log a greppable marker so "did the drafting session
|
|
2371
|
+
# actually use the per-project query" is answerable from the cycle log alone.
|
|
2372
|
+
# Stderr-marker convention: format is load-bearing elsewhere; keep it stable.
|
|
2373
|
+
TP_ONDEMAND=$(python3 -c "
|
|
2374
|
+
import json, os, sys
|
|
2375
|
+
path = os.path.join(os.environ.get('S4L_STATE_DIR', os.path.expanduser('~/.social-autoposter-mcp')), 'top-performers-invocations.jsonl')
|
|
2376
|
+
n, projects = 0, []
|
|
2377
|
+
try:
|
|
2378
|
+
for line in open(path):
|
|
2379
|
+
try:
|
|
2380
|
+
r = json.loads(line)
|
|
2381
|
+
except Exception:
|
|
2382
|
+
continue
|
|
2383
|
+
if r.get('invoked_by') == '$BATCH_ID':
|
|
2384
|
+
n += 1
|
|
2385
|
+
p = r.get('project')
|
|
2386
|
+
if p and p not in projects:
|
|
2387
|
+
projects.append(p)
|
|
2388
|
+
except OSError:
|
|
2389
|
+
pass
|
|
2390
|
+
print(f'{n} projects={projects}')
|
|
2391
|
+
" 2>/dev/null || echo "0 projects=[]")
|
|
2392
|
+
log "[project_top_performers] batch=$BATCH_ID on_demand_invocations=$TP_ONDEMAND"
|
|
2393
|
+
|
|
2367
2394
|
# twitter-browser lock was already released right after thread-media capture
|
|
2368
2395
|
# (before the Claude drafting call above), since nothing from there through
|
|
2369
2396
|
# Phase 2b-gen touches the browser. Phase 2b-post re-acquires unconditionally
|