@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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.4-rc.4",
3
- "installedAt": "2026-07-11T00:05:38.650Z"
2
+ "version": "1.7.4-rc.9",
3
+ "installedAt": "2026-07-11T00:35:33.514Z"
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.4",
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.4",
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.4",
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
- resp = api_get("/api/v1/posts", {"platform": "twitter",
328
- "has_our_url": "true", "limit": 2000})
329
- rows = ((resp or {}).get("data") or {}).get("posts") or []
330
- for r in rows:
331
- m = re.search(r"/status/(\d+)", str(r.get("our_url") or ""))
332
- if m:
333
- ids.add(m.group(1))
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