social-autoposter 1.6.77 → 1.6.79

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.
@@ -773,7 +773,12 @@ export async function ensureMenubar() {
773
773
  if (fs.existsSync(MENUBAR_ENTRY) &&
774
774
  fs.existsSync(MENUBAR_PLIST) &&
775
775
  (await menubarLoaded())) {
776
- return { ok: true, skipped: true, detail: "already installed" };
776
+ // Installed but refresh the stable copy if the bundled menu bar is newer.
777
+ // An .mcpb update ships new menu bar code, yet the running menu bar runs from
778
+ // this stable copy (so it survives extension replacement); without this it
779
+ // would stay the version it first installed at. Cheap: content-compare, and
780
+ // a kickstart only when something actually changed.
781
+ return await refreshMenubarIfStale();
777
782
  }
778
783
  const uv = findUv();
779
784
  if (!uv)
@@ -783,3 +788,35 @@ export async function ensureMenubar() {
783
788
  };
784
789
  return installMenubar(uv, uvEnv, resolvePython());
785
790
  }
791
+ // Re-copy the bundled menu bar python into the stable dir when it differs from
792
+ // what's installed, and kickstart the launchd job so the running menu bar picks
793
+ // up the new code. Content-compare keeps this a no-op on an unchanged boot, so
794
+ // it's cheap to call on every ensureMenubar(). This is what makes menu bar
795
+ // changes (e.g. the "Please update now" button) actually ship on an .mcpb update.
796
+ async function refreshMenubarIfStale() {
797
+ const src = menubarSourceDir();
798
+ if (!src)
799
+ return { ok: true, skipped: true, detail: "no bundle source to refresh from" };
800
+ let changed = false;
801
+ try {
802
+ for (const f of fs.readdirSync(src)) {
803
+ if (!f.endsWith(".py"))
804
+ continue;
805
+ const s = fs.readFileSync(path.join(src, f));
806
+ const dPath = path.join(MENUBAR_DIR, f);
807
+ const d = fs.existsSync(dPath) ? fs.readFileSync(dPath) : Buffer.alloc(0);
808
+ if (!s.equals(d)) {
809
+ fs.writeFileSync(dPath, s);
810
+ changed = true;
811
+ }
812
+ }
813
+ }
814
+ catch (e) {
815
+ return { ok: true, skipped: true, detail: `menu bar refresh check failed: ${e?.message || e}` };
816
+ }
817
+ if (!changed)
818
+ return { ok: true, skipped: true, detail: "menu bar already current" };
819
+ const uid = process.getuid ? process.getuid() : 0;
820
+ await sh("launchctl", ["kickstart", "-k", `gui/${uid}/${MENUBAR_LABEL}`], { timeoutMs: 15_000 });
821
+ return { ok: true, detail: "menu bar refreshed + restarted to bundled version" };
822
+ }
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.77",
3
- "installedAt": "2026-06-22T18:18:59.841Z"
2
+ "version": "1.6.79",
3
+ "installedAt": "2026-06-22T19:08:46.414Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.6.77",
5
+ "version": "1.6.79",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the S4L pipeline.",
7
- "long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Fully quit and restart Claude (quit and reopen, not just close the window).\n2. Choose **Set up S4L** from the prompt menu or type **Set me up on S4L end to end**. The agent installs the owned runtime, connects X, discovers and configures your product, and verifies with a draft-only cycle.",
7
+ "long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Click **Configure** and set every tool permission to **Always Allow**.\n2. Copy this prompt: **Set me up on S4L end to end**.\n3. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
9
9
  "name": "m13v",
10
10
  "email": "i@m13v.com",
@@ -17,9 +17,12 @@ rather than rumps.notification (which needs a bundle id).
17
17
  """
18
18
 
19
19
  import json
20
+ import os
20
21
  import subprocess
21
22
  import sys
23
+ import tempfile
22
24
  import threading
25
+ import time
23
26
 
24
27
  import rumps
25
28
 
@@ -111,6 +114,14 @@ class S4LMenuBar(rumps.App):
111
114
  # read per second.
112
115
  self._act_poll = rumps.Timer(self._poll_activity, 1.0)
113
116
  self._act_poll.start()
117
+ # Self-check for a newer published .mcpb, independent of the MCP server's
118
+ # npm check (which has been flaky). Slow timer + cache; drives the title
119
+ # indicator + the "Please update now" menu item.
120
+ self._update_available = False
121
+ self._latest_version = None
122
+ self._upd_timer = rumps.Timer(self._check_update, 1800) # every 30 min
123
+ self._upd_timer.start()
124
+ self._check_update(None)
114
125
  self._tick(None)
115
126
 
116
127
  # ---- side effects -----------------------------------------------------
@@ -172,6 +183,98 @@ class S4LMenuBar(rumps.App):
172
183
  def _update(self, _=None):
173
184
  self._send_to_claude(UPDATE_PROMPT)
174
185
 
186
+ # ---- .mcpb self-update (menu-bar driven) ------------------------------
187
+ EXT_DIR = os.path.expanduser(
188
+ "~/Library/Application Support/Claude/Claude Extensions/"
189
+ "local.mcpb.m13v.social-autoposter"
190
+ )
191
+ MCPB_URL = (
192
+ "https://github.com/m13v/social-autoposter/releases/latest/download/"
193
+ "social-autoposter.mcpb"
194
+ )
195
+ RELEASE_API = (
196
+ "https://api.github.com/repos/m13v/social-autoposter/releases/latest"
197
+ )
198
+
199
+ @staticmethod
200
+ def _vtuple(v):
201
+ try:
202
+ return tuple(int(x) for x in str(v).split("."))
203
+ except Exception:
204
+ return (0,)
205
+
206
+ def _check_update(self, _=None):
207
+ """Is a newer .mcpb published than what's installed? Compare the GitHub
208
+ latest release tag to the installed extension manifest. Offline / API
209
+ errors leave the cached value untouched (never flips to 'no update' on a
210
+ transient failure). A change forces a title + menu repaint."""
211
+ installed = None
212
+ try:
213
+ with open(os.path.join(self.EXT_DIR, "manifest.json")) as f:
214
+ installed = (json.load(f) or {}).get("version")
215
+ except Exception:
216
+ return # not a .mcpb install (or no manifest) -> nothing to offer
217
+ try:
218
+ r = subprocess.run(
219
+ ["curl", "-fsSL", "-m", "15", self.RELEASE_API],
220
+ capture_output=True, text=True, timeout=20,
221
+ )
222
+ if r.returncode != 0:
223
+ return
224
+ latest = ((json.loads(r.stdout) or {}).get("tag_name") or "").lstrip("v")
225
+ except Exception:
226
+ return
227
+ if not latest:
228
+ return
229
+ available = bool(installed) and self._vtuple(latest) > self._vtuple(installed)
230
+ if available != self._update_available or latest != self._latest_version:
231
+ self._update_available = available
232
+ self._latest_version = latest
233
+ self._sig = None # repaint on next tick
234
+
235
+ def _do_mcpb_update(self, _=None):
236
+ """User clicked 'Please update now'. Pull the latest .mcpb, unpack it over
237
+ the Desktop extension dir in place, and restart Claude so the new server
238
+ loads. The menu bar is a launchd process (not a Claude child), so the
239
+ restart is clean. Heavy work runs on a background thread."""
240
+ self._notify("S4L", "Updating… Claude will restart in a moment.")
241
+ threading.Thread(target=self._mcpb_update_work, daemon=True).start()
242
+
243
+ def _mcpb_update_work(self):
244
+ tmpd = tempfile.mkdtemp(prefix="s4l-update-")
245
+ mcpb = os.path.join(tmpd, "social-autoposter.mcpb")
246
+ try:
247
+ r = subprocess.run(["curl", "-fLs", "-m", "300", self.MCPB_URL, "-o", mcpb],
248
+ capture_output=True, timeout=320)
249
+ if r.returncode != 0 or not os.path.exists(mcpb) or os.path.getsize(mcpb) < 100000:
250
+ self._notify("S4L update failed", "Couldn't download the update — check your connection.")
251
+ return
252
+ r = subprocess.run(["unzip", "-oq", mcpb, "-d", self.EXT_DIR],
253
+ capture_output=True, timeout=180)
254
+ if r.returncode != 0:
255
+ self._notify("S4L update failed", "Couldn't unpack the update.")
256
+ return
257
+ # Restart Claude so the refreshed server loads (we're decoupled from it).
258
+ subprocess.run(["osascript", "-e", 'tell application "Claude" to quit'],
259
+ capture_output=True, timeout=20)
260
+ time.sleep(4)
261
+ subprocess.run(["killall", "Claude"], capture_output=True) # if quit was blocked
262
+ time.sleep(2)
263
+ subprocess.run(["killall", "-9", "Claude"], capture_output=True)
264
+ time.sleep(1)
265
+ subprocess.run(["open", "-a", CLAUDE_APP], capture_output=True, timeout=20)
266
+ self._update_available = False
267
+ self._sig = None
268
+ self._notify("S4L updated", "Claude restarted on the latest version.")
269
+ except Exception as e:
270
+ self._notify("S4L update failed", str(e)[:140])
271
+ finally:
272
+ try:
273
+ import shutil
274
+ shutil.rmtree(tmpd, ignore_errors=True)
275
+ except Exception:
276
+ pass
277
+
175
278
  def _open_dashboard(self, _=None):
176
279
  url = st.panel_url()
177
280
  if url:
@@ -350,6 +453,8 @@ class S4LMenuBar(rumps.App):
350
453
  elif not setup_complete and ob and not ob.get("complete"):
351
454
  done = sum(1 for m in ob["milestones"] if m.get("status") == "complete")
352
455
  self.title = f"S4L {done}/{len(ob['milestones'])}"
456
+ elif self._update_available:
457
+ self.title = "S4L ⬆" # update available — open the menu to update
353
458
  else:
354
459
  self.title = "S4L"
355
460
 
@@ -373,12 +478,11 @@ class S4LMenuBar(rumps.App):
373
478
 
374
479
  items.append(rumps.separator)
375
480
  items.append(rumps.MenuItem("Open dashboard", callback=self._open_dashboard))
376
- if snap.get("update_available") and snap.get("latest_version"):
481
+ if self._update_available and self._latest_version:
482
+ items.append(rumps.separator)
483
+ items.append(self._label(f"⬆ Update available · v{self._latest_version}"))
377
484
  items.append(
378
- rumps.MenuItem(
379
- f"Update to v{snap['latest_version']} in Claude",
380
- callback=self._update,
381
- )
485
+ rumps.MenuItem("Please update now", callback=self._do_mcpb_update)
382
486
  )
383
487
  items.append(rumps.MenuItem("Quit", callback=rumps.quit_application))
384
488
 
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.77",
3
+ "version": "1.6.79",
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.77",
3
+ "version": "1.6.79",
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"