social-autoposter 1.6.129 → 1.6.130
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/index.js +5 -1
- package/mcp/dist/repo.js +2 -2
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +160 -34
- package/mcp/package.json +1 -1
- package/package.json +1 -1
package/mcp/dist/index.js
CHANGED
|
@@ -2045,7 +2045,11 @@ async function autopilotLoaded() {
|
|
|
2045
2045
|
}
|
|
2046
2046
|
let auto_update_on = false;
|
|
2047
2047
|
try {
|
|
2048
|
-
|
|
2048
|
+
// noTee: this status probe dumps the entire launchd job table (hundreds of
|
|
2049
|
+
// lines) and fires on every dashboard/status poll — teeing it flooded Cloud
|
|
2050
|
+
// Logging (~98% of an install's log volume). We only need the substring
|
|
2051
|
+
// check, so keep the output in-memory and out of the relay. (2026-06-28)
|
|
2052
|
+
const res = await run("launchctl", ["list"], { timeoutMs: 10_000, noTee: true });
|
|
2049
2053
|
auto_update_on = res.stdout.split("\n").some((l) => l.includes(UPDATER_LABEL));
|
|
2050
2054
|
}
|
|
2051
2055
|
catch {
|
package/mcp/dist/repo.js
CHANGED
|
@@ -100,7 +100,7 @@ export function run(cmd, args, opts = {}) {
|
|
|
100
100
|
let sinkOutBuf = "";
|
|
101
101
|
let sinkErrBuf = "";
|
|
102
102
|
const sinkPump = (chunk, which, buf) => {
|
|
103
|
-
const sink = lineSink;
|
|
103
|
+
const sink = opts.noTee ? null : lineSink;
|
|
104
104
|
if (!sink)
|
|
105
105
|
return buf;
|
|
106
106
|
buf += chunk;
|
|
@@ -155,7 +155,7 @@ export function run(cmd, args, opts = {}) {
|
|
|
155
155
|
/* ignore */
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
const sink = lineSink;
|
|
158
|
+
const sink = opts.noTee ? null : lineSink;
|
|
159
159
|
if (sink) {
|
|
160
160
|
if (sinkOutBuf)
|
|
161
161
|
try {
|
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.130",
|
|
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": {
|
|
@@ -112,21 +112,12 @@ SETUP_PROMPT = (
|
|
|
112
112
|
)
|
|
113
113
|
DRAFT_PROMPT = "Run a social-autoposter draft cycle and show me the drafts to review."
|
|
114
114
|
UPDATE_PROMPT = "Update social-autoposter to the latest version."
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
# like Setup/Draft do. Targeted: it does NOT redo X/login/project setup.
|
|
122
|
-
REARM_PROMPT = (
|
|
123
|
-
"The social-autoposter draft autopilot stopped running (its scheduled-task "
|
|
124
|
-
"routines are missing under this Claude account). Re-arm it now: call "
|
|
125
|
-
"queue_setup, then for EACH returned task call the host tool "
|
|
126
|
-
"create_scheduled_task with its taskId, cronExpression, and prompt verbatim, "
|
|
127
|
-
"then call run_draft_cycle once to verify a draft card appears. Do not redo my "
|
|
128
|
-
"X connection or project setup."
|
|
129
|
-
)
|
|
115
|
+
# NOTE: re-arm does NOT drive the Claude UI. Pasting a prompt to make the agent
|
|
116
|
+
# call create_scheduled_task proved unreliable (keystroke automation into Electron
|
|
117
|
+
# races focus/timing even with Accessibility granted). Instead, re-arm writes the
|
|
118
|
+
# two worker routines straight into the host's scheduled-tasks.json registry during
|
|
119
|
+
# a Claude restart (the same down-window mechanism the cwd-rewrite already uses) —
|
|
120
|
+
# deterministic, no Accessibility, works headless. See _rearm_work().
|
|
130
121
|
|
|
131
122
|
# A pending draft job older than this (seconds) with nothing claiming it means no
|
|
132
123
|
# routine is draining the queue — the worker would claim within a minute if it
|
|
@@ -339,21 +330,161 @@ class S4LMenuBar(rumps.App):
|
|
|
339
330
|
self._send_to_claude(DRAFT_PROMPT)
|
|
340
331
|
|
|
341
332
|
def _rearm(self, _=None):
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
Claude
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
333
|
+
"""One-click recovery: re-create the draft autopilot's scheduled-task
|
|
334
|
+
routines under the CURRENT Claude account, deterministically — no GUI
|
|
335
|
+
keystrokes, no Accessibility. The host caches its registry in memory and
|
|
336
|
+
clobbers live edits, so we must edit while Claude is DOWN; this therefore
|
|
337
|
+
restarts Claude. Heavy work + the restart run off the main thread."""
|
|
338
|
+
self._notify("S4L", "Re-arming autopilot… Claude will restart briefly.")
|
|
339
|
+
threading.Thread(target=self._rearm_work, daemon=True).start()
|
|
340
|
+
|
|
341
|
+
def _rearm_work(self):
|
|
342
|
+
try:
|
|
343
|
+
# Bring Claude down so registry edits stick (it reloads the file fresh
|
|
344
|
+
# at launch; a live edit is clobbered on the next scheduler fire).
|
|
345
|
+
subprocess.run(["osascript", "-e", 'tell application "Claude" to quit'],
|
|
346
|
+
capture_output=True, timeout=20)
|
|
347
|
+
time.sleep(4)
|
|
348
|
+
subprocess.run(["killall", "Claude"], capture_output=True)
|
|
349
|
+
time.sleep(2)
|
|
350
|
+
subprocess.run(["killall", "-9", "Claude"], capture_output=True)
|
|
351
|
+
time.sleep(1)
|
|
352
|
+
n = self._ensure_routines_registered()
|
|
353
|
+
# Keep cwd correct + drop the deprecated task while we're down.
|
|
354
|
+
self._rewrite_scheduled_task_cwd()
|
|
355
|
+
subprocess.run(["open", "-a", CLAUDE_APP], capture_output=True, timeout=20)
|
|
356
|
+
self._sig = None
|
|
357
|
+
if n:
|
|
358
|
+
self._notify(
|
|
359
|
+
"S4L re-armed",
|
|
360
|
+
"Draft autopilot routines restored. Drafts resume within a few minutes.",
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
self._notify(
|
|
364
|
+
"S4L",
|
|
365
|
+
"Couldn't find where to register routines. Try again, or sign back "
|
|
366
|
+
"into the original Claude account.",
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
self._notify("S4L re-arm failed", str(e)[:140])
|
|
370
|
+
_capture(e, phase="rearm")
|
|
371
|
+
|
|
372
|
+
# ---- re-arm: write routines straight into the host registry -----------
|
|
373
|
+
def _worker_record_templates(self):
|
|
374
|
+
"""Clone existing host-authored worker records (so the shape is EXACTLY what
|
|
375
|
+
the scheduler accepts). Returns {taskId: record_dict}. Empty if none found
|
|
376
|
+
(e.g. a box that never had them)."""
|
|
377
|
+
out = {}
|
|
378
|
+
try:
|
|
379
|
+
for f in glob.glob(SCHED_REGISTRY_GLOB):
|
|
380
|
+
try:
|
|
381
|
+
with open(f) as fh:
|
|
382
|
+
d = json.load(fh)
|
|
383
|
+
except Exception:
|
|
384
|
+
continue
|
|
385
|
+
for t in d.get("scheduledTasks", []) or []:
|
|
386
|
+
tid = t.get("id")
|
|
387
|
+
if tid in WORKER_TASK_IDS and tid not in out:
|
|
388
|
+
out[tid] = dict(t)
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
return out
|
|
392
|
+
|
|
393
|
+
def _make_worker_record(self, tid, template):
|
|
394
|
+
"""Build a routine record. Prefer cloning a host-authored template (exact
|
|
395
|
+
accepted shape); override the fields that must be right for this account."""
|
|
396
|
+
rec = dict(template) if template else {}
|
|
397
|
+
rec["id"] = tid
|
|
398
|
+
rec["cronExpression"] = "* * * * *"
|
|
399
|
+
rec["enabled"] = True
|
|
400
|
+
rec["filePath"] = os.path.join(
|
|
401
|
+
os.path.expanduser("~"), ".claude", "scheduled-tasks", tid, "SKILL.md"
|
|
402
|
+
)
|
|
403
|
+
rec["cwd"] = WORKER_CWD
|
|
404
|
+
rec.setdefault("createdAt", int(time.time() * 1000))
|
|
405
|
+
# Let the host recompute run-history for this account.
|
|
406
|
+
for k in ("lastRunAt", "lastScheduledFor", "fireAt"):
|
|
407
|
+
rec.pop(k, None)
|
|
408
|
+
return rec
|
|
409
|
+
|
|
410
|
+
def _session_registry_targets(self):
|
|
411
|
+
"""Where to write routines: every existing scheduled-tasks.json (fix in
|
|
412
|
+
place) PLUS the most-recently-active session dir (the live account, which
|
|
413
|
+
may have no registry yet). Covers whichever account is logged in without
|
|
414
|
+
having to know which it is."""
|
|
415
|
+
targets = set(glob.glob(SCHED_REGISTRY_GLOB))
|
|
416
|
+
base = os.path.join(
|
|
417
|
+
os.path.expanduser("~"), "Library", "Application Support", "Claude",
|
|
418
|
+
"claude-code-sessions",
|
|
356
419
|
)
|
|
420
|
+
best, best_m = None, -1.0
|
|
421
|
+
try:
|
|
422
|
+
for ws in os.listdir(base):
|
|
423
|
+
wsp = os.path.join(base, ws)
|
|
424
|
+
if not os.path.isdir(wsp):
|
|
425
|
+
continue
|
|
426
|
+
for inner in os.listdir(wsp):
|
|
427
|
+
ip = os.path.join(wsp, inner)
|
|
428
|
+
if not os.path.isdir(ip):
|
|
429
|
+
continue
|
|
430
|
+
try:
|
|
431
|
+
m = os.path.getmtime(ip)
|
|
432
|
+
except OSError:
|
|
433
|
+
continue
|
|
434
|
+
if m > best_m:
|
|
435
|
+
best_m, best = m, ip
|
|
436
|
+
except Exception:
|
|
437
|
+
pass
|
|
438
|
+
if best:
|
|
439
|
+
targets.add(os.path.join(best, "scheduled-tasks.json"))
|
|
440
|
+
return sorted(targets)
|
|
441
|
+
|
|
442
|
+
def _ensure_routines_registered(self):
|
|
443
|
+
"""Create the two queue-worker routines (cwd=~/.s4l-worker, enabled) in the
|
|
444
|
+
live account's registry — recovering an account-switch orphan WITHOUT the
|
|
445
|
+
Claude UI. Caller MUST run this while Claude is DOWN. Best-effort; never
|
|
446
|
+
raises. Returns the number of registry files written."""
|
|
447
|
+
templates = self._worker_record_templates()
|
|
448
|
+
try:
|
|
449
|
+
os.makedirs(WORKER_CWD, exist_ok=True)
|
|
450
|
+
except Exception:
|
|
451
|
+
pass
|
|
452
|
+
written = 0
|
|
453
|
+
for f in self._session_registry_targets():
|
|
454
|
+
try:
|
|
455
|
+
if os.path.exists(f):
|
|
456
|
+
with open(f) as fh:
|
|
457
|
+
d = json.load(fh)
|
|
458
|
+
else:
|
|
459
|
+
d = {"scheduledTasks": [], "recordedSkips": {}}
|
|
460
|
+
except Exception:
|
|
461
|
+
d = {"scheduledTasks": [], "recordedSkips": {}}
|
|
462
|
+
tasks = d.get("scheduledTasks") or []
|
|
463
|
+
by_id = {t.get("id"): t for t in tasks}
|
|
464
|
+
dirty = False
|
|
465
|
+
for tid in WORKER_TASK_IDS:
|
|
466
|
+
if tid in by_id:
|
|
467
|
+
t = by_id[tid]
|
|
468
|
+
if not t.get("enabled") or t.get("cwd") != WORKER_CWD:
|
|
469
|
+
t["enabled"] = True
|
|
470
|
+
t["cwd"] = WORKER_CWD
|
|
471
|
+
dirty = True
|
|
472
|
+
else:
|
|
473
|
+
tasks.append(self._make_worker_record(tid, templates.get(tid)))
|
|
474
|
+
dirty = True
|
|
475
|
+
if not dirty:
|
|
476
|
+
continue
|
|
477
|
+
d["scheduledTasks"] = tasks
|
|
478
|
+
try:
|
|
479
|
+
os.makedirs(os.path.dirname(f), exist_ok=True)
|
|
480
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(f))
|
|
481
|
+
with os.fdopen(fd, "w") as fh:
|
|
482
|
+
json.dump(d, fh, indent=2)
|
|
483
|
+
os.replace(tmp, f)
|
|
484
|
+
written += 1
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
return written
|
|
357
488
|
|
|
358
489
|
# ---- autopilot liveness (the false-green fix) -------------------------
|
|
359
490
|
def _autopilot_stalled(self):
|
|
@@ -1196,11 +1327,6 @@ class S4LMenuBar(rumps.App):
|
|
|
1196
1327
|
if stalled:
|
|
1197
1328
|
items.append(self._label("⚠ Autopilot not running — no drafts"))
|
|
1198
1329
|
items.append(rumps.MenuItem("Re-arm autopilot", callback=self._rearm))
|
|
1199
|
-
# Always-works fallback when the keystroke-paste path is blocked by a
|
|
1200
|
-
# stale Accessibility grant: copy the prompt so the user can paste it.
|
|
1201
|
-
items.append(
|
|
1202
|
-
rumps.MenuItem("Re-arm: copy prompt (paste in Claude)", callback=self._rearm_copy)
|
|
1203
|
-
)
|
|
1204
1330
|
items.append(rumps.separator)
|
|
1205
1331
|
|
|
1206
1332
|
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.130",
|
|
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