@m13v/s4l 1.7.8-rc.3 → 1.7.8
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 +61 -27
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +84 -26
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/claude_job.py +57 -12
- package/scripts/install.sh +35 -0
package/mcp/dist/index.js
CHANGED
|
@@ -333,7 +333,8 @@ const server = new McpServer({
|
|
|
333
333
|
"anything; X is a reasonable default to SUGGEST, but do not connect it, or any platform, without " +
|
|
334
334
|
"the user's answer. Connect exactly what they choose via project_config action:'connect_x' " +
|
|
335
335
|
"and/or action:'connect_reddit'), AND " +
|
|
336
|
-
"the
|
|
336
|
+
"the draft autopilot has been armed via queue_setup (on Claude hosts that means its scheduled " +
|
|
337
|
+
"tasks are created and firing; on this app's host queue_setup alone completes it) " +
|
|
337
338
|
"(see SCHEDULE THE AUTOPILOT below). The owned runtime (private Python + Chromium) provisions " +
|
|
338
339
|
"ITSELF automatically on boot — you do NOT install it. Just poll runtime action:'status' until it " +
|
|
339
340
|
"reports ready; only call runtime action:'install' or action:'doctor' to TROUBLESHOOT if status " +
|
|
@@ -350,8 +351,8 @@ const server = new McpServer({
|
|
|
350
351
|
"DO schedule the draft autopilot as the final step (once running it only queues drafts for " +
|
|
351
352
|
"approval, it never posts on its own). When setup reaches done (" +
|
|
352
353
|
"runtime ready, a project configured with seeded topics, at least one platform connected, and the " +
|
|
353
|
-
"
|
|
354
|
-
"
|
|
354
|
+
"draft autopilot " +
|
|
355
|
+
"armed via queue_setup), call the `dashboard` tool so the user " +
|
|
355
356
|
"sees the finished setup rendered, then give " +
|
|
356
357
|
"the completion summary.\n\n" +
|
|
357
358
|
"SCHEDULE THE AUTOPILOT (final onboarding step). There is ONE drafting pipeline: a launchd " +
|
|
@@ -360,18 +361,24 @@ const server = new McpServer({
|
|
|
360
361
|
"menu-bar approval cards. Drafting is the queue + worker path, fired automatically by a launchd " +
|
|
361
362
|
"kicker this server installs (every few minutes) — it runs on its own once the tasks exist; " +
|
|
362
363
|
"onboarding does NOT need to wait for or verify a draft. Steps, in order: " +
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
364
|
+
(s4lHost() === "codex"
|
|
365
|
+
? "(1) call `queue_setup` once — on THIS host it arms the autopilot by itself (drafting runs " +
|
|
366
|
+
"inline through this app's engine) and returns no tasks; do NOT create any scheduled task or " +
|
|
367
|
+
"automation; (2) call the `dashboard` tool to confirm schedule_state 'ok', then summarize. " +
|
|
368
|
+
"Do NOT ask the user to review drafts or pause for input mid-onboarding; once queue_setup " +
|
|
369
|
+
"returns and the dashboard reads ok, setup is done.\n\n"
|
|
370
|
+
: "(1) call `queue_setup` — it pre-writes the worker task prompt and returns the task spec " +
|
|
371
|
+
"(s4l-worker, the universal queue worker); " +
|
|
372
|
+
"(2) for EACH returned spec, call the HOST tool `create_scheduled_task` with its taskId, cronExpression, " +
|
|
373
|
+
"prompt, and notifyOnCompletion VERBATIM — notifyOnCompletion MUST be false, or the per-minute worker " +
|
|
374
|
+
"pops a 'no jobs' notification every run (if it says the task already exists, that's fine — do not " +
|
|
375
|
+
"recreate, but DO call `update_scheduled_task` with the same taskId and notifyOnCompletion:false to " +
|
|
376
|
+
"silence an older install's per-run notifications, then move on); " +
|
|
377
|
+
"(3) the launchd kicker + scheduled tasks now run the pipeline on their own — call the `dashboard` " +
|
|
378
|
+
"tool to confirm the schedule is firing (schedule_state 'ok'), then summarize. Do NOT create the " +
|
|
379
|
+
"old 'social-autoposter-autopilot' task — it is deprecated. Do NOT ask the user to review drafts " +
|
|
380
|
+
"or pause for input mid-onboarding; once the tasks are created and firing, setup is done. If the " +
|
|
381
|
+
"host exposes no create_scheduled_task tool, tell the user the autopilot couldn't be scheduled here.\n\n") +
|
|
375
382
|
"REDDIT IS AN EQUAL PLATFORM CHOICE, ASKED ALONGSIDE X. Setup is complete with AT LEAST ONE " +
|
|
376
383
|
"platform connected: X-only, Reddit-only, or both — whichever the user picked when you asked " +
|
|
377
384
|
"up front. If they chose both, connect them one at a time (either order) in the same setup " +
|
|
@@ -2141,12 +2148,9 @@ tool("engagement_mode", {
|
|
|
2141
2148
|
: (personaQueryCount > 0
|
|
2142
2149
|
? `Personal-brand lane is on (the default); the persona is provisioned, topic-seeded, and ${personaQueryCount} search quer${personaQueryCount === 1 ? "y" : "ies"} seeded, so there `
|
|
2143
2150
|
: "Personal-brand lane is on (the default); the persona is provisioned + topic-seeded (but NO search_queries were supplied, so it will run one topic-as-query at a time — re-call engagement_mode action:'set' with a search_queries array of ~30 X search strings expanded from the persona topics to fan out). There ") +
|
|
2144
|
-
"is nothing more to configure (no product project is needed). NOW SCHEDULE THE AUTOPILOT:
|
|
2145
|
-
|
|
2146
|
-
"
|
|
2147
|
-
"call update_scheduled_task with notifyOnCompletion:false to silence per-run notifications), " +
|
|
2148
|
-
"then call the dashboard tool to confirm the schedule is firing. " +
|
|
2149
|
-
"That is the final setup step — do NOT stop before the autopilot is scheduled.";
|
|
2151
|
+
"is nothing more to configure (no product project is needed). NOW SCHEDULE THE AUTOPILOT: " +
|
|
2152
|
+
scheduleAutopilotSteps() + ". " +
|
|
2153
|
+
"That is the final setup step — do NOT stop before the autopilot is armed.";
|
|
2150
2154
|
return jsonContent({
|
|
2151
2155
|
ok: true,
|
|
2152
2156
|
flags: { personal_brand: personalBrand, promotion },
|
|
@@ -2718,7 +2722,7 @@ tool("project_config", {
|
|
|
2718
2722
|
? (anyPlatform
|
|
2719
2723
|
? "All configured projects are ready and a platform is connected" +
|
|
2720
2724
|
(xConnected ? "" : " (Reddit; X was skipped, which is fine)") +
|
|
2721
|
-
". SCHEDULE THE AUTOPILOT:
|
|
2725
|
+
". SCHEDULE THE AUTOPILOT: " + scheduleAutopilotSteps() + " — that is the terminal step; do NOT wait for or verify a draft card. Do NOT pause to ask the user to review drafts."
|
|
2722
2726
|
: "All configured projects are ready, but NO platform is connected yet; posting needs a logged-in session. X is the default: detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed. If the user prefers Reddit or X can't be connected, run action:'connect_reddit' instead; ONE connected platform completes setup.")
|
|
2723
2727
|
: "Some projects are missing required fields (see each project's missing_required). Derive them from config, context, profile_scan, and website research, then call project_config again. Ask only if a required field is genuinely unknowable." +
|
|
2724
2728
|
(anyPlatform ? "" : " No platform is connected yet either; X is the default (connect_x with confirm:true), or connect_reddit if the user prefers Reddit."),
|
|
@@ -2850,11 +2854,9 @@ tool("project_config", {
|
|
|
2850
2854
|
: result.ready
|
|
2851
2855
|
? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
|
|
2852
2856
|
`detect sources, warn about keychain prompts, and call project_config with ` +
|
|
2853
|
-
`action:'connect_x', confirm:true immediately. Once X is connected, schedule the autopilot ` +
|
|
2854
|
-
|
|
2855
|
-
`
|
|
2856
|
-
`dashboard to confirm the schedule is firing (schedule_state 'ok') — that is the final step, ` +
|
|
2857
|
-
`no need to wait for or verify a draft card.`
|
|
2857
|
+
`action:'connect_x', confirm:true immediately. Once X is connected, schedule the autopilot: ` +
|
|
2858
|
+
scheduleAutopilotSteps() +
|
|
2859
|
+
` — that is the final step, no need to wait for or verify a draft card.`
|
|
2858
2860
|
: `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
|
|
2859
2861
|
`First derive those fields from existing context, profile_scan, and website research, then ` +
|
|
2860
2862
|
`call project_config again with name='${result.project}'. Ask only if a required field is genuinely unknowable.`) +
|
|
@@ -3607,6 +3609,21 @@ function runtimeSnapshot() {
|
|
|
3607
3609
|
function s4lHost() {
|
|
3608
3610
|
return (process.env.S4L_HOST || "").trim().toLowerCase() === "codex" ? "codex" : "claude";
|
|
3609
3611
|
}
|
|
3612
|
+
// Host-aware "schedule the autopilot" step text, shared by every onboarding
|
|
3613
|
+
// guidance string. Codex/ChatGPT hosts have no create_scheduled_task tool and
|
|
3614
|
+
// need none: queue_setup alone arms the autopilot there (drafting runs inline
|
|
3615
|
+
// through the app's engine). Hoisted function so module-level instruction
|
|
3616
|
+
// strings can call it during load.
|
|
3617
|
+
function scheduleAutopilotSteps() {
|
|
3618
|
+
return s4lHost() === "codex"
|
|
3619
|
+
? "call queue_setup once — on this host it arms the draft autopilot by itself (drafting runs " +
|
|
3620
|
+
"inline through this app's engine) and returns NO tasks; do NOT create any scheduled task or " +
|
|
3621
|
+
"automation — then call the dashboard tool to confirm schedule_state 'ok'"
|
|
3622
|
+
: "call queue_setup and create each returned task with create_scheduled_task (prompt and " +
|
|
3623
|
+
"notifyOnCompletion verbatim — notifyOnCompletion must be false; 'already exists' is fine, but then " +
|
|
3624
|
+
"call update_scheduled_task with notifyOnCompletion:false to silence per-run notifications), " +
|
|
3625
|
+
"then call the dashboard tool to confirm the schedule is firing (schedule_state 'ok')";
|
|
3626
|
+
}
|
|
3610
3627
|
// Stamp <state_dir>/draft-provider.json, the single source of truth read at the
|
|
3611
3628
|
// run_claude.sh seam (scripts/draft_provider.py / claude_job.py) for which
|
|
3612
3629
|
// engine answers the pipeline's pure text->JSON drafting turns. Last setup
|
|
@@ -5200,6 +5217,23 @@ function startLocalPanel() {
|
|
|
5200
5217
|
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
5201
5218
|
localPanel = { url: `http://127.0.0.1:${port}/`, server: srv };
|
|
5202
5219
|
writePanelUrl(localPanel.url);
|
|
5220
|
+
// Periodic reclaim (2026-08-06, Codex port): on ChatGPT/Codex hosts the
|
|
5221
|
+
// app spawns a server per chat session and can kill it without the clean
|
|
5222
|
+
// relinquish (SIGKILL leaves panel-endpoint.json pointing at a dead pid).
|
|
5223
|
+
// Long-lived instances (the launchd keepalive server, a Desktop-hosted
|
|
5224
|
+
// one) re-run writePanelUrl on a slow tick: it cedes to any LIVE
|
|
5225
|
+
// registrant and only takes over dead/absent entries, so the shared file
|
|
5226
|
+
// self-heals within minutes instead of stranding menu-bar approvals.
|
|
5227
|
+
const reclaim = setInterval(() => {
|
|
5228
|
+
try {
|
|
5229
|
+
if (localPanel)
|
|
5230
|
+
writePanelUrl(localPanel.url);
|
|
5231
|
+
}
|
|
5232
|
+
catch {
|
|
5233
|
+
/* best-effort */
|
|
5234
|
+
}
|
|
5235
|
+
}, 120_000);
|
|
5236
|
+
reclaim.unref?.();
|
|
5203
5237
|
resolve(localPanel.url);
|
|
5204
5238
|
});
|
|
5205
5239
|
});
|
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.7.8
|
|
5
|
+
"version": "1.7.8",
|
|
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": {
|
|
@@ -1749,7 +1749,7 @@ class S4LMenuBar(rumps.App):
|
|
|
1749
1749
|
self._spin_i = 0
|
|
1750
1750
|
self._spinner = rumps.Timer(self._spin, 0.12)
|
|
1751
1751
|
self._spinner.start()
|
|
1752
|
-
self._notify("S4L", "Updating…
|
|
1752
|
+
self._notify("S4L", f"Updating… {self._update_host_word()} will restart in a moment.")
|
|
1753
1753
|
threading.Thread(target=self._mcpb_update_work, daemon=True).start()
|
|
1754
1754
|
self._tick(None)
|
|
1755
1755
|
|
|
@@ -1783,6 +1783,45 @@ class S4LMenuBar(rumps.App):
|
|
|
1783
1783
|
pass
|
|
1784
1784
|
return dirs
|
|
1785
1785
|
|
|
1786
|
+
# ---- install-flavor detection (2026-08-06, Codex/ChatGPT port) ---------
|
|
1787
|
+
# A box can carry either or both server installs: the Claude Desktop
|
|
1788
|
+
# extension dir (.mcpb double-click / npm lanes) and the host-neutral
|
|
1789
|
+
# bundle at <state>/app (curl-lane install for the ChatGPT/Codex app).
|
|
1790
|
+
# The updater refreshes every install that exists and restarts only the
|
|
1791
|
+
# host apps that correspond to them.
|
|
1792
|
+
def _has_claude_ext(self):
|
|
1793
|
+
return os.path.isdir(self._ext_dir())
|
|
1794
|
+
|
|
1795
|
+
@staticmethod
|
|
1796
|
+
def _app_bundle_dir():
|
|
1797
|
+
return os.path.join(st.state_dir(), "app")
|
|
1798
|
+
|
|
1799
|
+
def _has_app_bundle(self):
|
|
1800
|
+
return os.path.isfile(os.path.join(self._app_bundle_dir(), "dist", "index.js"))
|
|
1801
|
+
|
|
1802
|
+
def _update_host_word(self):
|
|
1803
|
+
"""Which host app the update flow will restart, for user-facing copy."""
|
|
1804
|
+
return "Claude Desktop" if self._has_claude_ext() else "ChatGPT"
|
|
1805
|
+
|
|
1806
|
+
@staticmethod
|
|
1807
|
+
def _chatgpt_running():
|
|
1808
|
+
try:
|
|
1809
|
+
return subprocess.run(["pgrep", "-x", "ChatGPT"],
|
|
1810
|
+
capture_output=True, timeout=10).returncode == 0
|
|
1811
|
+
except Exception:
|
|
1812
|
+
return False
|
|
1813
|
+
|
|
1814
|
+
def _restart_chatgpt_if_running(self):
|
|
1815
|
+
"""Bounce the ChatGPT app so its next MCP spawn runs the refreshed
|
|
1816
|
+
bundle. Only when it was already running: if it's closed there is
|
|
1817
|
+
nothing stale to replace (codex exec and the next app launch pick up
|
|
1818
|
+
the new files on their own)."""
|
|
1819
|
+
if not self._chatgpt_running():
|
|
1820
|
+
return
|
|
1821
|
+
subprocess.run(["killall", "ChatGPT"], capture_output=True)
|
|
1822
|
+
time.sleep(3)
|
|
1823
|
+
subprocess.run(["open", "-a", "ChatGPT"], capture_output=True, timeout=20)
|
|
1824
|
+
|
|
1786
1825
|
@staticmethod
|
|
1787
1826
|
def _claude_running():
|
|
1788
1827
|
"""True while a Claude Desktop main process is alive. pgrep -x matches
|
|
@@ -1862,13 +1901,26 @@ class S4LMenuBar(rumps.App):
|
|
|
1862
1901
|
self._sig = None
|
|
1863
1902
|
self._notify("S4L update failed", "Couldn't download the update — check your connection.")
|
|
1864
1903
|
return
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1904
|
+
# Refresh every install flavor present on this box (Claude
|
|
1905
|
+
# extension dir, ChatGPT/Codex app bundle, or both). Unzipping
|
|
1906
|
+
# into a flavor that isn't installed would fabricate a stray
|
|
1907
|
+
# install, so target only what exists; a box with neither keeps
|
|
1908
|
+
# the legacy behavior (ext dir) as the recovery path.
|
|
1909
|
+
has_ext = self._has_claude_ext()
|
|
1910
|
+
has_app = self._has_app_bundle()
|
|
1911
|
+
targets = [d for d, ok in ((self._ext_dir(), has_ext),
|
|
1912
|
+
(self._app_bundle_dir(), has_app)) if ok]
|
|
1913
|
+
if not targets:
|
|
1914
|
+
targets = [self._ext_dir()]
|
|
1915
|
+
has_ext = True
|
|
1916
|
+
for tdir in targets:
|
|
1917
|
+
r = subprocess.run(["unzip", "-oq", mcpb, "-d", tdir],
|
|
1918
|
+
capture_output=True, timeout=180)
|
|
1919
|
+
if r.returncode != 0:
|
|
1920
|
+
self._updating_mcpb = False
|
|
1921
|
+
self._sig = None
|
|
1922
|
+
self._notify("S4L update failed", "Couldn't unpack the update.")
|
|
1923
|
+
return
|
|
1872
1924
|
# Record what we just installed so the tick loop can verify the
|
|
1873
1925
|
# EFFECTIVE version actually advanced after the restart. The old
|
|
1874
1926
|
# flow claimed success unconditionally, which lied on boxes whose
|
|
@@ -1876,7 +1928,7 @@ class S4LMenuBar(rumps.App):
|
|
|
1876
1928
|
# extension dir updated but the running install stayed old.
|
|
1877
1929
|
target = ""
|
|
1878
1930
|
try:
|
|
1879
|
-
with open(os.path.join(
|
|
1931
|
+
with open(os.path.join(targets[0], "manifest.json")) as f:
|
|
1880
1932
|
target = str((json.load(f) or {}).get("version") or "")
|
|
1881
1933
|
except Exception:
|
|
1882
1934
|
target = ""
|
|
@@ -1886,36 +1938,42 @@ class S4LMenuBar(rumps.App):
|
|
|
1886
1938
|
json.dump({"target": target, "started_at": time.time()}, f)
|
|
1887
1939
|
except Exception:
|
|
1888
1940
|
pass
|
|
1889
|
-
# Restart
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1941
|
+
# Restart whichever host apps correspond to the refreshed installs
|
|
1942
|
+
# (we're decoupled from both).
|
|
1943
|
+
if has_ext:
|
|
1944
|
+
self._quit_claude_and_wait()
|
|
1945
|
+
# Claude is fully down now — relocate the autopilot scheduled tasks'
|
|
1946
|
+
# cwd so their once-a-minute runs stop flooding the user's interactive
|
|
1947
|
+
# `claude --resume` history. MUST happen while Claude is down (it caches
|
|
1948
|
+
# the registry in memory and clobbers live edits). See queueWorkerCwd()
|
|
1949
|
+
# in mcp/src/index.ts and the same routine in scripts/s4l_box_update.sh.
|
|
1950
|
+
# _fresh (not the in-process call): this is a mid-update self-heal,
|
|
1951
|
+
# so it must run the code that was JUST downloaded, not whatever
|
|
1952
|
+
# this already-running (about to be replaced) process imported at
|
|
1953
|
+
# ITS OWN startup — see _rewrite_scheduled_task_cwd_fresh's docstring.
|
|
1954
|
+
self._rewrite_scheduled_task_cwd_fresh()
|
|
1901
1955
|
if target:
|
|
1902
1956
|
# The graceful quit can eat minutes; restart the verify clock
|
|
1903
|
-
# now that
|
|
1957
|
+
# now that the host is actually down so UPDATE_VERIFY_GRACE_SEC
|
|
1904
1958
|
# measures server boot, not app teardown.
|
|
1905
1959
|
try:
|
|
1906
1960
|
with open(self._update_verify_path(), "w") as f:
|
|
1907
1961
|
json.dump({"target": target, "started_at": time.time()}, f)
|
|
1908
1962
|
except Exception:
|
|
1909
1963
|
pass
|
|
1910
|
-
|
|
1964
|
+
if has_ext:
|
|
1965
|
+
self._relaunch_claude(user_data_dirs)
|
|
1966
|
+
if has_app:
|
|
1967
|
+
self._restart_chatgpt_if_running()
|
|
1911
1968
|
self._update_available = False
|
|
1912
1969
|
self._sig = None
|
|
1970
|
+
host_word = "Claude" if has_ext else "ChatGPT"
|
|
1913
1971
|
if target:
|
|
1914
1972
|
# Honest phrasing: the verdict (success OR the real blocker)
|
|
1915
1973
|
# comes from _check_update_verdict once the new server settles.
|
|
1916
|
-
self._notify("S4L update", f"v{target} installed;
|
|
1974
|
+
self._notify("S4L update", f"v{target} installed; {host_word} is restarting. I'll confirm once it's live.")
|
|
1917
1975
|
else:
|
|
1918
|
-
self._notify("S4L updated", "
|
|
1976
|
+
self._notify("S4L updated", f"{host_word} restarted on the latest version.")
|
|
1919
1977
|
except Exception as e:
|
|
1920
1978
|
self._updating_mcpb = False
|
|
1921
1979
|
self._sig = None
|
|
@@ -4267,7 +4325,7 @@ class S4LMenuBar(rumps.App):
|
|
|
4267
4325
|
items.append(self._label(f"⬆ Update available · v{self._latest_version}"))
|
|
4268
4326
|
items.append(
|
|
4269
4327
|
rumps.MenuItem(
|
|
4270
|
-
"Update now & restart
|
|
4328
|
+
f"Update now & restart {self._update_host_word()}",
|
|
4271
4329
|
callback=self._do_mcpb_update,
|
|
4272
4330
|
)
|
|
4273
4331
|
)
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.8
|
|
3
|
+
"version": "1.7.8",
|
|
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
package/scripts/claude_job.py
CHANGED
|
@@ -875,6 +875,50 @@ def _run_codex_exec(ns, qtype: str, prompt: str, schema_text: str | None) -> int
|
|
|
875
875
|
_disarm_deathwatch(job_id)
|
|
876
876
|
|
|
877
877
|
|
|
878
|
+
def _run_claude_p(ns, qtype: str, prompt: str, schema_text: str | None) -> int:
|
|
879
|
+
"""claude-p provider: answer the turn inline via the real `claude -p`
|
|
880
|
+
(Claude Code CLI, subscription-authenticated). No queue, no worker, no
|
|
881
|
+
Desktop-app requirement. stdout passes through untouched: it IS the
|
|
882
|
+
claude json envelope the callers parse. Key transform vs the caller's
|
|
883
|
+
original argv: the schema goes INLINE (the CLI parses the --json-schema
|
|
884
|
+
value as JSON; a file path 400s), and the prompt rides stdin so huge
|
|
885
|
+
drafting prompts never hit ARG_MAX."""
|
|
886
|
+
batch = (os.environ.get("BATCH_ID") or os.environ.get("SA_CYCLE_ID") or "-").strip() or "-"
|
|
887
|
+
job_id = uuid.uuid4().hex
|
|
888
|
+
_arm_deathwatch(job_id, qtype, batch)
|
|
889
|
+
_act_write(qtype)
|
|
890
|
+
cmd = ["claude", "-p", "--output-format", "json"]
|
|
891
|
+
if schema_text:
|
|
892
|
+
cmd += ["--json-schema", schema_text]
|
|
893
|
+
budget = max(60, ns.timeout - 60)
|
|
894
|
+
_plog(f"claude-p start {qtype} job {job_id} batch={batch} timeout={budget}s")
|
|
895
|
+
started = time.time()
|
|
896
|
+
try:
|
|
897
|
+
proc = subprocess.run(cmd, input=prompt, capture_output=True, text=True, timeout=budget)
|
|
898
|
+
except subprocess.TimeoutExpired:
|
|
899
|
+
_act_clear()
|
|
900
|
+
_plog(f"claude-p timed out after {budget}s on job {job_id} ({qtype})")
|
|
901
|
+
_disarm_deathwatch(job_id)
|
|
902
|
+
return 79
|
|
903
|
+
except FileNotFoundError:
|
|
904
|
+
_act_clear()
|
|
905
|
+
_plog(f"claude-p provider active but no `claude` CLI on PATH; failing {qtype}")
|
|
906
|
+
_disarm_deathwatch(job_id)
|
|
907
|
+
return 1
|
|
908
|
+
sys.stdout.write(proc.stdout or "")
|
|
909
|
+
sys.stdout.flush()
|
|
910
|
+
if proc.returncode == 0 and '"subtype":"success"' in (proc.stdout or ""):
|
|
911
|
+
_mark_drain_success()
|
|
912
|
+
_stamp_heartbeat("claude-p", qtype)
|
|
913
|
+
_plog(f"claude-p done job {job_id} batch={batch} ({qtype}) in {int(time.time() - started)}s")
|
|
914
|
+
else:
|
|
915
|
+
_act_clear()
|
|
916
|
+
tail = (proc.stderr or "").strip()[-300:]
|
|
917
|
+
_plog(f"claude-p rc={proc.returncode} on job {job_id} ({qtype}); stderr tail: {tail}")
|
|
918
|
+
_disarm_deathwatch(job_id)
|
|
919
|
+
return proc.returncode
|
|
920
|
+
|
|
921
|
+
|
|
878
922
|
def cmd_provider(ns) -> int:
|
|
879
923
|
_apply_state_dir_override(ns)
|
|
880
924
|
qtype = TAG_TO_TYPE.get(ns.tag)
|
|
@@ -926,10 +970,13 @@ def cmd_provider(ns) -> int:
|
|
|
926
970
|
_plog(f"S4L paused; refusing to enqueue {qtype} job")
|
|
927
971
|
return 1
|
|
928
972
|
|
|
929
|
-
# Provider branch (2026-08-06). codex-exec
|
|
930
|
-
#
|
|
931
|
-
|
|
973
|
+
# Provider branch (2026-08-06). codex-exec and claude-p answer inline;
|
|
974
|
+
# anything else is the queue (the Claude Desktop worker default).
|
|
975
|
+
_prov = draft_provider.get()
|
|
976
|
+
if _prov == "codex-exec":
|
|
932
977
|
return _run_codex_exec(ns, qtype, prompt, schema_text)
|
|
978
|
+
if _prov == "claude-p":
|
|
979
|
+
return _run_claude_p(ns, qtype, prompt, schema_text)
|
|
933
980
|
|
|
934
981
|
job_id = uuid.uuid4().hex
|
|
935
982
|
created = time.time()
|
|
@@ -1384,16 +1431,14 @@ def cmd_eligible(ns: argparse.Namespace) -> int:
|
|
|
1384
1431
|
"""Routing probe for run_claude.sh: exit 0 when the tag is queue-mapped,
|
|
1385
1432
|
1 otherwise. TAG_TO_TYPE is the single routing truth — no env var.
|
|
1386
1433
|
|
|
1387
|
-
Provider
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1434
|
+
Provider selection happens INSIDE cmd_provider (codex-exec and claude-p
|
|
1435
|
+
both run inline there); eligible stays a pure tag probe. A fall-through
|
|
1436
|
+
design for claude-p was tried and reverted same-day 2026-08-06: pipeline
|
|
1437
|
+
callers pass --json-schema as a FILE PATH, and the real `claude` CLI
|
|
1438
|
+
parses that value as inline JSON, so the fall-through broke on every
|
|
1439
|
+
schema'd tag. The inline runner rewrites the schema to inline form.
|
|
1391
1440
|
"""
|
|
1392
|
-
if ns.tag
|
|
1393
|
-
return 1
|
|
1394
|
-
if draft_provider.get() == "claude-p":
|
|
1395
|
-
return 1
|
|
1396
|
-
return 0
|
|
1441
|
+
return 0 if ns.tag in TAG_TO_TYPE else 1
|
|
1397
1442
|
|
|
1398
1443
|
|
|
1399
1444
|
def main() -> int:
|
package/scripts/install.sh
CHANGED
|
@@ -107,6 +107,41 @@ os.replace(tmp, p)
|
|
|
107
107
|
PY
|
|
108
108
|
log "draft provider: $PROVIDER"
|
|
109
109
|
|
|
110
|
+
# 5. Persistent server (codex-flavor boxes only). Claude Desktop keeps the MCP
|
|
111
|
+
# server process alive while the app runs, so the menu bar always has a live
|
|
112
|
+
# loopback to POST approvals to. The ChatGPT app spawns servers per chat and
|
|
113
|
+
# kills them after, which would strand menu-bar approvals between sessions.
|
|
114
|
+
# A KeepAlive LaunchAgent holds one server up; stdin is held open by tail
|
|
115
|
+
# because the stdio MCP transport exits on stdin EOF.
|
|
116
|
+
if [ "$PROVIDER" = "codex-exec" ]; then
|
|
117
|
+
PLIST="$HOME/Library/LaunchAgents/com.m13v.s4l-server.plist"
|
|
118
|
+
mkdir -p "$HOME/Library/LaunchAgents" "$STATE_DIR/logs"
|
|
119
|
+
cat > "$PLIST" <<EOF
|
|
120
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
121
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
122
|
+
<plist version="1.0"><dict>
|
|
123
|
+
<key>Label</key><string>com.m13v.s4l-server</string>
|
|
124
|
+
<key>ProgramArguments</key><array>
|
|
125
|
+
<string>/bin/bash</string><string>-c</string>
|
|
126
|
+
<string>tail -f /dev/null | exec "$NODE" "$SERVER"</string>
|
|
127
|
+
</array>
|
|
128
|
+
<key>EnvironmentVariables</key><dict>
|
|
129
|
+
<key>S4L_HOST</key><string>codex</string>
|
|
130
|
+
<key>S4L_STATE_DIR</key><string>$STATE_DIR</string>
|
|
131
|
+
<key>PATH</key><string>$SAFE_PATH</string>
|
|
132
|
+
</dict>
|
|
133
|
+
<key>RunAtLoad</key><true/>
|
|
134
|
+
<key>KeepAlive</key><true/>
|
|
135
|
+
<key>StandardOutPath</key><string>$STATE_DIR/logs/s4l-server.log</string>
|
|
136
|
+
<key>StandardErrorPath</key><string>$STATE_DIR/logs/s4l-server.err.log</string>
|
|
137
|
+
</dict></plist>
|
|
138
|
+
EOF
|
|
139
|
+
launchctl bootout "gui/$(id -u)/com.m13v.s4l-server" 2>/dev/null || true
|
|
140
|
+
launchctl bootstrap "gui/$(id -u)" "$PLIST" 2>/dev/null \
|
|
141
|
+
&& log "persistent server LaunchAgent loaded (com.m13v.s4l-server)" \
|
|
142
|
+
|| log "WARN: could not load com.m13v.s4l-server; menu-bar approvals need the ChatGPT app open"
|
|
143
|
+
fi
|
|
144
|
+
|
|
110
145
|
log "done."
|
|
111
146
|
echo
|
|
112
147
|
echo "Next steps:"
|