social-autoposter 1.6.184 → 1.6.185

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
@@ -22,7 +22,7 @@ import fs from "node:fs";
22
22
  import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
23
23
  import { applySetup, resolveProject, personaReady, listManagedProjectStatus, ensureShortLinksDefault, ensurePersonaProject, findPersonaProject, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, normalizeStringList, } from "./setup.js";
24
24
  import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
25
- import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, ensurePipelineCurrent, ensureRuntimeProvisioned, } from "./runtime.js";
25
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, menubarRunning, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned, } from "./runtime.js";
26
26
  import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
27
27
  import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
28
28
  import { initSentry, sendHeartbeat, captureError, flushSentry, startLogStreaming, flushLogs } from "./telemetry.js";
@@ -2101,10 +2101,14 @@ tool("restart_menubar", {
2101
2101
  title: "Restart the S4L menu bar app",
2102
2102
  description: "Relaunch the always-on S4L menu bar (tray) app after it was quit. Re-loads its " +
2103
2103
  "LaunchAgent (installing the menu bar first if needed). Use when the dashboard reports the menu " +
2104
- "bar is not running, or the user asks to bring the S4L tray icon back. Does NOT touch the draft " +
2104
+ "bar is not running, or the user asks to start S4L, restart S4L, or bring the S4L tray icon " +
2105
+ "back. Does NOT touch the draft " +
2105
2106
  "schedule, X connection, or any posting — it only restarts the tray UI.",
2106
2107
  inputSchema: {},
2107
2108
  }, async () => {
2109
+ // Explicit user intent to start: lift the stop sentinel a tray Quit wrote,
2110
+ // otherwise ensureMenubar() would no-op forever.
2111
+ clearMenubarStop();
2108
2112
  const res = await ensureMenubar();
2109
2113
  const running = await menubarRunning();
2110
2114
  return jsonContent({
@@ -2145,6 +2149,11 @@ tool("queue_setup", {
2145
2149
  inputSchema: {},
2146
2150
  }, async () => {
2147
2151
  ensureQueueWorkerToolsAllowed();
2152
+ // Re-arming the autopilot is an explicit "start S4L" action: lift a prior
2153
+ // tray Quit so the review cards have a surface again. Best-effort and
2154
+ // async — task specs must return regardless of tray state.
2155
+ clearMenubarStop();
2156
+ void ensureMenubar();
2148
2157
  // Write each worker's canonical SKILL.md to disk NOW, before the agent calls
2149
2158
  // create_scheduled_task. The host's create_scheduled_task can report a task
2150
2159
  // "already exists" (e.g. a stale Routines registration left after a reset) and
@@ -84,6 +84,29 @@ const MENUBAR_DIR = path.join(STATE_DIR, "menubar");
84
84
  const MENUBAR_ENTRY = path.join(MENUBAR_DIR, "s4l_menubar.py");
85
85
  const MENUBAR_OUT_LOG = path.join(MENUBAR_DIR, "menubar.out.log");
86
86
  const MENUBAR_ERR_LOG = path.join(MENUBAR_DIR, "menubar.err.log");
87
+ // Stop sentinel: the menu bar's Quit flow writes this file (and boots itself
88
+ // out) to record that the USER explicitly stopped S4L. Every auto-start path
89
+ // (boot-time ensureMenubar, runtime provision) must respect it, otherwise the
90
+ // tray is guaranteed back on the next Claude restart — the exact bug users hit
91
+ // after Quit. Only an explicit start action (restart_menubar tool, queue_setup
92
+ // re-arm) clears it. KEEP the filename in sync with menubar/s4l_menubar.py.
93
+ const MENUBAR_STOP_FLAG = path.join(STATE_DIR, "stopped.flag");
94
+ export function menubarStopped() {
95
+ try {
96
+ return fs.existsSync(MENUBAR_STOP_FLAG);
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ export function clearMenubarStop() {
103
+ try {
104
+ fs.rmSync(MENUBAR_STOP_FLAG, { force: true });
105
+ }
106
+ catch {
107
+ /* best-effort */
108
+ }
109
+ }
87
110
  // A directory is a usable pipeline clone only if it carries requirements.txt
88
111
  // (the deps manifest) AND scripts/ (the pipeline). Guards against pointing at an
89
112
  // empty extension dir or a half-deleted state dir.
@@ -656,7 +679,15 @@ async function provision(progress) {
656
679
  // Non-fatal: a menu bar failure must never block a usable runtime, so on any
657
680
  // problem we mark the step errored and still persist runtime.json below.
658
681
  setStep("menubar", "running");
659
- if (process.platform === "darwin") {
682
+ if (process.platform !== "darwin") {
683
+ setStep("menubar", "done", "skipped (macOS only)");
684
+ }
685
+ else if (menubarStopped()) {
686
+ // A runtime repair/re-provision must not resurrect a tray the user
687
+ // explicitly quit; an explicit start clears the flag first.
688
+ setStep("menubar", "done", "skipped (user stopped the menu bar)");
689
+ }
690
+ else {
660
691
  const mb = await installMenubar(uv, uvEnv, VENV_PYTHON);
661
692
  setStep("menubar", mb.ok ? "done" : "error", mb.detail);
662
693
  // Non-fatal step, so the only prior signal of a menu bar install failure was
@@ -669,9 +700,6 @@ async function provision(progress) {
669
700
  });
670
701
  }
671
702
  }
672
- else {
673
- setStep("menubar", "done", "skipped (macOS only)");
674
- }
675
703
  // --- Persist the result ---------------------------------------------------
676
704
  const info = {
677
705
  python: VENV_PYTHON,
@@ -790,6 +818,10 @@ export async function menubarRunning() {
790
818
  return true;
791
819
  if (!runtimeReady())
792
820
  return true;
821
+ // User explicitly quit the tray: it is down ON PURPOSE, so report "fine" —
822
+ // the dashboard banner must not nag about a state the user chose.
823
+ if (menubarStopped())
824
+ return true;
793
825
  try {
794
826
  return await menubarLoaded();
795
827
  }
@@ -846,6 +878,12 @@ export async function installMenubar(uv, uvEnv, python) {
846
878
  export async function ensureMenubar() {
847
879
  if (process.platform !== "darwin")
848
880
  return { ok: true, skipped: true, detail: "non-macOS" };
881
+ // The user clicked Quit in the tray: stay stopped across Claude restarts,
882
+ // regardless of runtime state. Explicit start paths (restart_menubar tool,
883
+ // queue_setup) clear the flag before calling this.
884
+ if (menubarStopped()) {
885
+ return { ok: true, skipped: true, detail: "user stopped the menu bar (stopped.flag)" };
886
+ }
849
887
  if (!runtimeReady())
850
888
  return { ok: false, skipped: true, detail: "runtime not ready" };
851
889
  if (fs.existsSync(MENUBAR_ENTRY) &&
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.184",
3
- "installedAt": "2026-07-01T21:13:50.137Z"
2
+ "version": "1.6.185",
3
+ "installedAt": "2026-07-01T21:48:09.463Z"
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.184",
5
+ "version": "1.6.185",
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": {
@@ -118,6 +118,19 @@ def _activate_front():
118
118
  CLAUDE_APP = "Claude"
119
119
  POLL_SECONDS = 5
120
120
 
121
+ # Our own LaunchAgent. Quit boots it out and deletes the plist so the tray is
122
+ # genuinely gone (KeepAlive can't respawn it, RunAtLoad can't resurrect it at
123
+ # next login). Keep the label in sync with MENUBAR_LABEL in mcp/src/runtime.ts.
124
+ MENUBAR_LABEL = "com.m13v.social-autoposter.menubar"
125
+ MENUBAR_PLIST = os.path.join(
126
+ os.path.expanduser("~"), "Library", "LaunchAgents", f"{MENUBAR_LABEL}.plist"
127
+ )
128
+ # Stop sentinel read by the MCP server's ensureMenubar()/provision paths: while
129
+ # present, no auto-start path may reinstall the tray. Cleared only by explicit
130
+ # start actions (restart_menubar tool, queue_setup re-arm). Keep the filename in
131
+ # sync with MENUBAR_STOP_FLAG in mcp/src/runtime.ts.
132
+ STOP_FLAG = os.path.join(st.state_dir(), "stopped.flag")
133
+
121
134
  # Autopilot scheduled tasks. The two queue workers must RUN in a dedicated folder
122
135
  # (~/.s4l-worker) so their once-a-minute sessions don't flood the user's
123
136
  # interactive Claude Code history (Claude buckets sessions by cwd). The single
@@ -704,22 +717,26 @@ class S4LMenuBar(rumps.App):
704
717
 
705
718
  def _quit_app(self, _=None):
706
719
  """The single Quit path. Quitting stops the autopilot completely: the
707
- draft/query scheduled tasks are removed so they no longer fire. Claude
708
- Desktop OWNS the live schedule and caches the registry in memory,
709
- clobbering any live edit on the next fire — so the only reliable way to
710
- disable them is to quit Claude, strip the tasks while it's down, then
711
- relaunch. We warn the user with a modal FIRST that Claude Desktop will
712
- restart, since the app window will close and reopen under them."""
720
+ draft/query scheduled tasks are removed so they no longer fire, AND the
721
+ tray itself goes away for good (stop flag + plist removal + self
722
+ bootout see _quit_work). Claude Desktop OWNS the live schedule and
723
+ caches the registry in memory, clobbering any live edit on the next
724
+ fire so the only reliable way to disable them is to quit Claude,
725
+ strip the tasks while it's down, then relaunch. We warn the user with a
726
+ modal FIRST that Claude Desktop will restart, since the app window will
727
+ close and reopen under them."""
713
728
  _activate_front()
714
729
  choice = rumps.alert(
715
730
  title="Quit the S4L autoposter?",
716
731
  message=(
717
732
  "Quitting stops the autoposter completely: the draft + query "
718
- "scheduled tasks are removed so nothing fires anymore.\n\n"
733
+ "scheduled tasks are removed so nothing fires anymore, and this "
734
+ "menu bar icon goes away and stays away.\n\n"
719
735
  "Claude Desktop will quit and restart to apply this — its window "
720
736
  "will close and reopen in a moment. Your X login, browser layer, "
721
- "and config all stay; you can re-arm the schedule any time from "
722
- "this menu."
737
+ "and config all stay.\n\n"
738
+ "To start S4L again later, open Claude and say \"start S4L\" "
739
+ "(or re-run setup)."
723
740
  ),
724
741
  ok="Quit & restart Claude", cancel="Cancel",
725
742
  )
@@ -768,10 +785,23 @@ class S4LMenuBar(rumps.App):
768
785
  pass
769
786
 
770
787
  def _quit_work(self):
771
- """Quit/kill Claude, strip the scheduled tasks while it's down, relaunch.
772
- Mirror of _relocate_restart_work's restart block. The menu bar is a separate
773
- launchd process, so killing Claude does not kill us."""
788
+ """Quit/kill Claude, strip the scheduled tasks while it's down, relaunch
789
+ Claude, then take THIS tray down for good. Mirror of
790
+ _relocate_restart_work's restart block. The menu bar is a separate
791
+ launchd process, so killing Claude does not kill us.
792
+
793
+ The stop flag is written FIRST: the relaunched Claude boots the MCP
794
+ server, whose ensureMenubar() would otherwise reinstall the tray
795
+ unconditionally (the reappearing-icon bug). The plist is deleted so
796
+ RunAtLoad can't resurrect us at next login, and the final bootout
797
+ removes the KeepAlive job — which kills this process, so it must be
798
+ the last thing we do."""
774
799
  try:
800
+ try:
801
+ with open(STOP_FLAG, "w") as fh:
802
+ fh.write(f"user quit via menu bar at {time.strftime('%Y-%m-%dT%H:%M:%S%z')}\n")
803
+ except Exception as e:
804
+ _capture(e, action="quit_stop_flag")
775
805
  subprocess.run(["osascript", "-e", 'tell application "Claude" to quit'],
776
806
  capture_output=True, timeout=20)
777
807
  time.sleep(4)
@@ -780,11 +810,30 @@ class S4LMenuBar(rumps.App):
780
810
  subprocess.run(["killall", "-9", "Claude"], capture_output=True)
781
811
  time.sleep(1)
782
812
  self._remove_scheduled_tasks()
813
+ try:
814
+ os.remove(MENUBAR_PLIST)
815
+ except FileNotFoundError:
816
+ pass
817
+ except Exception as e:
818
+ _capture(e, action="quit_remove_plist")
783
819
  subprocess.run(["open", "-a", CLAUDE_APP], capture_output=True, timeout=20)
784
- self._notify("S4L", "Autoposter stopped. Claude restarted.")
820
+ self._notify("S4L", "Autoposter stopped. Say \"start S4L\" in Claude to bring it back.")
785
821
  except Exception as e:
786
822
  _capture(e, action="quit_app")
787
823
  self._notify("S4L", "Couldn't fully stop the autoposter — see logs.")
824
+ finally:
825
+ # Boot out our own KeepAlive agent. launchd kills this process as
826
+ # part of the bootout, so nothing after this line is guaranteed to
827
+ # run. Runs even if the Claude restart above failed: the user asked
828
+ # for the tray to be gone.
829
+ subprocess.run(
830
+ ["launchctl", "bootout", f"gui/{os.getuid()}/{MENUBAR_LABEL}"],
831
+ capture_output=True, timeout=15,
832
+ )
833
+ # Only reached if bootout didn't kill us (e.g. dev run outside
834
+ # launchd). Exit 0: KeepAlive {SuccessfulExit: false} treats a clean
835
+ # exit as final. os._exit because we're on a background thread.
836
+ os._exit(0)
788
837
 
789
838
  def _update(self, _=None):
790
839
  self._send_to_claude(UPDATE_PROMPT)
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.184",
3
+ "version": "1.6.185",
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.184",
3
+ "version": "1.6.185",
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"