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 CHANGED
@@ -2045,7 +2045,11 @@ async function autopilotLoaded() {
2045
2045
  }
2046
2046
  let auto_update_on = false;
2047
2047
  try {
2048
- const res = await run("launchctl", ["list"], { timeoutMs: 10_000 });
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 {
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.129",
3
- "installedAt": "2026-06-28T04:38:52.828Z"
2
+ "version": "1.6.130",
3
+ "installedAt": "2026-06-28T04:56:09.516Z"
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.129",
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
- # Re-arm after the draft autopilot's scheduled-task routines got orphaned most
116
- # often because the user logged Claude Desktop into a different account, which
117
- # leaves the routines registered only under the OLD account's session so nothing
118
- # drains the draft queue (the SKILL.md files persist globally, so the old presence
119
- # check stayed falsely "green"). The host's create_scheduled_task only exists
120
- # inside an interactive Desktop session, so recovery routes through the composer
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
- self._send_to_claude(REARM_PROMPT)
343
-
344
- def _rearm_copy(self, _=None):
345
- """Always-works fallback: copy the re-arm prompt to the clipboard and open
346
- Claude, no Accessibility/automation needed. The user pastes it (⌘V) and
347
- presses Enter. This is the escape hatch for when the keystroke-paste path
348
- is blocked by a stale TCC grant."""
349
- copied = self._copy_to_clipboard(REARM_PROMPT)
350
- self._open_claude()
351
- self._notify(
352
- "S4L · re-arm prompt copied" if copied else "S4L",
353
- "Paste it into Claude (⌘V) and press Enter to re-arm the autopilot."
354
- if copied
355
- else "Open Claude and ask it to re-arm the draft autopilot.",
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.129",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.129",
3
+ "version": "1.6.130",
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"