@m13v/s4l 1.6.202 → 1.6.203-rc.10
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 +123 -65
- package/mcp/dist/panel.html +47 -28
- package/mcp/dist/product-link.html +1 -1
- package/mcp/dist/setup.js +2 -2
- package/mcp/dist/telemetry.js +1 -1
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/dashboard_server.py +72 -2
- package/mcp/menubar/s4l_card.py +248 -213
- package/mcp/menubar/s4l_menubar.py +30 -20
- package/mcp/menubar/s4l_state.py +23 -14
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/apply_onboarding_selections.py +5 -5
- package/scripts/build_persona.py +18 -12
- package/scripts/claude_job.py +1 -1
- package/scripts/dev_dashboard_server.py +18 -0
- package/scripts/engage_github.py +25 -0
- package/scripts/engage_reddit.py +14 -1
- package/scripts/feedback_digest.py +7 -5
- package/scripts/pick_search_topic.py +16 -11
- package/scripts/post_github.py +9 -0
- package/scripts/post_reddit.py +11 -0
- package/scripts/s4l_activity.py +126 -0
- package/scripts/s4l_mode.py +414 -0
- package/scripts/saps_activity.py +10 -118
- package/scripts/saps_mode.py +8 -318
- package/scripts/snapshot.py +1 -1
- package/skill/run-draft-and-publish.sh +39 -12
- package/skill/run-linkedin.sh +12 -0
- package/skill/run-twitter-cycle.sh +30 -19
package/mcp/dist/index.js
CHANGED
|
@@ -408,8 +408,8 @@ function toolActivityLabel(name, args) {
|
|
|
408
408
|
const fallback = TOOL_ACTIVITY[name];
|
|
409
409
|
if (!fallback)
|
|
410
410
|
return null;
|
|
411
|
-
const override = typeof args?.
|
|
412
|
-
? args.
|
|
411
|
+
const override = typeof args?.__s4l_activity_label === "string"
|
|
412
|
+
? args.__s4l_activity_label.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
413
413
|
: "";
|
|
414
414
|
return override || fallback;
|
|
415
415
|
}
|
|
@@ -1247,7 +1247,7 @@ async function seedSearchQueriesForProject(project, rawQueries) {
|
|
|
1247
1247
|
// ---- engagement_mode: choose personal-brand vs product (setup-time) --------
|
|
1248
1248
|
// Part of onboarding: AFTER X connect + profile_scan, BEFORE product config, the
|
|
1249
1249
|
// agent asks the user which mode they want and calls this. It persists the mode
|
|
1250
|
-
// (scripts/
|
|
1250
|
+
// (scripts/s4l_mode.py, the single source of truth the cycle reads) and
|
|
1251
1251
|
// provisions the persona project (grounded in the profile scan), then the agent
|
|
1252
1252
|
// continues to product setup — a product is always configured regardless of mode.
|
|
1253
1253
|
tool("engagement_mode", {
|
|
@@ -1301,7 +1301,7 @@ tool("engagement_mode", {
|
|
|
1301
1301
|
.optional()
|
|
1302
1302
|
.describe("The RAW voice-memo transcript from the onboarding dictation interview, VERBATIM (do NOT " +
|
|
1303
1303
|
"paraphrase or summarize). Persisted to the persona_corpus.txt sidecar (never config.json), " +
|
|
1304
|
-
"capped ~
|
|
1304
|
+
"capped ~100000 chars. This is the grounding pool the drafter quotes real specifics from " +
|
|
1305
1305
|
"(actual projects, numbers, opinions, phrasing), so keep it dense and first-hand."),
|
|
1306
1306
|
voice: z
|
|
1307
1307
|
.any()
|
|
@@ -1323,7 +1323,7 @@ tool("engagement_mode", {
|
|
|
1323
1323
|
}, async (args) => {
|
|
1324
1324
|
const action = args.action || "get";
|
|
1325
1325
|
const readFlags = async () => {
|
|
1326
|
-
const cur = await runPython("scripts/
|
|
1326
|
+
const cur = await runPython("scripts/s4l_mode.py", ["flags"], { timeoutMs: 15_000 });
|
|
1327
1327
|
try {
|
|
1328
1328
|
const f = JSON.parse((cur.stdout || "").trim());
|
|
1329
1329
|
return { personal_brand: !!f.personal_brand, promotion: !!f.promotion };
|
|
@@ -1339,11 +1339,11 @@ tool("engagement_mode", {
|
|
|
1339
1339
|
return jsonContent({ flags, mode, persona: persona ? persona.name : null });
|
|
1340
1340
|
}
|
|
1341
1341
|
// Lightweight flip of ONE lane (the dashboard/menu-bar quick toggle): just
|
|
1342
|
-
// rewrite mode.json via
|
|
1342
|
+
// rewrite mode.json via s4l_mode.py — NO persona provisioning. Mirrors the
|
|
1343
1343
|
// menu bar's pure-local _toggle_lane so flipping from either surface is cheap.
|
|
1344
1344
|
if (action === "toggle") {
|
|
1345
1345
|
const lane = args.lane === "promotion" ? "promotion" : "personal_brand";
|
|
1346
|
-
const res = await runPython("scripts/
|
|
1346
|
+
const res = await runPython("scripts/s4l_mode.py", ["toggle", lane], { timeoutMs: 15_000 });
|
|
1347
1347
|
if (res.code !== 0) {
|
|
1348
1348
|
const tail = (res.stderr || res.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
1349
1349
|
return textContent(`Could not switch lane: ${tail}`);
|
|
@@ -1373,12 +1373,18 @@ tool("engagement_mode", {
|
|
|
1373
1373
|
}
|
|
1374
1374
|
const mode = personalBrand ? "personal_brand" : "promotion";
|
|
1375
1375
|
recordOnboardingAttempt("mode_chosen", { personal_brand: personalBrand, promotion });
|
|
1376
|
-
const setRes = await runPython("scripts/
|
|
1376
|
+
const setRes = await runPython("scripts/s4l_mode.py", ["set-flags", personalBrand ? "1" : "0", promotion ? "1" : "0"], { timeoutMs: 15_000 });
|
|
1377
1377
|
if (setRes.code !== 0) {
|
|
1378
1378
|
const tail = (setRes.stderr || setRes.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
1379
1379
|
blockOnboardingMilestone("mode_chosen", "mode_set_failed", tail, { personal_brand: personalBrand, promotion });
|
|
1380
1380
|
return textContent(`Couldn't save the engagement lanes: ${tail}`);
|
|
1381
1381
|
}
|
|
1382
|
+
// NOTE (2026-07-06): the draft-only flag (mode.json {"draft_only": false} —
|
|
1383
|
+
// promotion cycles POST autonomously instead of drafting cards) is
|
|
1384
|
+
// DELIBERATELY NOT exposed on this tool or any user surface. It is an
|
|
1385
|
+
// operator-only switch, set via `scripts/s4l_mode.py draft-only on|off`.
|
|
1386
|
+
// run-draft-and-publish.sh reads it per cycle. Do not add a tool param,
|
|
1387
|
+
// menubar toggle, or onboarding step for it.
|
|
1382
1388
|
// Provision the persona (grounded from the scan when supplied) regardless of
|
|
1383
1389
|
// mode, so the toggle always has a real persona to flip to.
|
|
1384
1390
|
let personaName;
|
|
@@ -1794,70 +1800,52 @@ tool("project_config", {
|
|
|
1794
1800
|
}
|
|
1795
1801
|
// Status / discovery mode: no project name supplied, or explicitly asked.
|
|
1796
1802
|
if (args.status === true || !args.name) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
const configured = projects.some((p) => p.ready);
|
|
1809
|
-
if (rtReady)
|
|
1810
|
-
completeOnboardingMilestone("runtime_ready");
|
|
1811
|
-
if (x.connected) {
|
|
1812
|
-
completeOnboardingMilestone("x_connected", { state: x.state || "connected" });
|
|
1813
|
-
}
|
|
1814
|
-
if (configured) {
|
|
1815
|
-
completeOnboardingMilestone("project_ready", {
|
|
1816
|
-
missing_count: 0,
|
|
1817
|
-
});
|
|
1818
|
-
}
|
|
1819
|
-
// mode_chosen completes when the user explicitly picked a mode (mode.json
|
|
1820
|
-
// exists) OR this is a legacy install already past setup (a ready product),
|
|
1821
|
-
// so adding this step never regresses an already-onboarded box.
|
|
1822
|
-
if (modeChosen() || configured) {
|
|
1823
|
-
completeOnboardingMilestone("mode_chosen", {
|
|
1824
|
-
source: modeChosen() ? "chosen" : "backfilled_legacy",
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1803
|
+
// ONE compute path: buildSnapshot -> scripts/snapshot.py, the same source
|
|
1804
|
+
// the menu bar and browser dashboard read. This branch used to recompute
|
|
1805
|
+
// projects/X/version inline (listManagedProjectStatus + xStatus), which
|
|
1806
|
+
// excluded the persona project and carried no setup_complete — so the
|
|
1807
|
+
// panel's refresh() disagreed with the dashboard tool and the menu bar on
|
|
1808
|
+
// persona-only setups. Do not reintroduce a parallel compute here.
|
|
1809
|
+
const snap = await buildSnapshot();
|
|
1810
|
+
const projects = Array.isArray(snap.projects) ? snap.projects : [];
|
|
1811
|
+
const rtReady = !!snap.runtime_ready;
|
|
1812
|
+
const xConnected = !!snap.x_connected;
|
|
1813
|
+
const configured = (snap.projects_ready || 0) > 0;
|
|
1827
1814
|
return jsonContent({
|
|
1828
1815
|
configured,
|
|
1829
1816
|
projects,
|
|
1830
1817
|
runtime_ready: rtReady,
|
|
1831
|
-
x_connected:
|
|
1832
|
-
x_state:
|
|
1833
|
-
x_handle:
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1818
|
+
x_connected: xConnected,
|
|
1819
|
+
x_state: snap.x_state,
|
|
1820
|
+
x_handle: snap.x_handle ?? null,
|
|
1821
|
+
setup_complete: !!snap.setup_complete,
|
|
1822
|
+
mcp_version: snap.version,
|
|
1823
|
+
latest_version: snap.latest_version,
|
|
1824
|
+
update_available: !!snap.update_available,
|
|
1825
|
+
mode: snap.mode,
|
|
1826
|
+
flags: snap.flags,
|
|
1827
|
+
update_hint: snap.update_available
|
|
1828
|
+
? `A newer version (${snap.latest_version}) is available — you're on ${snap.version}. ` +
|
|
1841
1829
|
`Tell the user and offer to run the \`runtime\` tool with action:'update' ` +
|
|
1842
1830
|
`(or \`npx social-autoposter@latest update\`).`
|
|
1843
1831
|
: undefined,
|
|
1844
1832
|
required_fields: REQUIRED_FIELDS,
|
|
1845
1833
|
recommended_fields: RECOMMENDED_FIELDS,
|
|
1846
1834
|
config_path: configPath(),
|
|
1847
|
-
ready_for_verification:
|
|
1848
|
-
onboarding:
|
|
1835
|
+
ready_for_verification: !!snap.setup_complete,
|
|
1836
|
+
onboarding: snap.onboarding,
|
|
1849
1837
|
next_step: !rtReady
|
|
1850
1838
|
? "Runtime is not ready yet. It provisions automatically on boot — poll runtime action:'status' until ready (only call runtime action:'install' to retry if status shows the boot provision failed or stalled), then continue setup automatically."
|
|
1851
1839
|
: projects.length === 0
|
|
1852
1840
|
? "No projects yet. Discover the product from conversation context and the connected X profile; research its website, infer a conservative complete project, and call project_config. Ask only if no product can be identified." +
|
|
1853
|
-
(
|
|
1841
|
+
(xConnected ? "" : " X is not connected yet either — detect_x_sources, warn about keychain prompts, then run connect_x with confirm:true without a separate permission turn.")
|
|
1854
1842
|
: projects.every((p) => p.ready)
|
|
1855
|
-
? (
|
|
1843
|
+
? (xConnected
|
|
1856
1844
|
? "All configured projects are ready and X is connected. SCHEDULE THE AUTOPILOT: (1) call queue_setup and create each returned task with create_scheduled_task (prompt verbatim; 'already exists' is fine); (2) the autopilot then runs on its own (launchd kicker + queue worker). Call the `dashboard` tool to confirm the schedule is firing (schedule_state 'ok') — that is the terminal step; do NOT wait for or verify a draft card. Do NOT pause to ask the user to review drafts."
|
|
1857
1845
|
: "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
|
|
1858
1846
|
"x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
|
|
1859
1847
|
: "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." +
|
|
1860
|
-
(
|
|
1848
|
+
(xConnected ? "" : " X is also not connected yet; detect sources and run connect_x with confirm:true."),
|
|
1861
1849
|
});
|
|
1862
1850
|
}
|
|
1863
1851
|
// Apply mode (incremental): merge whatever fields were supplied onto the
|
|
@@ -2964,22 +2952,38 @@ function ensureCoworkMcpRegistered() {
|
|
|
2964
2952
|
console.error(`[cowork-mcp] ensureCoworkMcpRegistered error: ${e?.message || e}`);
|
|
2965
2953
|
}
|
|
2966
2954
|
}
|
|
2967
|
-
// ---- launchd kicker: run the REAL pipeline in
|
|
2955
|
+
// ---- launchd kicker: run the REAL pipeline in queue mode ---------------------
|
|
2968
2956
|
// Reinstates com.m13v.social-twitter-cycle as the customer-box kicker. It runs
|
|
2969
|
-
// run-twitter-cycle.sh straight through (scan -> score -> draft -> link-gen)
|
|
2970
|
-
//
|
|
2971
|
-
//
|
|
2972
|
-
//
|
|
2973
|
-
//
|
|
2974
|
-
//
|
|
2975
|
-
|
|
2957
|
+
// run-twitter-cycle.sh straight through (scan -> score -> draft -> link-gen); its
|
|
2958
|
+
// `claude -p` steps route through the job queue (S4L_CLAUDE_PROVIDER=queue) for
|
|
2959
|
+
// the scheduled-task workers to service. The per-cycle DRAFT_ONLY value is NOT
|
|
2960
|
+
// baked here anymore: run-draft-and-publish.sh decides it from mode.json
|
|
2961
|
+
// (draft-only flag, 2026-07-06) — while draft-only is ON (default) cycles stop
|
|
2962
|
+
// before posting and merge into review cards; with draft-only OFF (operator
|
|
2963
|
+
// opt-out) promotion cycles post autonomously behind the virality bar. The DRAFT_ONLY=1 below is only the safe baseline an OLD wrapper (which
|
|
2964
|
+
// never overrides it) inherits.
|
|
2965
|
+
// 60s tick (2026-07-06): parity with the retired claude -p twitter-cycle plist.
|
|
2966
|
+
// The preflight slot gate (max-1) makes this safe — a tick that fires while a
|
|
2967
|
+
// cycle is still running skips in milliseconds — so the effective cadence is
|
|
2968
|
+
// back-to-back ~10-min cycles with ≤1 min idle, not one cycle per minute.
|
|
2969
|
+
const QUEUE_KICKER_INTERVAL_SECS = 60;
|
|
2976
2970
|
function kickerEnv() {
|
|
2977
2971
|
return {
|
|
2978
2972
|
DRAFT_ONLY: "1",
|
|
2979
2973
|
S4L_CLAUDE_PROVIDER: "queue",
|
|
2980
2974
|
S4L_STATE_DIR: sapsStateDir(),
|
|
2981
|
-
TWITTER_TAIL_LINK_RATE: "0",
|
|
2982
2975
|
TWITTER_PAGE_GEN_RATE: "0",
|
|
2976
|
+
// Thread-media context for the drafter (2026-07-06): parity with the
|
|
2977
|
+
// retired CLI twitter-cycle plist. Without it the prep step drafts blind to
|
|
2978
|
+
// images in the candidate threads.
|
|
2979
|
+
S4L_TWITTER_CAPTURE_MEDIA: "1",
|
|
2980
|
+
// NOTE: TWITTER_TAIL_LINK_RATE is deliberately NOT set here (2026-07-06).
|
|
2981
|
+
// The persona lane exports =0 per cycle via s4l_mode.py env (link-free
|
|
2982
|
+
// organic replies); promotion cycles keep the script's own default so
|
|
2983
|
+
// draft-only-OFF posts carry the project link per the A/B gate, and card posts
|
|
2984
|
+
// still force =1.0 inside post_drafts.
|
|
2985
|
+
// Virality bar percentile is NOT set here: it is hardcoded to 0.90 in
|
|
2986
|
+
// skill/run-twitter-cycle.sh (single source of truth, no env dependency).
|
|
2983
2987
|
};
|
|
2984
2988
|
}
|
|
2985
2989
|
async function ensureQueueKickerInstalled() {
|
|
@@ -3428,6 +3432,29 @@ async function buildSnapshot() {
|
|
|
3428
3432
|
completeOnboardingMilestone("project_ready", { missing_count: 0 });
|
|
3429
3433
|
if (snap.schedule_state === "ok")
|
|
3430
3434
|
completeOnboardingMilestone("tasks_scheduled");
|
|
3435
|
+
// mode_chosen completes when the user explicitly picked a mode (mode.json
|
|
3436
|
+
// exists) OR this is a legacy install already past setup (a ready project),
|
|
3437
|
+
// so adding this step never regresses an already-onboarded box. (Moved here
|
|
3438
|
+
// from project_config's status branch so ALL milestone truth lives in one
|
|
3439
|
+
// place next to the snapshot.)
|
|
3440
|
+
if (modeChosen() || (snap.projects_ready || 0) > 0) {
|
|
3441
|
+
completeOnboardingMilestone("mode_chosen", {
|
|
3442
|
+
source: modeChosen() ? "chosen" : "backfilled_legacy",
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
3445
|
+
// profile_scanned has no file signal of its own, but a provisioned persona
|
|
3446
|
+
// project can only come from the scan+dictation flow — treat its presence as
|
|
3447
|
+
// the completed scan so old installs can't stick at "profile pending".
|
|
3448
|
+
if ((Array.isArray(snap.projects) ? snap.projects : []).some((p) => p?.persona)) {
|
|
3449
|
+
completeOnboardingMilestone("profile_scanned", { backfill: true });
|
|
3450
|
+
}
|
|
3451
|
+
// topics_seeded has no cheap live signal (it's a DB fact, not a file fact), so
|
|
3452
|
+
// installs that finished setup before the persona-path completion landed stay
|
|
3453
|
+
// stuck at 7/8 forever. Heal by re-running the idempotent seed for the ready
|
|
3454
|
+
// project(s) and completing the milestone on success. Fire-and-forget: the
|
|
3455
|
+
// ledger flip shows on the next snapshot.
|
|
3456
|
+
if ((snap.projects_ready || 0) > 0)
|
|
3457
|
+
void healTopicsSeeded(snap);
|
|
3431
3458
|
// Persist this snapshot so the menu bar can answer "set up?" the SAME way when
|
|
3432
3459
|
// the loopback server is unreachable (Claude Desktop closed or mid-restart)
|
|
3433
3460
|
// instead of falling back to a divergent local rule. Refreshed on every
|
|
@@ -3436,6 +3463,37 @@ async function buildSnapshot() {
|
|
|
3436
3463
|
persistStatusSummary(snap);
|
|
3437
3464
|
return snap;
|
|
3438
3465
|
}
|
|
3466
|
+
// One attempt per process: a failed seed (offline DB) retries on the next MCP
|
|
3467
|
+
// launch, not on every dashboard poll.
|
|
3468
|
+
let topicsSeedHealAttempted = false;
|
|
3469
|
+
async function healTopicsSeeded(snap) {
|
|
3470
|
+
if (topicsSeedHealAttempted)
|
|
3471
|
+
return;
|
|
3472
|
+
topicsSeedHealAttempted = true;
|
|
3473
|
+
try {
|
|
3474
|
+
const ledger = onboardingLedger();
|
|
3475
|
+
if (ledger?.milestones?.topics_seeded?.status === "complete")
|
|
3476
|
+
return;
|
|
3477
|
+
const ready = (Array.isArray(snap.projects) ? snap.projects : []).filter((p) => p && p.ready && typeof p.name === "string");
|
|
3478
|
+
for (const p of ready) {
|
|
3479
|
+
const seed = await runPython("scripts/seed_search_topics.py", ["--project", p.name], {
|
|
3480
|
+
timeoutMs: 60_000,
|
|
3481
|
+
});
|
|
3482
|
+
if (seed.code === 0) {
|
|
3483
|
+
const m = /planned=(\d+)\s+inserted=(\d+)\s+updated=(\d+)/.exec(seed.stdout);
|
|
3484
|
+
completeOnboardingMilestone("topics_seeded", {
|
|
3485
|
+
project: p.name,
|
|
3486
|
+
topic_count: m ? Number(m[1]) : 0,
|
|
3487
|
+
backfill: true,
|
|
3488
|
+
});
|
|
3489
|
+
return;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
catch (e) {
|
|
3494
|
+
console.error("[snapshot] topics_seeded heal failed:", e?.message || e);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3439
3497
|
// ---- dashboard localhost fallback -----------------------------------------
|
|
3440
3498
|
// When the connected host doesn't support MCP Apps UI (Claude Code / Cowork
|
|
3441
3499
|
// today), serve the SAME dist/panel.html from a loopback HTTP server. The page
|
|
@@ -3580,7 +3638,7 @@ function sapsStateDir() {
|
|
|
3580
3638
|
}
|
|
3581
3639
|
// Has the user explicitly chosen an engagement mode? mode.json is written by the
|
|
3582
3640
|
// engagement_mode tool (setup) and the menu-bar toggle. Used to complete the
|
|
3583
|
-
// mode_chosen onboarding milestone. (Source of truth: scripts/
|
|
3641
|
+
// mode_chosen onboarding milestone. (Source of truth: scripts/s4l_mode.py.)
|
|
3584
3642
|
function modeChosen() {
|
|
3585
3643
|
try {
|
|
3586
3644
|
return fs.existsSync(path.join(sapsStateDir(), "mode.json"));
|
|
@@ -3590,8 +3648,8 @@ function modeChosen() {
|
|
|
3590
3648
|
}
|
|
3591
3649
|
}
|
|
3592
3650
|
// The current engagement lane flags, surfaced in the snapshot so the dashboard
|
|
3593
|
-
// AND menu bar read them from ONE place (mode.json, the same file
|
|
3594
|
-
// writes). Mirrors
|
|
3651
|
+
// AND menu bar read them from ONE place (mode.json, the same file s4l_mode.py
|
|
3652
|
+
// writes). Mirrors s4l_mode.py get_flags(): explicit flag keys win; else map a
|
|
3595
3653
|
// legacy {"mode": ...} string; else default personal ON / promotion OFF.
|
|
3596
3654
|
function currentFlags() {
|
|
3597
3655
|
try {
|