social-autoposter 1.6.145 → 1.6.147

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.6.145",
3
- "installedAt": "2026-06-29T21:07:58.592Z"
2
+ "version": "1.6.147",
3
+ "installedAt": "2026-06-29T21:43:27.026Z"
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.6.145",
5
+ "version": "1.6.147",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 end to end**\n\n2\\. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
@@ -501,11 +501,19 @@ class S4LMenuBar(rumps.App):
501
501
  except Exception:
502
502
  continue
503
503
  low = txt.lower()
504
+ # (a) Human "usage limit reached" prose (the older shape).
504
505
  if "weekly limit" in low or "usage limit" in low or "hit your limit" in low:
505
506
  import re
506
507
  m = re.search(r"resets [^\"\\]{0,40}", txt)
507
508
  limit_msg = m.group(0).strip().rstrip(".") if m else "Claude usage limit reached"
508
509
  break
510
+ # (b) HTTP-429 rate_limit api_error — the shape the Claude Desktop
511
+ # routines lane ACTUALLY returns when the account is over its limit.
512
+ # The prose strings above never appear in this case; the routine
513
+ # transcript carries {"error":"rate_limit",...,"apiErrorStatus":429}.
514
+ if '"apierrorstatus":429' in low or '"error":"rate_limit"' in low:
515
+ limit_msg = "Claude rate limit reached (429)"
516
+ break
509
517
  except Exception:
510
518
  pass
511
519
  return ran, limit_msg
@@ -1019,6 +1027,22 @@ class S4LMenuBar(rumps.App):
1019
1027
  schedule_state = self._schedule_state()
1020
1028
  self._schedule_state_cache = schedule_state
1021
1029
  attention = setup_complete and schedule_state in ("missing", "disabled")
1030
+ # Routines-lane rate limit (429): the draft tasks ARE registered and firing
1031
+ # for this account, but every run dies on a Claude rate limit, so nothing
1032
+ # drafts. Re-arm can't fix that — surface it as its own ⚠ attention state
1033
+ # with a "rate-limited" reason. Only meaningful when the schedule is firing
1034
+ # ('ok'); the missing/disabled case already owns the ⚠. Throttled (~30s):
1035
+ # scanning the worker-transcript bucket is glob-heavy and changes slowly.
1036
+ if setup_complete and schedule_state == "ok":
1037
+ now_rl = time.time()
1038
+ if now_rl - getattr(self, "_rl_checked_at", 0.0) >= 30:
1039
+ self._rl_checked_at = now_rl
1040
+ reason, msg = self._stall_reason()
1041
+ self._stall_reason_info = (reason, msg) if reason == "rate_limited" else ("", "")
1042
+ if self._stall_reason_info[0] == "rate_limited":
1043
+ attention = True
1044
+ else:
1045
+ self._stall_reason_info = ("", "")
1022
1046
  # Drop the stale "drafting" spinner while we need attention so the ⚠ shows.
1023
1047
  self._stalled = attention
1024
1048
 
@@ -1035,7 +1059,13 @@ class S4LMenuBar(rumps.App):
1035
1059
  self._last_blocker_code = blocker_code
1036
1060
  # Notify once per episode (the draft schedule isn't running for this account).
1037
1061
  if attention and not self._stall_notified:
1038
- if schedule_state == "disabled":
1062
+ if self._stall_reason_info[0] == "rate_limited":
1063
+ self._notify(
1064
+ "S4L Claude rate-limited",
1065
+ "Drafts can’t run — this Claude account hit its rate limit. "
1066
+ + (self._stall_reason_info[1] or "Wait for the limit to reset or switch account."),
1067
+ )
1068
+ elif schedule_state == "disabled":
1039
1069
  self._notify(
1040
1070
  "S4L draft tasks disabled",
1041
1071
  "The draft tasks are scheduled but disabled. Open the S4L menu → "
@@ -1337,11 +1367,19 @@ class S4LMenuBar(rumps.App):
1337
1367
  # When the schedule IS firing (ok), attention is False and nothing shows here
1338
1368
  # — a firing autopilot reads as healthy even if no draft has drained yet.
1339
1369
  if attention:
1340
- if schedule_state == "disabled":
1370
+ if self._stall_reason_info[0] == "rate_limited":
1371
+ # Routines fire but every run dies on a Claude rate limit (429).
1372
+ # Re-arm can't fix this, so don't offer it — just say what's wrong.
1373
+ items.append(self._label("⚠ Claude rate-limited — drafts can’t run"))
1374
+ items.append(self._label(
1375
+ " " + (self._stall_reason_info[1] or "wait for reset or switch account")
1376
+ ))
1377
+ elif schedule_state == "disabled":
1341
1378
  items.append(self._label("⚠ Draft tasks are scheduled but disabled"))
1379
+ items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
1342
1380
  else:
1343
1381
  items.append(self._label("⚠ Draft tasks aren’t scheduled on this account"))
1344
- items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
1382
+ items.append(rumps.MenuItem("Set up draft schedule for this account", callback=self._rearm))
1345
1383
  items.append(rumps.separator)
1346
1384
 
1347
1385
  if not runtime_ready:
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.145",
3
+ "version": "1.6.147",
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": "social-autoposter",
3
- "version": "1.6.145",
3
+ "version": "1.6.147",
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"
@@ -436,6 +436,15 @@ def main():
436
436
  "longer passes this flag; keep it for old rows and "
437
437
  "manual/backfill writes to posts.length_arm. Expected "
438
438
  "values: 'treatment' or 'control'.")
439
+ parser.add_argument("--draft-prompt-variant", default=None,
440
+ help="Draft-prompt A/B arm for Twitter posts: 'treatment' "
441
+ "(decoupled draft directive; reply stands on its own, "
442
+ "no forced concede->pivot to product) or 'control' "
443
+ "(current directive). Assigned per cycle in "
444
+ "run-twitter-cycle.sh and read from SAPS_DRAFT_PROMPT_VARIANT "
445
+ "by twitter_post_plan.py. NULL for non-Twitter rows and "
446
+ "rows pre-dating the experiment. Stored in "
447
+ "posts.draft_prompt_variant.")
439
448
  parser.add_argument("--urns", default=None,
440
449
  help="LinkedIn-only: comma- or whitespace-separated list "
441
450
  "of 16-19 digit URN IDs that identify this post "
@@ -574,6 +583,8 @@ def main():
574
583
  body["link_source"] = args.link_source
575
584
  if args.tail_link_variant:
576
585
  body["tail_link_variant"] = args.tail_link_variant
586
+ if args.draft_prompt_variant:
587
+ body["draft_prompt_variant"] = args.draft_prompt_variant
577
588
  if args.target_chars:
578
589
  body["target_chars"] = args.target_chars
579
590
  if args.length_arm:
@@ -66,9 +66,22 @@ trap 'rm -rf "$tmpd"' EXIT
66
66
  mcpb="$tmpd/social-autoposter.mcpb"
67
67
 
68
68
  echo "downloading $MCPB_URL ..."
69
- curl -fLs -m 300 "$MCPB_URL" -o "$mcpb" || { echo "download failed" >&2; exit 2; }
70
- sz=$(stat -f%z "$mcpb" 2>/dev/null || echo 0)
71
- [ "$sz" -ge 100000 ] || { echo "download too small ($sz bytes), aborting" >&2; exit 2; }
69
+ # Retry: a freshly-cut GitHub release's asset download endpoint 404s for up to a
70
+ # couple minutes while the CDN propagates (the release API shows the tag/asset as
71
+ # "uploaded" before the download URL serves it). A single curl loses that race and
72
+ # the update silently "fails." Retry with backoff so the standard pipeline is
73
+ # robust to that window.
74
+ sz=0
75
+ for attempt in 1 2 3 4 5 6 7 8 9 10; do
76
+ if curl -fLs -m 300 "$MCPB_URL" -o "$mcpb" 2>/dev/null; then
77
+ sz=$(stat -f%z "$mcpb" 2>/dev/null || echo 0)
78
+ [ "$sz" -ge 100000 ] && break
79
+ fi
80
+ echo " download attempt $attempt not ready yet (asset propagating); retrying in 15s..." >&2
81
+ sz=0
82
+ sleep 15
83
+ done
84
+ [ "$sz" -ge 100000 ] || { echo "download failed after retries (asset never became available)" >&2; exit 2; }
72
85
 
73
86
  echo "unpacking into extension dir ..."
74
87
  unzip -oq "$mcpb" -d "$EXT_DIR" || { echo "unpack failed" >&2; exit 3; }
@@ -727,6 +727,13 @@ def post_one(c: dict, picker_assignment: dict | None = None) -> tuple[str, str]:
727
727
  log_args += ["--search-topic", search_topic]
728
728
  if tail_link_variant:
729
729
  log_args += ["--tail-link-variant", tail_link_variant]
730
+ # Draft-prompt A/B arm: assigned ONCE per cycle in run-twitter-cycle.sh and
731
+ # exported as SAPS_DRAFT_PROMPT_VARIANT, so every post this cycle inherits the
732
+ # same arm (the whole prep batch shared one draft directive). Stamp it onto
733
+ # posts.draft_prompt_variant, mirroring tail_link_variant.
734
+ draft_prompt_variant = os.environ.get("SAPS_DRAFT_PROMPT_VARIANT") or None
735
+ if draft_prompt_variant:
736
+ log_args += ["--draft-prompt-variant", draft_prompt_variant]
730
737
  # LENGTH A/B concluded 2026-06-04; future production posts are no longer
731
738
  # stamped into posts.length_arm so the archived experiment readout stays
732
739
  # frozen to the actual test window.
@@ -1762,6 +1762,35 @@ except Exception:
1762
1762
  " 2>/dev/null)
1763
1763
  log "Engagement style assigned: mode=$PICKED_MODE style=${PICKED_STYLE:-(invent)}"
1764
1764
 
1765
+ # --- Draft-prompt A/B: decouple product pivot (2026-06-29) -------------------
1766
+ # Per-CYCLE arm (the prep session drafts the whole batch from ONE prompt, so
1767
+ # assignment is at cycle granularity, not per post; the whole batch shares it).
1768
+ # control = the current draft directive verbatim.
1769
+ # treatment = "decoupled" wording: the reply must stand on its own and NOT be
1770
+ # built as a concede-the-obvious-then-pivot-to-the-product setup;
1771
+ # product mentioned only when genuinely relevant.
1772
+ # The arm is stamped onto every post this cycle via SAPS_DRAFT_PROMPT_VARIANT
1773
+ # (read by twitter_post_plan.py -> log_post.py -> posts.draft_prompt_variant),
1774
+ # mirroring the tail_link_variant plumbing. Split tunable via
1775
+ # TWITTER_DRAFT_PROMPT_AB_RATE (fraction assigned to 'treatment', default 0.5).
1776
+ DRAFT_PROMPT_AB_RATE="${TWITTER_DRAFT_PROMPT_AB_RATE:-0.5}"
1777
+ SAPS_DRAFT_PROMPT_VARIANT=$(python3 -c "
1778
+ import random
1779
+ try:
1780
+ rate = float('$DRAFT_PROMPT_AB_RATE')
1781
+ except Exception:
1782
+ rate = 0.5
1783
+ rate = min(1.0, max(0.0, rate))
1784
+ print('treatment' if random.random() < rate else 'control')
1785
+ " 2>/dev/null || echo control)
1786
+ export SAPS_DRAFT_PROMPT_VARIANT
1787
+ log "Draft-prompt A/B arm: $SAPS_DRAFT_PROMPT_VARIANT (rate=$DRAFT_PROMPT_AB_RATE)"
1788
+ if [ "$SAPS_DRAFT_PROMPT_VARIANT" = "treatment" ]; then
1789
+ DRAFT_DIRECTIVE="Otherwise: draft a direct, natural reply that stands on its own as a useful contribution to the thread. Mention the matched project only when it is genuinely the most relevant thing to say, and state it plainly in one clause; most replies will not need it. Do NOT build the reply as a concede-the-obvious-then-pivot-to-the-product setup. Length is governed ENTIRELY by the per-style LENGTH LIMIT in the style block above; obey that target and ceiling, do not apply any other length rule here. NEVER em dashes. Apply the matched project's \`voice\` block from ALL_PROJECTS_JSON: follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present."
1790
+ else
1791
+ DRAFT_DIRECTIVE="Otherwise: draft a reply using the best engagement style. Length is governed ENTIRELY by the per-style LENGTH LIMIT in the style block above; obey that target and ceiling, do not apply any other length rule here. NEVER em dashes. Apply the matched project's \`voice\` block from ALL_PROJECTS_JSON: follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present."
1792
+ fi
1793
+
1765
1794
  if [ -n "$PICKED_STYLE" ]; then
1766
1795
  TOP_REPORT=$(python3 "$REPO_DIR/scripts/top_performers.py" --platform twitter --style "$PICKED_STYLE" 2>/dev/null || echo "(top performers report unavailable)")
1767
1796
  else
@@ -1923,7 +1952,7 @@ For each chosen candidate:
1923
1952
  2. Understand the context from that inlined text (you have no browser; everything you need to draft is already in this prompt).
1924
1953
  3. DRAFT HANDLING (existing vs fresh):
1925
1954
  - If the candidate block shows an EXISTING DRAFT line AND draft age < 30 minutes, REUSE the draft text verbatim. Set engagement_style to the existing style. Do NOT call log_draft.py; do NOT redraft. Reason: prior cycle paid the LLM cost.
1926
- - Otherwise: draft a reply using the best engagement style. Length is governed ENTIRELY by the per-style LENGTH LIMIT in the style block above; obey that target and ceiling, do not apply any other length rule here. NEVER em dashes. Apply the matched project's \`voice\` block from ALL_PROJECTS_JSON: follow voice.tone, never violate voice.never, mirror voice.examples / voice.examples_good when present.
1955
+ - $DRAFT_DIRECTIVE
1927
1956
  3a. PERSIST FRESH DRAFTS (skip for reused drafts):
1928
1957
  python3 $REPO_DIR/scripts/log_draft.py --candidate-id CANDIDATE_ID --text 'YOUR_REPLY_TEXT' --style STYLE --assigned-style '$PICKED_STYLE' --assigned-mode '$PICKED_MODE'
1929
1958
  The --assigned-style / --assigned-mode flags carry the orchestrator's picker output (this cycle: mode=$PICKED_MODE style='${PICKED_STYLE:-(invent)}') into the candidate row so the post pipeline can coerce drift and register invented styles. Pass them VERBATIM as shown.