@m13v/s4l 1.7.7-rc.3 → 1.7.7-rc.4

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/repo.js CHANGED
@@ -71,6 +71,18 @@ export function run(cmd, args, opts = {}) {
71
71
  catch {
72
72
  /* a spawn observer must never break the run */
73
73
  }
74
+ if (opts.stdin != null) {
75
+ // Swallow async pipe errors (e.g. EPIPE when the child exits before
76
+ // reading) — an unhandled stream error would crash the whole process.
77
+ child.stdin.on("error", () => { });
78
+ try {
79
+ child.stdin.write(opts.stdin);
80
+ child.stdin.end();
81
+ }
82
+ catch {
83
+ /* a closed stdin must never break the run */
84
+ }
85
+ }
74
86
  let stdout = "";
75
87
  let stderr = "";
76
88
  // Per-stream partial-line buffers so onLine fires on whole lines only,
@@ -195,16 +195,63 @@ async function latestFromGithubRedirect() {
195
195
  return null;
196
196
  }
197
197
  }
198
+ // Optional GitHub token (2026-07-30): authenticated probes get 5000/h instead
199
+ // of the anonymous 60/h-per-IP quota that silenced the staging update banner
200
+ // on 2026-07-13 and again on 2026-07-30. Sources, in order: GITHUB_TOKEN /
201
+ // GH_TOKEN env, then `gh auth token` when the gh CLI exists (dev/operator
202
+ // machines; .mcpb boxes have neither and resolve to null instantly). The token
203
+ // is only ever sent to api.github.com, always via stdin (curl -H @-) so it
204
+ // never appears in argv/`ps`, and the `gh auth token` shell-out is noTee so it
205
+ // never reaches the telemetry relay. Keep in lockstep with
206
+ // scripts/snapshot.py::_github_token.
207
+ let ghTokCache = { at: 0, tok: null };
208
+ const GH_TOK_TTL_MS = 900_000;
209
+ async function githubToken() {
210
+ const now = Date.now();
211
+ if (ghTokCache.at && now - ghTokCache.at < GH_TOK_TTL_MS)
212
+ return ghTokCache.tok;
213
+ let tok = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || null;
214
+ if (!tok) {
215
+ for (const gh of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "gh"]) {
216
+ const res = await run(gh, ["auth", "token"], { timeoutMs: 5000, noTee: true });
217
+ if (res.code === -1)
218
+ continue; // not spawnable at this path; try the next
219
+ const cand = (res.stdout || "").trim();
220
+ if (res.code === 0 && cand)
221
+ tok = cand;
222
+ break;
223
+ }
224
+ }
225
+ ghTokCache = { at: now, tok };
226
+ return tok;
227
+ }
198
228
  // Conditional-request state lives in the SHARED cache file (latest-release.json)
199
229
  // so the ETag survives process boundaries: short-lived MCP respawns used to pay
200
230
  // a full 200 per process; now every probe sends If-None-Match and gets a free
201
- // 304 between releases.
231
+ // 304 between releases. Probes authenticate when a GitHub token is available;
232
+ // a 401 (revoked/expired token) retries anonymously so a bad token is never
233
+ // worse than no token.
202
234
  async function curlConditional(url, etag) {
235
+ const tok = await githubToken();
236
+ const first = await curlOnce(url, etag, tok);
237
+ if (first.status === 401 && tok) {
238
+ ghTokCache = { at: Date.now(), tok: null }; // drop the dead token
239
+ return curlOnce(url, etag, null);
240
+ }
241
+ return first;
242
+ }
243
+ async function curlOnce(url, etag, token) {
203
244
  const args = ["-sS", "-m", "10", "-H", "Accept: application/vnd.github+json"];
245
+ if (token)
246
+ args.push("-H", "@-"); // Authorization arrives via stdin, never argv
204
247
  if (etag)
205
248
  args.push("-H", `If-None-Match: ${etag}`);
206
249
  args.push("-w", "\n__CURL_STATUS__:%{http_code}\n__CURL_ETAG__:%header{etag}", url);
207
- const res = await run("curl", args, { timeoutMs: 12000, noTee: true });
250
+ const res = await run("curl", args, {
251
+ timeoutMs: 12000,
252
+ noTee: true,
253
+ stdin: token ? `Authorization: Bearer ${token}` : undefined,
254
+ });
208
255
  let status = 0;
209
256
  let newEtag = null;
210
257
  const body = [];
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.7-rc.3",
3
- "installedAt": "2026-07-30T22:30:41.307Z"
2
+ "version": "1.7.7-rc.4",
3
+ "installedAt": "2026-07-30T23:35:31.407Z"
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.7-rc.3",
5
+ "version": "1.7.7-rc.4",
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.7-rc.3",
3
+ "version": "1.7.7-rc.4",
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.7-rc.3",
3
+ "version": "1.7.7-rc.4",
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",
@@ -73,11 +73,36 @@ channel = ch if ch in ("stable", "staging") else "stable"
73
73
  REPO = "m13v/s4l"
74
74
  TAG_DL = "https://github.com/%s/releases/download/%s/social-autoposter.mcpb"
75
75
 
76
- def curl(url):
76
+ # Optional GitHub token (2026-07-30): authenticated requests get 5000/h vs the
77
+ # anonymous 60/h-per-IP quota (which, exhausted, makes this resolver fail
78
+ # closed). Sources: GITHUB_TOKEN / GH_TOKEN env, then `gh auth token` when the
79
+ # gh CLI exists (boxes have neither; resolves to None instantly). Sent via
80
+ # stdin (-H @-), never argv. Keep in lockstep with snapshot.py::_github_token.
81
+ def gh_token():
82
+ tok = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or None
83
+ if not tok:
84
+ for gh in ("/opt/homebrew/bin/gh", "/usr/local/bin/gh", "gh"):
85
+ try:
86
+ r = subprocess.run([gh, "auth", "token"],
87
+ capture_output=True, text=True, timeout=5)
88
+ cand = (r.stdout or "").strip()
89
+ if r.returncode == 0 and cand:
90
+ tok = cand
91
+ break
92
+ except FileNotFoundError:
93
+ continue
94
+ except Exception:
95
+ break
96
+ return tok
97
+
98
+ def curl(url, token=None):
77
99
  try:
78
- r = subprocess.run(["/usr/bin/curl", "-fsSL", "-m", "15",
79
- "-H", "Accept: application/vnd.github+json", url],
80
- capture_output=True, text=True, timeout=20)
100
+ args = ["/usr/bin/curl", "-fsSL", "-m", "15",
101
+ "-H", "Accept: application/vnd.github+json"]
102
+ if token:
103
+ args += ["-H", "@-"]
104
+ r = subprocess.run(args + [url], capture_output=True, text=True, timeout=20,
105
+ input=("Authorization: Bearer %s" % token) if token else None)
81
106
  return r.stdout if r.returncode == 0 else ""
82
107
  except Exception:
83
108
  return ""
@@ -93,8 +118,13 @@ def ver_key(v):
93
118
  m = re.findall(r"\d+", pre)
94
119
  return (nums[0], nums[1], nums[2], 0, int(m[-1]) if m else 0)
95
120
 
121
+ _tok = gh_token()
122
+ _url = "https://api.github.com/repos/%s/releases?per_page=30" % REPO
123
+ _raw = curl(_url, _tok)
124
+ if not _raw and _tok:
125
+ _raw = curl(_url) # bad token must never be worse than anonymous
96
126
  try:
97
- rels = json.loads(curl("https://api.github.com/repos/%s/releases?per_page=30" % REPO) or "[]")
127
+ rels = json.loads(_raw or "[]")
98
128
  except Exception:
99
129
  rels = []
100
130
  best = None
@@ -482,18 +482,69 @@ def _latest_from_github_redirect():
482
482
  return None
483
483
 
484
484
 
485
+ # ---- optional GitHub token (2026-07-30) -----------------------------------
486
+ # Authenticated probes get a 5000/h quota instead of the anonymous 60/h-per-IP
487
+ # quota that silenced the staging update banner on 2026-07-13 and again on
488
+ # 2026-07-30 (rate-limited staging probe degraded to the prerelease-blind
489
+ # releases/latest redirect, so a staging box resolved stable and never saw the
490
+ # rc). Sources, in order: GITHUB_TOKEN / GH_TOKEN env, then `gh auth token`
491
+ # when the gh CLI exists (dev/operator machines; .mcpb boxes have neither and
492
+ # resolve to None instantly). The token is only ever sent to api.github.com,
493
+ # always via stdin (-H @-) so it never appears in `ps` or logs. Keep in
494
+ # lockstep with mcp/src/version.ts::githubToken.
495
+ _gh_tok_cache = {"at": 0.0, "tok": None}
496
+ _GH_TOK_TTL = 900.0
497
+
498
+
499
+ def _github_token():
500
+ now = time.time()
501
+ if _gh_tok_cache["at"] and now - _gh_tok_cache["at"] < _GH_TOK_TTL:
502
+ return _gh_tok_cache["tok"]
503
+ tok = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or None
504
+ if not tok:
505
+ for gh in ("/opt/homebrew/bin/gh", "/usr/local/bin/gh", "gh"):
506
+ try:
507
+ res = subprocess.run([gh, "auth", "token"],
508
+ capture_output=True, text=True, timeout=5)
509
+ cand = (res.stdout or "").strip()
510
+ if res.returncode == 0 and cand:
511
+ tok = cand
512
+ break
513
+ except FileNotFoundError:
514
+ continue
515
+ except Exception:
516
+ break
517
+ _gh_tok_cache.update(at=now, tok=tok)
518
+ return tok
519
+
520
+
485
521
  # Conditional-request state lives in the SHARED cache file (latest-release.json)
486
522
  # so the ETag survives process boundaries: short-lived shell-outs used to pay a
487
523
  # full 200 per process; now every probe sends If-None-Match and gets a free 304
488
524
  # between releases.
489
525
  def _curl_conditional(url, etag):
490
- """GET url with optional If-None-Match. Returns (status, new_etag, body)."""
526
+ """GET url with optional If-None-Match, authenticated when a GitHub token
527
+ is available. Returns (status, new_etag, body). On 401 with a token
528
+ (revoked/expired) retries anonymously so a bad token is never worse than
529
+ no token."""
530
+ tok = _github_token()
531
+ status, new_etag, body = _curl_once(url, etag, tok)
532
+ if status == 401 and tok:
533
+ _gh_tok_cache.update(tok=None) # drop the dead token for this process
534
+ status, new_etag, body = _curl_once(url, etag, None)
535
+ return status, new_etag, body
536
+
537
+
538
+ def _curl_once(url, etag, token):
491
539
  args = ["/usr/bin/curl", "-sS", "-m", "10",
492
540
  "-H", "Accept: application/vnd.github+json"]
541
+ if token:
542
+ args += ["-H", "@-"] # Authorization arrives via stdin, never argv
493
543
  if etag:
494
544
  args += ["-H", "If-None-Match: %s" % etag]
495
545
  args += ["-w", "\n__CURL_STATUS__:%{http_code}\n__CURL_ETAG__:%header{etag}", url]
496
- res = subprocess.run(args, capture_output=True, text=True, timeout=12)
546
+ res = subprocess.run(args, capture_output=True, text=True, timeout=12,
547
+ input=("Authorization: Bearer %s" % token) if token else None)
497
548
  status, new_etag, body = 0, None, []
498
549
  for line in (res.stdout or "").splitlines():
499
550
  if line.startswith("__CURL_STATUS__:"):