@m13v/s4l 1.7.1-rc.13 → 1.7.1-rc.15
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 +163 -18
- package/mcp/dist/panel.html +42 -31
- package/mcp/dist/product-link.html +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +5 -1
- package/mcp/menubar/dashboard_server.py +12 -0
- package/mcp/menubar/s4l_menubar.py +55 -0
- package/mcp/menubar/s4l_state.py +79 -0
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/active_users.py +3 -6
- package/scripts/autopilot_stall_watch.py +10 -1
- package/scripts/claude_job.py +6 -0
- package/scripts/identity.py +26 -0
- package/scripts/twitter_post_plan.py +7 -1
- package/skill/dm-outreach-twitter.sh +19 -0
- package/skill/engage-twitter.sh +27 -0
package/mcp/dist/index.js
CHANGED
|
@@ -1181,10 +1181,21 @@ async function postApproved(batchId, plan) {
|
|
|
1181
1181
|
}
|
|
1182
1182
|
// Post failures are HANDLED in the pipeline (it returns a count, never throws),
|
|
1183
1183
|
// so they never reach Sentry on their own. Capture an explicit event whenever
|
|
1184
|
-
// the run exited non-zero OR
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
|
|
1184
|
+
// the run exited non-zero OR a REAL failure happened. This is the only
|
|
1185
|
+
// telemetry channel that reaches a customer .mcpb install (their cycle log
|
|
1186
|
+
// lives on their machine). install_id/hostname are auto-tagged.
|
|
1187
|
+
//
|
|
1188
|
+
// Gated on failure_reasons (not just realPosted < approved.length): the
|
|
1189
|
+
// Python pipeline already reports every shortfall to Sentry itself
|
|
1190
|
+
// (twitter_post_plan.py's capture_message), including benign skips like a
|
|
1191
|
+
// deleted target tweet. Re-reporting the SAME benign skip here as a second,
|
|
1192
|
+
// independently-fingerprinted `Error` (this capture group) doubled every
|
|
1193
|
+
// such event into two distinct Sentry issues. Only add this Node-side
|
|
1194
|
+
// capture when there's a real failure_reasons entry or a non-zero exit,
|
|
1195
|
+
// i.e. something the Python capture wouldn't already have flagged as an
|
|
1196
|
+
// actionable error on its own.
|
|
1197
|
+
const hasRealFailure = res.code !== 0 || Boolean(summObj?.failure_reasons);
|
|
1198
|
+
if (hasRealFailure) {
|
|
1188
1199
|
captureError(new Error(`post_drafts: ${realPosted}/${approved.length} posted (exit=${res.code})`), {
|
|
1189
1200
|
component: "post",
|
|
1190
1201
|
exit_code: String(res.code),
|
|
@@ -1594,7 +1605,10 @@ tool("engagement_mode", {
|
|
|
1594
1605
|
// persona is seeded. (2026-06-30) Skipped when promotion-only, since the
|
|
1595
1606
|
// product project isn't configured yet (it stays gated until project_config).
|
|
1596
1607
|
let kickerInstall = null;
|
|
1597
|
-
if (personalBrand) {
|
|
1608
|
+
if (personalBrand && isPaused()) {
|
|
1609
|
+
kickerInstall = { ok: false, detail: "skip (paused)" };
|
|
1610
|
+
}
|
|
1611
|
+
else if (personalBrand) {
|
|
1598
1612
|
try {
|
|
1599
1613
|
kickerInstall = await ensureQueueKickerInstalled();
|
|
1600
1614
|
console.error(`[engagement_mode] launchd kicker: ${kickerInstall.ok ? "ok" : "skip"} (${kickerInstall.detail})`);
|
|
@@ -2088,7 +2102,10 @@ tool("project_config", {
|
|
|
2088
2102
|
// persona-aware, so calling it from both setup paths is safe. Best-effort:
|
|
2089
2103
|
// a kicker hiccup never fails setup. (2026-06-30)
|
|
2090
2104
|
let kickerInstall = null;
|
|
2091
|
-
if (result.ready) {
|
|
2105
|
+
if (result.ready && isPaused()) {
|
|
2106
|
+
kickerInstall = { ok: false, detail: "skip (paused)" };
|
|
2107
|
+
}
|
|
2108
|
+
else if (result.ready) {
|
|
2092
2109
|
try {
|
|
2093
2110
|
kickerInstall = await ensureQueueKickerInstalled();
|
|
2094
2111
|
console.error(`[project_config] launchd kicker: ${kickerInstall.ok ? "ok" : "skip"} (${kickerInstall.detail})`);
|
|
@@ -2484,6 +2501,7 @@ tool("runtime", {
|
|
|
2484
2501
|
return jsonContent({
|
|
2485
2502
|
...snapshot,
|
|
2486
2503
|
menubar_running: await menubarRunning(),
|
|
2504
|
+
paused: isPaused(),
|
|
2487
2505
|
onboarding: onboardingSnapshot(),
|
|
2488
2506
|
});
|
|
2489
2507
|
});
|
|
@@ -2514,6 +2532,37 @@ tool("restart_menubar", {
|
|
|
2514
2532
|
menubar_running: running,
|
|
2515
2533
|
});
|
|
2516
2534
|
});
|
|
2535
|
+
// ---- pause_s4l: temporarily stop drafting/posting, reversibly --------------
|
|
2536
|
+
// The lighter alternative to Quit: unloads only the launchd jobs that scan,
|
|
2537
|
+
// draft, and post (plus their support daemons), leaving Claude Desktop, the
|
|
2538
|
+
// S4L tray, X connection, and the draft schedule registration untouched.
|
|
2539
|
+
// Fully reversible — action:'resume' reinstalls the exact same daemons via the
|
|
2540
|
+
// same idempotent ensure*Installed() functions boot uses. NOT "autopilot": S4L
|
|
2541
|
+
// is draft-first (a human approves every post), so this pauses/resumes the
|
|
2542
|
+
// draft pipeline itself, not some autonomous posting mode.
|
|
2543
|
+
tool("pause_s4l", {
|
|
2544
|
+
title: "Pause or resume S4L",
|
|
2545
|
+
description: "Pause temporarily stops S4L's own draft pipeline (the launchd kicker that scans/drafts/posts, " +
|
|
2546
|
+
"plus its reaper/stall-watch/memory-snapshot support jobs) WITHOUT touching Claude Desktop, the " +
|
|
2547
|
+
"S4L tray, your X connection, or the draft schedule registration — nothing is deleted, so it " +
|
|
2548
|
+
"survives a Claude Desktop restart and Resume brings it right back. Use when the user asks to " +
|
|
2549
|
+
"pause, stop, or temporarily disable S4L, or to resume/unpause it. This is NOT the same as Quit " +
|
|
2550
|
+
"(which kills and restarts Claude Desktop and removes the draft schedule) — Pause is the lighter, " +
|
|
2551
|
+
"fully reversible option. action:'status' (default) reports whether it's currently paused.",
|
|
2552
|
+
inputSchema: {
|
|
2553
|
+
action: z.enum(["status", "pause", "resume"]).optional(),
|
|
2554
|
+
},
|
|
2555
|
+
}, async ({ action }) => {
|
|
2556
|
+
if (action === "pause") {
|
|
2557
|
+
const res = await pauseS4L();
|
|
2558
|
+
return jsonContent({ action: "pause", paused: isPaused(), ...res });
|
|
2559
|
+
}
|
|
2560
|
+
if (action === "resume") {
|
|
2561
|
+
const res = await resumeS4L();
|
|
2562
|
+
return jsonContent({ action: "resume", paused: isPaused(), ...res });
|
|
2563
|
+
}
|
|
2564
|
+
return jsonContent({ action: "status", paused: isPaused() });
|
|
2565
|
+
});
|
|
2517
2566
|
// ---- report_diagnosis: ship a field diagnosis to the developers -------------
|
|
2518
2567
|
// First-class MCP wrapper over scripts/send_diagnostic_report.py (the same
|
|
2519
2568
|
// Sentry lane the menubar "Diagnose & fix" prompt uses). Before this existed
|
|
@@ -3576,6 +3625,76 @@ async function ensureMemorySnapshotInstalled() {
|
|
|
3576
3625
|
return { ok: false, detail: e?.message || String(e) };
|
|
3577
3626
|
}
|
|
3578
3627
|
}
|
|
3628
|
+
// ---- pause/resume: stop drafting/posting without touching Claude Desktop ---
|
|
3629
|
+
// Unlike Quit (which kills + relaunches Claude Desktop and deletes the draft
|
|
3630
|
+
// schedule), Pause only unloads the launchd jobs that actually DO work — the
|
|
3631
|
+
// kicker that scans/drafts/posts (TWITTER_AUTOPILOT_LABEL) plus its reaper,
|
|
3632
|
+
// stall-watch, and memory-snapshot support daemons — while leaving Claude
|
|
3633
|
+
// Desktop, the tray, and the Claude-native s4l-worker scheduled task alone.
|
|
3634
|
+
// The scheduled task still fires on its own cadence; claude_job.py's `next`
|
|
3635
|
+
// checks the same flag file and reports "no work" instantly instead of
|
|
3636
|
+
// draining the queue, so nothing drafts or posts while paused even if a
|
|
3637
|
+
// worker session wakes up mid-pause. A flag file (nothing is deleted) is what
|
|
3638
|
+
// makes Resume a plain reinstall of the same 4 daemons — see resumeS4L.
|
|
3639
|
+
function pauseFlagPath() {
|
|
3640
|
+
return path.join(s4lStateDir(), "paused.flag");
|
|
3641
|
+
}
|
|
3642
|
+
function isPaused() {
|
|
3643
|
+
try {
|
|
3644
|
+
return fs.existsSync(pauseFlagPath());
|
|
3645
|
+
}
|
|
3646
|
+
catch {
|
|
3647
|
+
return false;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const PAUSE_TARGETS = [
|
|
3651
|
+
{ label: TWITTER_AUTOPILOT_LABEL, plist: TWITTER_AUTOPILOT_PLIST },
|
|
3652
|
+
{ label: REAPER_LABEL, plist: REAPER_PLIST },
|
|
3653
|
+
{ label: STALL_WATCH_LABEL, plist: STALL_WATCH_PLIST },
|
|
3654
|
+
{ label: MEMORY_SNAPSHOT_LABEL, plist: MEMORY_SNAPSHOT_PLIST },
|
|
3655
|
+
];
|
|
3656
|
+
async function pauseS4L() {
|
|
3657
|
+
if (isPaused())
|
|
3658
|
+
return { ok: true, detail: "already paused" };
|
|
3659
|
+
try {
|
|
3660
|
+
fs.mkdirSync(path.dirname(pauseFlagPath()), { recursive: true });
|
|
3661
|
+
fs.writeFileSync(pauseFlagPath(), `paused at ${new Date().toISOString()}\n`, "utf-8");
|
|
3662
|
+
}
|
|
3663
|
+
catch (e) {
|
|
3664
|
+
return { ok: false, detail: `could not write pause flag: ${e?.message || e}` };
|
|
3665
|
+
}
|
|
3666
|
+
const uid = process.getuid ? process.getuid() : 0;
|
|
3667
|
+
const results = [];
|
|
3668
|
+
for (const { label, plist } of PAUSE_TARGETS) {
|
|
3669
|
+
try {
|
|
3670
|
+
const res = await unloadPlist(label, plist, uid);
|
|
3671
|
+
results.push(`${label}: unloaded (rc=${res.code})`);
|
|
3672
|
+
}
|
|
3673
|
+
catch (e) {
|
|
3674
|
+
results.push(`${label}: ${e?.message || e}`);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
return { ok: true, detail: results.join("; ") };
|
|
3678
|
+
}
|
|
3679
|
+
async function resumeS4L() {
|
|
3680
|
+
try {
|
|
3681
|
+
fs.rmSync(pauseFlagPath(), { force: true });
|
|
3682
|
+
}
|
|
3683
|
+
catch {
|
|
3684
|
+
/* best-effort */
|
|
3685
|
+
}
|
|
3686
|
+
const kicker = await ensureQueueKickerInstalled();
|
|
3687
|
+
const reaper = await ensureClaudeReaperInstalled();
|
|
3688
|
+
const stall = await ensureStallWatchInstalled();
|
|
3689
|
+
const mem = await ensureMemorySnapshotInstalled();
|
|
3690
|
+
const detail = [
|
|
3691
|
+
`kicker: ${kicker.ok ? "ok" : "skip"} (${kicker.detail})`,
|
|
3692
|
+
`reaper: ${reaper.ok ? "ok" : "skip"} (${reaper.detail})`,
|
|
3693
|
+
`stall-watch: ${stall.ok ? "ok" : "skip"} (${stall.detail})`,
|
|
3694
|
+
`memory-snapshot: ${mem.ok ? "ok" : "skip"} (${mem.detail})`,
|
|
3695
|
+
].join("; ");
|
|
3696
|
+
return { ok: true, detail };
|
|
3697
|
+
}
|
|
3579
3698
|
// Install/refresh the on-screen overlay watcher launchd job. Promotes the
|
|
3580
3699
|
// harness status overlay from a best-effort, fired-from-other-tools nicety to a
|
|
3581
3700
|
// first-class self-healing job. We run `harness_overlay.py watch` directly in
|
|
@@ -3771,6 +3890,7 @@ async function buildSnapshot() {
|
|
|
3771
3890
|
// (it's a launchctl check the Node side owns), so layer it on here. The panel
|
|
3772
3891
|
// uses it to offer a one-click "restart menu bar" when the tray was quit.
|
|
3773
3892
|
snap.menubar_running = await menubarRunning();
|
|
3893
|
+
snap.paused = isPaused();
|
|
3774
3894
|
await ensureDoctorPhase(snap.x_connected ? "full" : "pre_connect");
|
|
3775
3895
|
if (snap.runtime_ready)
|
|
3776
3896
|
completeOnboardingMilestone("runtime_ready");
|
|
@@ -4806,16 +4926,31 @@ async function main() {
|
|
|
4806
4926
|
catch (e) {
|
|
4807
4927
|
console.error(`[queue-worker] could not create worker folder: ${e?.message || e}`);
|
|
4808
4928
|
}
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4929
|
+
// The 4 pipeline daemons (kicker, reaper, stall-watch, memory-snapshot) are
|
|
4930
|
+
// skipped at boot while paused.flag is present, so a Claude Desktop restart
|
|
4931
|
+
// during a Pause doesn't silently un-pause the pipeline — see pauseS4L/
|
|
4932
|
+
// resumeS4L. Feedback-digest is NOT gated: it only distills past review
|
|
4933
|
+
// decisions, not drafting/posting, so it's harmless to keep running.
|
|
4934
|
+
if (!isPaused()) {
|
|
4935
|
+
void ensureQueueKickerInstalled()
|
|
4936
|
+
.then((r) => console.error(`[queue-worker] launchd kicker: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4937
|
+
.catch((e) => console.error("[queue-worker] kicker install failed:", e?.message || e));
|
|
4938
|
+
}
|
|
4939
|
+
else {
|
|
4940
|
+
console.error("[queue-worker] launchd kicker: skip (paused)");
|
|
4941
|
+
}
|
|
4812
4942
|
// Self-healing reaper for the agent-mode session leak the queue autopilot
|
|
4813
4943
|
// produces (finished `claude` worker sessions Desktop never tears down). A
|
|
4814
4944
|
// standalone guardrail; install unconditionally so it caps memory even on a
|
|
4815
4945
|
// box whose project isn't ready yet. Best-effort; must never block boot.
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4946
|
+
if (!isPaused()) {
|
|
4947
|
+
void ensureClaudeReaperInstalled()
|
|
4948
|
+
.then((r) => console.error(`[claude-reaper] launchd reaper: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4949
|
+
.catch((e) => console.error("[claude-reaper] reaper install failed:", e?.message || e));
|
|
4950
|
+
}
|
|
4951
|
+
else {
|
|
4952
|
+
console.error("[claude-reaper] launchd reaper: skip (paused)");
|
|
4953
|
+
}
|
|
4819
4954
|
// Feedback digest: hourly distillation of the user's card approve/reject
|
|
4820
4955
|
// decisions into learned_preferences (see scripts/feedback_digest.py).
|
|
4821
4956
|
// Best-effort; a box with no review events runs a no-op.
|
|
@@ -4825,15 +4960,25 @@ async function main() {
|
|
|
4825
4960
|
// Autopilot stall watchdog: fleet-side Sentry alert when the draft routines stop
|
|
4826
4961
|
// draining (most often an account switch orphaning them). The menu bar shows the
|
|
4827
4962
|
// user the Re-arm action; this is the part we see. Best-effort; never blocks boot.
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4963
|
+
if (!isPaused()) {
|
|
4964
|
+
void ensureStallWatchInstalled()
|
|
4965
|
+
.then((r) => console.error(`[stall-watch] launchd watchdog: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4966
|
+
.catch((e) => console.error("[stall-watch] watchdog install failed:", e?.message || e));
|
|
4967
|
+
}
|
|
4968
|
+
else {
|
|
4969
|
+
console.error("[stall-watch] launchd watchdog: skip (paused)");
|
|
4970
|
+
}
|
|
4831
4971
|
// Periodic host-resource sampler (memory/process snapshot -> local JSONL). Gives
|
|
4832
4972
|
// us per-box resource history to diagnose RAM blowups (e.g. the agent-mode
|
|
4833
4973
|
// session leak). Best-effort; never blocks boot. Disable with S4L_MEMORY_SNAPSHOT=0.
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4974
|
+
if (!isPaused()) {
|
|
4975
|
+
void ensureMemorySnapshotInstalled()
|
|
4976
|
+
.then((r) => console.error(`[memory-snapshot] launchd sampler: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4977
|
+
.catch((e) => console.error("[memory-snapshot] sampler install failed:", e?.message || e));
|
|
4978
|
+
}
|
|
4979
|
+
else {
|
|
4980
|
+
console.error("[memory-snapshot] launchd sampler: skip (paused)");
|
|
4981
|
+
}
|
|
4837
4982
|
// On-screen overlay watcher supervisor. The harness status overlay only renders
|
|
4838
4983
|
// while the watcher process is alive, and that watcher had no supervisor — when
|
|
4839
4984
|
// it died nothing respawned it and the overlay silently vanished. Install it as
|