@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 +12 -0
- package/mcp/dist/version.js +49 -2
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/s4l_box_update.sh +35 -5
- package/scripts/snapshot.py +53 -2
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,
|
package/mcp/dist/version.js
CHANGED
|
@@ -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, {
|
|
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 = [];
|
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.7-rc.
|
|
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
|
+
"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
|
@@ -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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
127
|
+
rels = json.loads(_raw or "[]")
|
|
98
128
|
except Exception:
|
|
99
129
|
rels = []
|
|
100
130
|
best = None
|
package/scripts/snapshot.py
CHANGED
|
@@ -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
|
|
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__:"):
|