social-autoposter 1.6.113 → 1.6.115

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.
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.113",
3
- "installedAt": "2026-06-25T19:38:00.335Z"
2
+ "version": "1.6.115",
3
+ "installedAt": "2026-06-26T20:55:06.436Z"
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.113",
5
+ "version": "1.6.115",
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": {
@@ -168,6 +168,16 @@ class S4LMenuBar(rumps.App):
168
168
  self._posting_batch_done = 0
169
169
  self._spin_i = 0
170
170
  self._spinner = None # fast rumps.Timer animating the title while busy
171
+ # Durable posting progress, derived from the review-queue PLAN rather than
172
+ # this process's in-memory burst queue. The in-memory counter dies on a
173
+ # menu bar restart and is blind to posts driven by the autopilot/agent, so
174
+ # the title used to fall back to plain "S4L" mid-drain. These track a drain
175
+ # by the plan's posted count climbing, with hysteresis across the multi-
176
+ # second gaps between individual posts so the indicator never blinks off.
177
+ self._posting_label = None
178
+ self._drain_baseline = None # posted count just before this drain started
179
+ self._drain_last_posted = None
180
+ self._drain_last_change = 0.0
171
181
  # Reliable self-check of our own Accessibility (TCC) grant — this is the
172
182
  # faithful reading (our launchd process identity, not a parent's). Logged
173
183
  # so menubar.err.log records whether keystroke posting will work.
@@ -395,15 +405,70 @@ class S4LMenuBar(rumps.App):
395
405
  # animates the title with the label and stops itself when activity clears.
396
406
  # Both run on the main thread (rumps timers).
397
407
  def _poll_activity(self, _):
408
+ # Refresh the durable plan-based posting label (cheap: one small file read)
409
+ # so the title can show steady posting progress even when the server's
410
+ # per-post activity.json is momentarily empty between posts.
411
+ self._posting_label = self._compute_posting_label()
398
412
  act = st.read_activity()
399
- if act and act.get("label") and self._spinner is None:
413
+ has_label = bool((act and act.get("label")) or self._posting_label)
414
+ if has_label and self._spinner is None:
400
415
  self._spin_i = 0
401
416
  self._spinner = rumps.Timer(self._spin, 0.12)
402
417
  self._spinner.start()
403
418
 
419
+ def _compute_posting_label(self):
420
+ """Posting progress from the durable review-queue plan, with hysteresis.
421
+
422
+ A drain is detected by the plan's posted count INCREASING (or the server's
423
+ activity.json reporting a post in flight) — never by the raw unposted
424
+ backlog, which sits non-zero for drafts merely awaiting review. The label
425
+ is held for a grace window after the last post so the indicator doesn't
426
+ blink back to "S4L" during the multi-second gaps between posts. Survives a
427
+ menu bar restart and reflects posts driven by the autopilot/agent, not just
428
+ this process's own approval queue."""
429
+ now = time.time()
430
+ try:
431
+ posted = st.review_queue_posted_count()
432
+ except Exception:
433
+ posted = None
434
+ act = st.read_activity()
435
+ act_posting = bool(act and "posting" in str(act.get("label") or ""))
436
+ if posted is None:
437
+ posted = self._drain_last_posted # ride through a transient read miss
438
+ if posted is None:
439
+ return None
440
+ if self._drain_last_posted is None:
441
+ self._drain_last_posted = posted
442
+ advanced = posted > self._drain_last_posted
443
+ if advanced or act_posting:
444
+ if self._drain_baseline is None:
445
+ # New drain: baseline is the count just BEFORE its first post.
446
+ self._drain_baseline = self._drain_last_posted
447
+ self._drain_last_change = now
448
+ if advanced:
449
+ self._drain_last_posted = posted
450
+ if self._drain_baseline is None:
451
+ return None
452
+ # Drain is over once the server is idle AND no new post landed for a grace
453
+ # window (covers the long gaps between the last few slow posts).
454
+ if not act_posting and now - self._drain_last_change > 45.0:
455
+ self._drain_baseline = None
456
+ return None
457
+ sent = max(0, posted - self._drain_baseline)
458
+ return f"posting · {sent} sent"
459
+
404
460
  def _spin(self, _):
405
461
  act = st.read_activity()
406
462
  label = act.get("label") if act else None
463
+ act_state = act.get("state") if act else None
464
+ # For POSTING, prefer the menu bar's durable cumulative label over the
465
+ # server's per-call "1/1" so the count climbs smoothly and the indicator
466
+ # holds through the gaps between posts. Non-posting activity (scanning /
467
+ # drafting) keeps its own server label.
468
+ if self._posting_label and (
469
+ not label or act_state == "posting" or "posting" in str(label)
470
+ ):
471
+ label = self._posting_label
407
472
  if label:
408
473
  # The update arrow must stay visible even while a tool runs, so the
409
474
  # "update available" signal is never masked by activity. _tick skips the
@@ -349,6 +349,26 @@ def read_plan(plan_path):
349
349
  return None
350
350
 
351
351
 
352
+ def review_queue_posted_count():
353
+ """Posts that have LANDED in the review-queue plan — the durable, cross-process
354
+ truth. Independent of the menu bar's in-memory burst queue (which dies on a
355
+ restart) and of WHICH process is posting (the menu bar worker, the autopilot,
356
+ or a host agent draining via post_drafts). Returns the posted count, or None
357
+ when the plan can't be read. Drives the menu-bar posting indicator so progress
358
+ stays visible regardless of how the drain is driven."""
359
+ plan_path = None
360
+ req = read_review_request()
361
+ if req:
362
+ plan_path = req.get("plan_path")
363
+ if not plan_path:
364
+ plan_path = "/tmp/twitter_cycle_plan_review-queue.json"
365
+ plan = read_plan(plan_path)
366
+ cands = (plan or {}).get("candidates")
367
+ if not cands:
368
+ return None
369
+ return sum(1 for c in cands if c.get("posted") is True)
370
+
371
+
352
372
  def review_drafts(plan):
353
373
  """Flatten a plan into the card model: only UNDECIDED candidates. A card that's
354
374
  posted, terminal (rejected/dead), or already approved is a settled decision and
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.113",
3
+ "version": "1.6.115",
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.113",
3
+ "version": "1.6.115",
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"