social-autoposter 1.6.78 → 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.
- package/mcp/dist/runtime.js +38 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +109 -5
- package/mcp/package.json +1 -1
- package/package.json +1 -1
package/mcp/dist/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
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.79",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the S4L pipeline.",
|
|
7
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": {
|
|
@@ -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
|
|
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.
|
|
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