@m13v/s4l 1.7.1-rc.16 → 1.7.1-rc.2
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/bin/cli.js +0 -23
- package/mcp/dist/index.js +24 -332
- package/mcp/dist/onboarding.js +10 -3
- package/mcp/dist/panel.html +31 -42
- package/mcp/dist/product-link.html +1 -1
- package/mcp/dist/runtime.js +1 -101
- package/mcp/dist/telemetry.js +3 -80
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -9
- package/mcp/menubar/dashboard_server.py +0 -12
- package/mcp/menubar/s4l_card.py +49 -292
- package/mcp/menubar/s4l_menubar.py +26 -269
- package/mcp/menubar/s4l_state.py +23 -169
- package/mcp/package.json +1 -1
- package/mcp/shared/doctor.cjs +0 -8
- package/mcp/shared/onboarding-ledger.cjs +0 -1
- package/package.json +1 -3
- package/scripts/active_users.py +6 -3
- package/scripts/autopilot_stall_watch.py +16 -200
- package/scripts/claude_job.py +0 -6
- package/scripts/feedback_digest.py +6 -34
- package/scripts/identity.py +0 -26
- package/scripts/learned_preferences.py +32 -241
- package/scripts/link_tail.py +91 -167
- package/scripts/memory_snapshot.py +0 -35
- package/scripts/pick_twitter_thread_target.py +1 -1
- package/scripts/release-mcpb.sh +0 -48
- package/scripts/reset-test-machine.sh +3 -85
- package/scripts/scheduled_tasks_snapshot.py +0 -107
- package/scripts/sentry_digest.py +189 -81
- package/scripts/sentry_init.py +15 -43
- package/scripts/setup_twitter_auth.py +38 -76
- package/scripts/snapshot.py +0 -20
- package/scripts/twitter_post_plan.py +13 -39
- package/skill/dm-outreach-twitter.sh +0 -19
- package/skill/engage-twitter.sh +0 -27
- package/skill/run-linkedin.sh +1 -9
- package/skill/run-twitter-cycle.sh +27 -199
- package/skill/sentry-digest.sh +12 -125
- package/scripts/get-latest-staging-mcpb.sh +0 -50
- package/scripts/mark_event.py +0 -128
- package/scripts/send_gmail_report.py +0 -76
package/bin/cli.js
CHANGED
|
@@ -823,29 +823,6 @@ function installMcp() {
|
|
|
823
823
|
} catch (e) {
|
|
824
824
|
console.warn(' WARNING: could not stamp MCP version:', e && e.message);
|
|
825
825
|
}
|
|
826
|
-
// Auto-opt a fresh install into the staging channel when the version being
|
|
827
|
-
// installed is itself a pre-release (-rc.N) — e.g. `npx social-autoposter@
|
|
828
|
-
// X.Y.Z-rc.N init`. Without this, channel.json stays absent, which
|
|
829
|
-
// scripts/s4l_channel.py's fail-safe default reads as "stable", so the box
|
|
830
|
-
// would install this one rc and then silently stop tracking staging (never
|
|
831
|
-
// pick up the next rc). Mirrors the same one-time write in
|
|
832
|
-
// mcp/src/runtime.ts's provision() for the .mcpb (Desktop) install path.
|
|
833
|
-
// Only writes when no channel marker exists yet — never overrides an
|
|
834
|
-
// existing preference (e.g. a user who already opted back out).
|
|
835
|
-
try {
|
|
836
|
-
const pkgVersion = require('../package.json').version;
|
|
837
|
-
if (pkgVersion.includes('-rc.')) {
|
|
838
|
-
const stateDir = process.env.S4L_STATE_DIR || path.join(os.homedir(), '.social-autoposter-mcp');
|
|
839
|
-
const channelPath = path.join(stateDir, 'channel.json');
|
|
840
|
-
if (!fs.existsSync(channelPath)) {
|
|
841
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
842
|
-
fs.writeFileSync(channelPath, JSON.stringify({ channel: 'staging' }, null, 2) + '\n');
|
|
843
|
-
console.log(' pre-release install detected -> opted into the staging channel');
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
} catch (e) {
|
|
847
|
-
console.warn(' WARNING: could not set staging channel:', e && e.message);
|
|
848
|
-
}
|
|
849
826
|
console.log(' installing MCP runtime deps (npm install --omit=dev in mcp/)');
|
|
850
827
|
const npmRes = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
851
828
|
cwd: mcpDest,
|
package/mcp/dist/index.js
CHANGED
|
@@ -22,10 +22,10 @@ import fs from "node:fs";
|
|
|
22
22
|
import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
|
|
23
23
|
import { applySetup, resolveProject, personaReady, listManagedProjectStatus, listProjectSettings, 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, clearMenubarStop, ensurePipelineCurrent, ensureRuntimeProvisioned,
|
|
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
|
-
import { initSentry, sendHeartbeat, sendStateSnapshot, captureError,
|
|
28
|
+
import { initSentry, sendHeartbeat, sendStateSnapshot, captureError, flushSentry, startLogStreaming, flushLogs, logLine } from "./telemetry.js";
|
|
29
29
|
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, getUiCapability, } from "@modelcontextprotocol/ext-apps/server";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import http from "node:http";
|
|
@@ -1181,21 +1181,10 @@ 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
|
-
// telemetry channel that reaches a customer .mcpb install (their cycle
|
|
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) {
|
|
1184
|
+
// the run exited non-zero OR fewer drafts posted than were approved. This is
|
|
1185
|
+
// the only telemetry channel that reaches a customer .mcpb install (their cycle
|
|
1186
|
+
// log lives on their machine). install_id/hostname are auto-tagged.
|
|
1187
|
+
if (res.code !== 0 || realPosted < approved.length) {
|
|
1199
1188
|
captureError(new Error(`post_drafts: ${realPosted}/${approved.length} posted (exit=${res.code})`), {
|
|
1200
1189
|
component: "post",
|
|
1201
1190
|
exit_code: String(res.code),
|
|
@@ -1605,10 +1594,7 @@ tool("engagement_mode", {
|
|
|
1605
1594
|
// persona is seeded. (2026-06-30) Skipped when promotion-only, since the
|
|
1606
1595
|
// product project isn't configured yet (it stays gated until project_config).
|
|
1607
1596
|
let kickerInstall = null;
|
|
1608
|
-
if (personalBrand
|
|
1609
|
-
kickerInstall = { ok: false, detail: "skip (paused)" };
|
|
1610
|
-
}
|
|
1611
|
-
else if (personalBrand) {
|
|
1597
|
+
if (personalBrand) {
|
|
1612
1598
|
try {
|
|
1613
1599
|
kickerInstall = await ensureQueueKickerInstalled();
|
|
1614
1600
|
console.error(`[engagement_mode] launchd kicker: ${kickerInstall.ok ? "ok" : "skip"} (${kickerInstall.detail})`);
|
|
@@ -1849,18 +1835,6 @@ tool("project_config", {
|
|
|
1849
1835
|
// as expected. Once connect_x succeeds, run the full phase immediately
|
|
1850
1836
|
// to verify persistence, CDP, and the durable cookie mirror.
|
|
1851
1837
|
doctorReport = await runDoctorPhase("full");
|
|
1852
|
-
// Only mark x_verified once doctor confirms all X checks pass (session valid,
|
|
1853
|
-
// cookies persisted, CDP responding, keychain accessible). This prevents the
|
|
1854
|
-
// autopilot from starting if X connection didn't actually persist.
|
|
1855
|
-
if (doctorReport?.ok) {
|
|
1856
|
-
completeOnboardingMilestone("x_verified", {
|
|
1857
|
-
x_session: doctorReport.checks?.find(c => c.id === "x_session")?.status,
|
|
1858
|
-
x_cookies: doctorReport.checks?.find(c => c.id === "x_cookie_sqlite")?.status,
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
else {
|
|
1862
|
-
recordOnboardingAttempt("x_verified", { doctor_ok: false });
|
|
1863
|
-
}
|
|
1864
1838
|
}
|
|
1865
1839
|
else {
|
|
1866
1840
|
blockOnboardingMilestone("x_connected", `x_${r.state || "not_connected"}`, r.error || r.note || summarizeXAuth(r), { state: r.state || "not_connected" });
|
|
@@ -2102,10 +2076,7 @@ tool("project_config", {
|
|
|
2102
2076
|
// persona-aware, so calling it from both setup paths is safe. Best-effort:
|
|
2103
2077
|
// a kicker hiccup never fails setup. (2026-06-30)
|
|
2104
2078
|
let kickerInstall = null;
|
|
2105
|
-
if (result.ready
|
|
2106
|
-
kickerInstall = { ok: false, detail: "skip (paused)" };
|
|
2107
|
-
}
|
|
2108
|
-
else if (result.ready) {
|
|
2079
|
+
if (result.ready) {
|
|
2109
2080
|
try {
|
|
2110
2081
|
kickerInstall = await ensureQueueKickerInstalled();
|
|
2111
2082
|
console.error(`[project_config] launchd kicker: ${kickerInstall.ok ? "ok" : "skip"} (${kickerInstall.detail})`);
|
|
@@ -2183,16 +2154,7 @@ tool("post_drafts", {
|
|
|
2183
2154
|
.optional()
|
|
2184
2155
|
.describe("1-based draft numbers to post as drafted, e.g. [1, 3, 5]."),
|
|
2185
2156
|
edits: z
|
|
2186
|
-
.array(z.object({
|
|
2187
|
-
n: z.number().int().positive(),
|
|
2188
|
-
text: z.string(),
|
|
2189
|
-
variant: z
|
|
2190
|
-
.enum(["a", "b"])
|
|
2191
|
-
.optional()
|
|
2192
|
-
.describe("Two-draft cards only: which of candidate n's `drafts` entries this text came " +
|
|
2193
|
-
"from, so the candidate's style/assigned_style/assigned_mode are switched to " +
|
|
2194
|
-
"match (otherwise engagement_style stats would be attributed to the wrong style)."),
|
|
2195
|
-
}))
|
|
2157
|
+
.array(z.object({ n: z.number().int().positive(), text: z.string() }))
|
|
2196
2158
|
.optional()
|
|
2197
2159
|
.describe("Rewrites: each {n, text} replaces draft n's wording, then posts it."),
|
|
2198
2160
|
post_all: z.boolean().optional().describe("Post every draft in the batch."),
|
|
@@ -2248,22 +2210,7 @@ tool("post_drafts", {
|
|
|
2248
2210
|
warnings.push(`ignored empty edit for #${e.n}`);
|
|
2249
2211
|
return;
|
|
2250
2212
|
}
|
|
2251
|
-
|
|
2252
|
-
c.reply_text = text;
|
|
2253
|
-
// Two-draft cards: the human switched to the OTHER draft (with or
|
|
2254
|
-
// without further hand-editing its text). Carry that draft's own
|
|
2255
|
-
// style/assigned_style/assigned_mode onto the candidate so
|
|
2256
|
-
// twitter_post_plan.py's per-candidate drift-coercion posts under (and
|
|
2257
|
-
// logs) the style that's ACTUALLY posting, not whichever draft was
|
|
2258
|
-
// recommended at plan-write time.
|
|
2259
|
-
if (e.variant && Array.isArray(c.drafts)) {
|
|
2260
|
-
const chosen = c.drafts.find((d) => d.variant === e.variant);
|
|
2261
|
-
if (chosen) {
|
|
2262
|
-
c.engagement_style = chosen.style ?? c.engagement_style;
|
|
2263
|
-
c.assigned_style = chosen.assigned_style ?? null;
|
|
2264
|
-
c.assigned_mode = chosen.assigned_mode ?? c.assigned_mode;
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2213
|
+
candidates[e.n - 1].reply_text = text;
|
|
2267
2214
|
approve.add(e.n);
|
|
2268
2215
|
editedCount++;
|
|
2269
2216
|
});
|
|
@@ -2506,26 +2453,16 @@ tool("runtime", {
|
|
|
2506
2453
|
});
|
|
2507
2454
|
}
|
|
2508
2455
|
// ---- status (default): runtime install snapshot -----------------------
|
|
2509
|
-
|
|
2456
|
+
const snapshot = runtimeSnapshot();
|
|
2510
2457
|
if (snapshot.runtime_ready) {
|
|
2511
2458
|
completeOnboardingMilestone("runtime_ready");
|
|
2512
2459
|
}
|
|
2513
2460
|
else if (snapshot.progress?.done && !snapshot.progress.ok) {
|
|
2514
|
-
|
|
2515
|
-
// self-heals a transient failure instead of parking until the next boot;
|
|
2516
|
-
// the provisioner cleans its own partial artifacts, so a retry is safe.
|
|
2517
|
-
// Only surface the failure to the onboarding ledger once retries are spent.
|
|
2518
|
-
if (retryProvisionIfStalled()) {
|
|
2519
|
-
snapshot = runtimeSnapshot(); // reflect the restarted, in-flight run
|
|
2520
|
-
}
|
|
2521
|
-
else {
|
|
2522
|
-
blockOnboardingMilestone("runtime_ready", "runtime_install_failed", snapshot.progress.error || "Runtime installation failed", { outcome: "failed" });
|
|
2523
|
-
}
|
|
2461
|
+
blockOnboardingMilestone("runtime_ready", "runtime_install_failed", snapshot.progress.error || "Runtime installation failed", { outcome: "failed" });
|
|
2524
2462
|
}
|
|
2525
2463
|
return jsonContent({
|
|
2526
2464
|
...snapshot,
|
|
2527
2465
|
menubar_running: await menubarRunning(),
|
|
2528
|
-
paused: isPaused(),
|
|
2529
2466
|
onboarding: onboardingSnapshot(),
|
|
2530
2467
|
});
|
|
2531
2468
|
});
|
|
@@ -2556,37 +2493,6 @@ tool("restart_menubar", {
|
|
|
2556
2493
|
menubar_running: running,
|
|
2557
2494
|
});
|
|
2558
2495
|
});
|
|
2559
|
-
// ---- pause_s4l: temporarily stop drafting/posting, reversibly --------------
|
|
2560
|
-
// The lighter alternative to Quit: unloads only the launchd jobs that scan,
|
|
2561
|
-
// draft, and post (plus their support daemons), leaving Claude Desktop, the
|
|
2562
|
-
// S4L tray, X connection, and the draft schedule registration untouched.
|
|
2563
|
-
// Fully reversible — action:'resume' reinstalls the exact same daemons via the
|
|
2564
|
-
// same idempotent ensure*Installed() functions boot uses. NOT "autopilot": S4L
|
|
2565
|
-
// is draft-first (a human approves every post), so this pauses/resumes the
|
|
2566
|
-
// draft pipeline itself, not some autonomous posting mode.
|
|
2567
|
-
tool("pause_s4l", {
|
|
2568
|
-
title: "Pause or resume S4L",
|
|
2569
|
-
description: "Pause temporarily stops S4L's own draft pipeline (the launchd kicker that scans/drafts/posts, " +
|
|
2570
|
-
"plus its reaper/stall-watch/memory-snapshot support jobs) WITHOUT touching Claude Desktop, the " +
|
|
2571
|
-
"S4L tray, your X connection, or the draft schedule registration — nothing is deleted, so it " +
|
|
2572
|
-
"survives a Claude Desktop restart and Resume brings it right back. Use when the user asks to " +
|
|
2573
|
-
"pause, stop, or temporarily disable S4L, or to resume/unpause it. This is NOT the same as Quit " +
|
|
2574
|
-
"(which kills and restarts Claude Desktop and removes the draft schedule) — Pause is the lighter, " +
|
|
2575
|
-
"fully reversible option. action:'status' (default) reports whether it's currently paused.",
|
|
2576
|
-
inputSchema: {
|
|
2577
|
-
action: z.enum(["status", "pause", "resume"]).optional(),
|
|
2578
|
-
},
|
|
2579
|
-
}, async ({ action }) => {
|
|
2580
|
-
if (action === "pause") {
|
|
2581
|
-
const res = await pauseS4L();
|
|
2582
|
-
return jsonContent({ action: "pause", paused: isPaused(), ...res });
|
|
2583
|
-
}
|
|
2584
|
-
if (action === "resume") {
|
|
2585
|
-
const res = await resumeS4L();
|
|
2586
|
-
return jsonContent({ action: "resume", paused: isPaused(), ...res });
|
|
2587
|
-
}
|
|
2588
|
-
return jsonContent({ action: "status", paused: isPaused() });
|
|
2589
|
-
});
|
|
2590
2496
|
// ---- report_diagnosis: ship a field diagnosis to the developers -------------
|
|
2591
2497
|
// First-class MCP wrapper over scripts/send_diagnostic_report.py (the same
|
|
2592
2498
|
// Sentry lane the menubar "Diagnose & fix" prompt uses). Before this existed
|
|
@@ -2625,29 +2531,6 @@ tool("report_diagnosis", {
|
|
|
2625
2531
|
return jsonContent({ ok: false, detail: String(e?.message || e).slice(0, 300) });
|
|
2626
2532
|
}
|
|
2627
2533
|
});
|
|
2628
|
-
// ---- client_event: lightweight UI telemetry ping from the dashboard panel --
|
|
2629
|
-
// The panel iframe is a browser context with no Sentry SDK and no server-side
|
|
2630
|
-
// telemetry access of its own. Before this, a panel button click (e.g. "Set up
|
|
2631
|
-
// draft schedule") had NO record anywhere — report_diagnosis needs a full
|
|
2632
|
-
// markdown report from an agent turn, which doesn't fit a plain click, so the
|
|
2633
|
-
// panel's rearm button silently had zero telemetry while its menu-bar sibling
|
|
2634
|
-
// did (see s4l_menubar.py _capture_msg). Not for agent use: the panel calls
|
|
2635
|
-
// this directly via app.callServerTool, never through chat.
|
|
2636
|
-
tool("client_event", {
|
|
2637
|
-
title: "Log a lightweight client UI event",
|
|
2638
|
-
description: "Internal telemetry hook for the dashboard panel to report a UI event (e.g. a button click). " +
|
|
2639
|
-
"Not intended for the agent to call from chat.",
|
|
2640
|
-
inputSchema: {
|
|
2641
|
-
event: z.string().describe("Short event name, e.g. rearm_clicked"),
|
|
2642
|
-
surface: z.string().optional().describe("UI surface the event came from, e.g. panel"),
|
|
2643
|
-
},
|
|
2644
|
-
}, async ({ event, surface }) => {
|
|
2645
|
-
captureMessage(`S4L client event: ${event}`, {
|
|
2646
|
-
level: "info",
|
|
2647
|
-
tags: { component: "panel", event, surface: surface || "panel" },
|
|
2648
|
-
});
|
|
2649
|
-
return jsonContent({ ok: true });
|
|
2650
|
-
});
|
|
2651
2534
|
function runtimeSnapshot() {
|
|
2652
2535
|
const rt = readRuntime();
|
|
2653
2536
|
const progress = readProgress();
|
|
@@ -3342,18 +3225,6 @@ async function ensureQueueKickerInstalled() {
|
|
|
3342
3225
|
detail: "no ready project or active persona yet",
|
|
3343
3226
|
};
|
|
3344
3227
|
}
|
|
3345
|
-
// Additional gate: X must be connected and verified before autopilot starts.
|
|
3346
|
-
// This prevents wasted cycles against a logged-out x.com if X connection didn't
|
|
3347
|
-
// persist after import/login. x_verified milestone only completes after doctor
|
|
3348
|
-
// confirms X session and cookies are valid.
|
|
3349
|
-
const onboardingState = onboardingSnapshot();
|
|
3350
|
-
const xVerified = onboardingState?.milestones?.some((m) => m.id === "x_verified" && m.status === "complete");
|
|
3351
|
-
if (!xVerified) {
|
|
3352
|
-
return {
|
|
3353
|
-
ok: false,
|
|
3354
|
-
detail: "X not yet verified (awaiting explicit pre-flight confirmation)",
|
|
3355
|
-
};
|
|
3356
|
-
}
|
|
3357
3228
|
const logDir = path.join(repoDir(), "skill", "logs");
|
|
3358
3229
|
try {
|
|
3359
3230
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -3649,76 +3520,6 @@ async function ensureMemorySnapshotInstalled() {
|
|
|
3649
3520
|
return { ok: false, detail: e?.message || String(e) };
|
|
3650
3521
|
}
|
|
3651
3522
|
}
|
|
3652
|
-
// ---- pause/resume: stop drafting/posting without touching Claude Desktop ---
|
|
3653
|
-
// Unlike Quit (which kills + relaunches Claude Desktop and deletes the draft
|
|
3654
|
-
// schedule), Pause only unloads the launchd jobs that actually DO work — the
|
|
3655
|
-
// kicker that scans/drafts/posts (TWITTER_AUTOPILOT_LABEL) plus its reaper,
|
|
3656
|
-
// stall-watch, and memory-snapshot support daemons — while leaving Claude
|
|
3657
|
-
// Desktop, the tray, and the Claude-native s4l-worker scheduled task alone.
|
|
3658
|
-
// The scheduled task still fires on its own cadence; claude_job.py's `next`
|
|
3659
|
-
// checks the same flag file and reports "no work" instantly instead of
|
|
3660
|
-
// draining the queue, so nothing drafts or posts while paused even if a
|
|
3661
|
-
// worker session wakes up mid-pause. A flag file (nothing is deleted) is what
|
|
3662
|
-
// makes Resume a plain reinstall of the same 4 daemons — see resumeS4L.
|
|
3663
|
-
function pauseFlagPath() {
|
|
3664
|
-
return path.join(s4lStateDir(), "paused.flag");
|
|
3665
|
-
}
|
|
3666
|
-
function isPaused() {
|
|
3667
|
-
try {
|
|
3668
|
-
return fs.existsSync(pauseFlagPath());
|
|
3669
|
-
}
|
|
3670
|
-
catch {
|
|
3671
|
-
return false;
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
const PAUSE_TARGETS = [
|
|
3675
|
-
{ label: TWITTER_AUTOPILOT_LABEL, plist: TWITTER_AUTOPILOT_PLIST },
|
|
3676
|
-
{ label: REAPER_LABEL, plist: REAPER_PLIST },
|
|
3677
|
-
{ label: STALL_WATCH_LABEL, plist: STALL_WATCH_PLIST },
|
|
3678
|
-
{ label: MEMORY_SNAPSHOT_LABEL, plist: MEMORY_SNAPSHOT_PLIST },
|
|
3679
|
-
];
|
|
3680
|
-
async function pauseS4L() {
|
|
3681
|
-
if (isPaused())
|
|
3682
|
-
return { ok: true, detail: "already paused" };
|
|
3683
|
-
try {
|
|
3684
|
-
fs.mkdirSync(path.dirname(pauseFlagPath()), { recursive: true });
|
|
3685
|
-
fs.writeFileSync(pauseFlagPath(), `paused at ${new Date().toISOString()}\n`, "utf-8");
|
|
3686
|
-
}
|
|
3687
|
-
catch (e) {
|
|
3688
|
-
return { ok: false, detail: `could not write pause flag: ${e?.message || e}` };
|
|
3689
|
-
}
|
|
3690
|
-
const uid = process.getuid ? process.getuid() : 0;
|
|
3691
|
-
const results = [];
|
|
3692
|
-
for (const { label, plist } of PAUSE_TARGETS) {
|
|
3693
|
-
try {
|
|
3694
|
-
const res = await unloadPlist(label, plist, uid);
|
|
3695
|
-
results.push(`${label}: unloaded (rc=${res.code})`);
|
|
3696
|
-
}
|
|
3697
|
-
catch (e) {
|
|
3698
|
-
results.push(`${label}: ${e?.message || e}`);
|
|
3699
|
-
}
|
|
3700
|
-
}
|
|
3701
|
-
return { ok: true, detail: results.join("; ") };
|
|
3702
|
-
}
|
|
3703
|
-
async function resumeS4L() {
|
|
3704
|
-
try {
|
|
3705
|
-
fs.rmSync(pauseFlagPath(), { force: true });
|
|
3706
|
-
}
|
|
3707
|
-
catch {
|
|
3708
|
-
/* best-effort */
|
|
3709
|
-
}
|
|
3710
|
-
const kicker = await ensureQueueKickerInstalled();
|
|
3711
|
-
const reaper = await ensureClaudeReaperInstalled();
|
|
3712
|
-
const stall = await ensureStallWatchInstalled();
|
|
3713
|
-
const mem = await ensureMemorySnapshotInstalled();
|
|
3714
|
-
const detail = [
|
|
3715
|
-
`kicker: ${kicker.ok ? "ok" : "skip"} (${kicker.detail})`,
|
|
3716
|
-
`reaper: ${reaper.ok ? "ok" : "skip"} (${reaper.detail})`,
|
|
3717
|
-
`stall-watch: ${stall.ok ? "ok" : "skip"} (${stall.detail})`,
|
|
3718
|
-
`memory-snapshot: ${mem.ok ? "ok" : "skip"} (${mem.detail})`,
|
|
3719
|
-
].join("; ");
|
|
3720
|
-
return { ok: true, detail };
|
|
3721
|
-
}
|
|
3722
3523
|
// Install/refresh the on-screen overlay watcher launchd job. Promotes the
|
|
3723
3524
|
// harness status overlay from a best-effort, fired-from-other-tools nicety to a
|
|
3724
3525
|
// first-class self-healing job. We run `harness_overlay.py watch` directly in
|
|
@@ -3786,73 +3587,6 @@ async function ensureOverlayWatchInstalled() {
|
|
|
3786
3587
|
return { ok: false, detail: e?.message || String(e) };
|
|
3787
3588
|
}
|
|
3788
3589
|
}
|
|
3789
|
-
// Install/refresh the daily self-updater launchd job. This used to be bundled
|
|
3790
|
-
// into the now-deleted `autopilot` MCP tool (removed 2026-06-19, 88bd1cb9):
|
|
3791
|
-
// calling `autopilot enable` installed this job as a side effect, so removing
|
|
3792
|
-
// the tool silently orphaned it — UPDATER_LABEL/UPDATER_PLIST and the
|
|
3793
|
-
// auto_update_on status check survived (buildSnapshot still reports them), but
|
|
3794
|
-
// nothing has installed the plist since, on ANY box provisioned after that
|
|
3795
|
-
// commit (auto_update_on reads false forever). Restored here as its own
|
|
3796
|
-
// deterministic boot-time job, same pattern as its five siblings above.
|
|
3797
|
-
//
|
|
3798
|
-
// Points at scripts/s4l_box_update.sh, NOT skill/social-autoposter-update.sh:
|
|
3799
|
-
// the latter is the npm-lane updater (npm view + npx update) and is a silent
|
|
3800
|
-
// no-op on a .mcpb box, which has no npm/npx on PATH (see version.ts). The
|
|
3801
|
-
// .mcpb-lane equivalent downloads the .mcpb directly from the channel-resolved
|
|
3802
|
-
// GitHub release and unpacks it over the extension dir — see that script's own
|
|
3803
|
-
// header for the channel/no-downgrade/retry guards. Default mode there
|
|
3804
|
-
// downloads + unpacks + restarts Claude Desktop with NO human in the loop,
|
|
3805
|
-
// matching the original bundled updater's intent ("keeps a headless install
|
|
3806
|
-
// current"); RunAtLoad so a box that boots already-behind checks promptly.
|
|
3807
|
-
async function ensureUpdaterInstalled() {
|
|
3808
|
-
try {
|
|
3809
|
-
if (process.platform !== "darwin")
|
|
3810
|
-
return { ok: false, detail: "not macOS" };
|
|
3811
|
-
if ((process.env.S4L_AUTO_UPDATE) === "0")
|
|
3812
|
-
return { ok: false, detail: "disabled (S4L_AUTO_UPDATE=0)" };
|
|
3813
|
-
const logDir = path.join(repoDir(), "skill", "logs");
|
|
3814
|
-
try {
|
|
3815
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
3816
|
-
}
|
|
3817
|
-
catch {
|
|
3818
|
-
/* best-effort */
|
|
3819
|
-
}
|
|
3820
|
-
const xml = plistXml({
|
|
3821
|
-
label: UPDATER_LABEL,
|
|
3822
|
-
programArgs: ["/bin/bash", path.join(repoDir(), "scripts", "s4l_box_update.sh")],
|
|
3823
|
-
intervalSecs: 86_400,
|
|
3824
|
-
runAtLoad: true,
|
|
3825
|
-
stdoutLog: path.join(logDir, "launchd-self-update-stdout.log"),
|
|
3826
|
-
stderrLog: path.join(logDir, "launchd-self-update-stderr.log"),
|
|
3827
|
-
});
|
|
3828
|
-
const uid = process.getuid ? process.getuid() : 0;
|
|
3829
|
-
let cur = null;
|
|
3830
|
-
try {
|
|
3831
|
-
cur = fs.readFileSync(UPDATER_PLIST, "utf-8");
|
|
3832
|
-
}
|
|
3833
|
-
catch {
|
|
3834
|
-
cur = null;
|
|
3835
|
-
}
|
|
3836
|
-
let detail;
|
|
3837
|
-
if (cur === xml) {
|
|
3838
|
-
const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
|
|
3839
|
-
detail = `current (load rc=${res.code})`;
|
|
3840
|
-
}
|
|
3841
|
-
else {
|
|
3842
|
-
if (cur !== null) {
|
|
3843
|
-
await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
|
|
3844
|
-
}
|
|
3845
|
-
fs.mkdirSync(path.dirname(UPDATER_PLIST), { recursive: true });
|
|
3846
|
-
fs.writeFileSync(UPDATER_PLIST, xml, "utf-8");
|
|
3847
|
-
const res = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
|
|
3848
|
-
detail = cur === null ? "installed + loaded" : `rewritten + reloaded (rc=${res.code})`;
|
|
3849
|
-
}
|
|
3850
|
-
return { ok: true, detail };
|
|
3851
|
-
}
|
|
3852
|
-
catch (e) {
|
|
3853
|
-
return { ok: false, detail: e?.message || String(e) };
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
3590
|
// Is the draft schedule registered AND running for the LIVE account?
|
|
3857
3591
|
// 'ok' — worker tasks present+enabled and FIRING (host actively running).
|
|
3858
3592
|
// 'disabled' — present but a worker task is disabled.
|
|
@@ -3914,7 +3648,6 @@ async function buildSnapshot() {
|
|
|
3914
3648
|
// (it's a launchctl check the Node side owns), so layer it on here. The panel
|
|
3915
3649
|
// uses it to offer a one-click "restart menu bar" when the tray was quit.
|
|
3916
3650
|
snap.menubar_running = await menubarRunning();
|
|
3917
|
-
snap.paused = isPaused();
|
|
3918
3651
|
await ensureDoctorPhase(snap.x_connected ? "full" : "pre_connect");
|
|
3919
3652
|
if (snap.runtime_ready)
|
|
3920
3653
|
completeOnboardingMilestone("runtime_ready");
|
|
@@ -4900,15 +4633,6 @@ async function drainApprovedBacklog() {
|
|
|
4900
4633
|
}
|
|
4901
4634
|
async function main() {
|
|
4902
4635
|
initSentry();
|
|
4903
|
-
// Detect a self-update (old_version -> new_version) as the very first thing
|
|
4904
|
-
// after Sentry is up, before anything else that could restart/exit. See
|
|
4905
|
-
// checkVersionChange's own docstring for why this exists.
|
|
4906
|
-
try {
|
|
4907
|
-
checkVersionChange();
|
|
4908
|
-
}
|
|
4909
|
-
catch (e) {
|
|
4910
|
-
console.error("[social-autoposter-mcp] version-change check failed:", e?.message || e);
|
|
4911
|
-
}
|
|
4912
4636
|
// Tee the verbatim stdout/stderr of every pipeline subprocess to the s4l
|
|
4913
4637
|
// Cloud Run relay (-> Cloud Logging) so we can troubleshoot/rescue any user
|
|
4914
4638
|
// scenario (silent stalls, partial onboarding) without asking them to ship a
|
|
@@ -4950,31 +4674,16 @@ async function main() {
|
|
|
4950
4674
|
catch (e) {
|
|
4951
4675
|
console.error(`[queue-worker] could not create worker folder: ${e?.message || e}`);
|
|
4952
4676
|
}
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
// resumeS4L. Feedback-digest is NOT gated: it only distills past review
|
|
4957
|
-
// decisions, not drafting/posting, so it's harmless to keep running.
|
|
4958
|
-
if (!isPaused()) {
|
|
4959
|
-
void ensureQueueKickerInstalled()
|
|
4960
|
-
.then((r) => console.error(`[queue-worker] launchd kicker: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4961
|
-
.catch((e) => console.error("[queue-worker] kicker install failed:", e?.message || e));
|
|
4962
|
-
}
|
|
4963
|
-
else {
|
|
4964
|
-
console.error("[queue-worker] launchd kicker: skip (paused)");
|
|
4965
|
-
}
|
|
4677
|
+
void ensureQueueKickerInstalled()
|
|
4678
|
+
.then((r) => console.error(`[queue-worker] launchd kicker: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4679
|
+
.catch((e) => console.error("[queue-worker] kicker install failed:", e?.message || e));
|
|
4966
4680
|
// Self-healing reaper for the agent-mode session leak the queue autopilot
|
|
4967
4681
|
// produces (finished `claude` worker sessions Desktop never tears down). A
|
|
4968
4682
|
// standalone guardrail; install unconditionally so it caps memory even on a
|
|
4969
4683
|
// box whose project isn't ready yet. Best-effort; must never block boot.
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
.catch((e) => console.error("[claude-reaper] reaper install failed:", e?.message || e));
|
|
4974
|
-
}
|
|
4975
|
-
else {
|
|
4976
|
-
console.error("[claude-reaper] launchd reaper: skip (paused)");
|
|
4977
|
-
}
|
|
4684
|
+
void ensureClaudeReaperInstalled()
|
|
4685
|
+
.then((r) => console.error(`[claude-reaper] launchd reaper: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4686
|
+
.catch((e) => console.error("[claude-reaper] reaper install failed:", e?.message || e));
|
|
4978
4687
|
// Feedback digest: hourly distillation of the user's card approve/reject
|
|
4979
4688
|
// decisions into learned_preferences (see scripts/feedback_digest.py).
|
|
4980
4689
|
// Best-effort; a box with no review events runs a no-op.
|
|
@@ -4984,25 +4693,15 @@ async function main() {
|
|
|
4984
4693
|
// Autopilot stall watchdog: fleet-side Sentry alert when the draft routines stop
|
|
4985
4694
|
// draining (most often an account switch orphaning them). The menu bar shows the
|
|
4986
4695
|
// user the Re-arm action; this is the part we see. Best-effort; never blocks boot.
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
.catch((e) => console.error("[stall-watch] watchdog install failed:", e?.message || e));
|
|
4991
|
-
}
|
|
4992
|
-
else {
|
|
4993
|
-
console.error("[stall-watch] launchd watchdog: skip (paused)");
|
|
4994
|
-
}
|
|
4696
|
+
void ensureStallWatchInstalled()
|
|
4697
|
+
.then((r) => console.error(`[stall-watch] launchd watchdog: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4698
|
+
.catch((e) => console.error("[stall-watch] watchdog install failed:", e?.message || e));
|
|
4995
4699
|
// Periodic host-resource sampler (memory/process snapshot -> local JSONL). Gives
|
|
4996
4700
|
// us per-box resource history to diagnose RAM blowups (e.g. the agent-mode
|
|
4997
4701
|
// session leak). Best-effort; never blocks boot. Disable with S4L_MEMORY_SNAPSHOT=0.
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
.catch((e) => console.error("[memory-snapshot] sampler install failed:", e?.message || e));
|
|
5002
|
-
}
|
|
5003
|
-
else {
|
|
5004
|
-
console.error("[memory-snapshot] launchd sampler: skip (paused)");
|
|
5005
|
-
}
|
|
4702
|
+
void ensureMemorySnapshotInstalled()
|
|
4703
|
+
.then((r) => console.error(`[memory-snapshot] launchd sampler: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
4704
|
+
.catch((e) => console.error("[memory-snapshot] sampler install failed:", e?.message || e));
|
|
5006
4705
|
// On-screen overlay watcher supervisor. The harness status overlay only renders
|
|
5007
4706
|
// while the watcher process is alive, and that watcher had no supervisor — when
|
|
5008
4707
|
// it died nothing respawned it and the overlay silently vanished. Install it as
|
|
@@ -5011,13 +4710,6 @@ async function main() {
|
|
|
5011
4710
|
void ensureOverlayWatchInstalled()
|
|
5012
4711
|
.then((r) => console.error(`[overlay-watch] launchd supervisor: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
5013
4712
|
.catch((e) => console.error("[overlay-watch] supervisor install failed:", e?.message || e));
|
|
5014
|
-
// Daily self-updater: restored 2026-07-08 after 88bd1cb9 ("Remove autopilot
|
|
5015
|
-
// tool") silently dropped its only install path (it used to be bundled into
|
|
5016
|
-
// the deleted `autopilot enable` action). Best-effort; never blocks boot.
|
|
5017
|
-
// Disable with S4L_AUTO_UPDATE=0.
|
|
5018
|
-
void ensureUpdaterInstalled()
|
|
5019
|
-
.then((r) => console.error(`[self-update] launchd updater: ${r.ok ? "ok" : "skip"} (${r.detail})`))
|
|
5020
|
-
.catch((e) => console.error("[self-update] updater install failed:", e?.message || e));
|
|
5021
4713
|
// Heal installs onboarded before short_links_live defaulted to false: such a
|
|
5022
4714
|
// project wraps short links against the customer's own domain, which has no
|
|
5023
4715
|
// /r/[code] resolver, so every minted link 404s. Re-point them at the s4l.ai
|
package/mcp/dist/onboarding.js
CHANGED
|
@@ -10,11 +10,18 @@ import { createRequire } from "node:module";
|
|
|
10
10
|
import { repoDir, run } from "./repo.js";
|
|
11
11
|
import { resolvePython } from "./runtime.js";
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
|
+
export const ONBOARDING_MILESTONES = [
|
|
14
|
+
"environment_checked",
|
|
15
|
+
"runtime_ready",
|
|
16
|
+
"x_connected",
|
|
17
|
+
"profile_scanned",
|
|
18
|
+
"mode_chosen",
|
|
19
|
+
"project_ready",
|
|
20
|
+
"topics_seeded",
|
|
21
|
+
"tasks_scheduled",
|
|
22
|
+
];
|
|
13
23
|
const ledgerApi = require("../shared/onboarding-ledger.cjs");
|
|
14
24
|
const doctorApi = require("../shared/doctor.cjs");
|
|
15
|
-
// Re-export of the ONE runtime array (see the comment above the imports): this
|
|
16
|
-
// is not a second list, just a typed alias for ledgerApi.MILESTONES.
|
|
17
|
-
export const ONBOARDING_MILESTONES = ledgerApi.MILESTONES;
|
|
18
25
|
export function onboardingSnapshot() {
|
|
19
26
|
return ledgerApi.publicSnapshot();
|
|
20
27
|
}
|