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.
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +41 -3
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/log_post.py +11 -0
- package/scripts/s4l_box_update.sh +16 -3
- package/scripts/twitter_post_plan.py +7 -0
- package/skill/run-twitter-cycle.sh +30 -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.6.
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
package/scripts/log_post.py
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
-
|
|
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.
|