social-autoposter 1.6.166 → 1.6.168
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 +121 -58
- package/mcp/dist/panel.html +9 -9
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +3 -3
- package/mcp/menubar/dashboard_server.py +14 -7
- package/mcp/menubar/s4l_menubar.py +43 -26
- package/mcp/menubar/s4l_state.py +60 -18
- package/mcp/package.json +1 -1
- package/package.json +1 -1
- package/scripts/saps_mode.py +194 -49
- package/scripts/snapshot.py +20 -2
package/mcp/dist/index.js
CHANGED
|
@@ -271,9 +271,10 @@ const server = new McpServer({
|
|
|
271
271
|
"user and offer to run `runtime` with action:'update'. The `project_config` tool's status also " +
|
|
272
272
|
"surfaces `update_available` and an `update_hint`.\n\n" +
|
|
273
273
|
"TYPICAL FLOW: `project_config` (connect X + scan the profile) -> `engagement_mode` (after the " +
|
|
274
|
-
"profile scan
|
|
275
|
-
"
|
|
276
|
-
"
|
|
274
|
+
"profile scan: personal-brand is ON by default, so ASK the user the ONE question — do they ALSO " +
|
|
275
|
+
"want to promote a product? — and call action:'set' with personal_brand:true and " +
|
|
276
|
+
"promotion:true|false; this provisions the persona) -> IF they wanted promotion, `project_config` " +
|
|
277
|
+
"(configure the product project) -> `queue_setup` + " +
|
|
277
278
|
"`create_scheduled_task` (set up the draft autopilot once) -> the autopilot then runs on its own " +
|
|
278
279
|
"(scans, drafts via the queue + worker, and merges into the approval cards; nothing posts) -> the " +
|
|
279
280
|
"user approves in the menu bar -> `post_drafts` (post the approved ones) -> `get_stats` (see " +
|
|
@@ -1042,26 +1043,39 @@ const WEBSITE_RESEARCH_INSTRUCTIONS = "PRODUCT RESEARCH (do this before saving t
|
|
|
1042
1043
|
// provisions the persona project (grounded in the profile scan), then the agent
|
|
1043
1044
|
// continues to product setup — a product is always configured regardless of mode.
|
|
1044
1045
|
tool("engagement_mode", {
|
|
1045
|
-
title: "Choose engagement
|
|
1046
|
-
description: "Set or read the engagement
|
|
1047
|
-
"
|
|
1048
|
-
"
|
|
1049
|
-
"
|
|
1050
|
-
"
|
|
1051
|
-
"
|
|
1052
|
-
"
|
|
1053
|
-
"
|
|
1054
|
-
"
|
|
1046
|
+
title: "Choose engagement lanes (personal brand + optional product promotion)",
|
|
1047
|
+
description: "Set or read the engagement LANES the autopilot drafts in. There are TWO independent lanes that " +
|
|
1048
|
+
"can BOTH be on (the cycle then splits 50/50): PERSONAL BRAND (organic, link-free engagement in " +
|
|
1049
|
+
"the user's own voice — ON by default) and PRODUCT PROMOTION (the marketing pipeline, link " +
|
|
1050
|
+
"replies — OFF by default, opt-in). This is a SETUP step: AFTER X is connected and the profile " +
|
|
1051
|
+
"is scanned, personal-brand is already the default, so ASK the user the ONE question: do they " +
|
|
1052
|
+
"ALSO want to promote a product? Then call action:'set' with personal_brand:true and " +
|
|
1053
|
+
"promotion:true|false. Pass the voice/description/topics you extracted from the profile scan so " +
|
|
1054
|
+
"the persona is grounded in who they actually are. If they want promotion too, continue to " +
|
|
1055
|
+
"configure the product project with project_config afterward. The user flips either lane any " +
|
|
1056
|
+
"time from the menu-bar checkmarks.",
|
|
1055
1057
|
inputSchema: {
|
|
1056
1058
|
action: z
|
|
1057
1059
|
.enum(["get", "set", "toggle"])
|
|
1058
1060
|
.optional()
|
|
1059
|
-
.describe("get = read current
|
|
1061
|
+
.describe("get = read current lane flags + persona status. set = record the user's chosen lanes (provisions the persona). toggle = lightweight flip of ONE lane (pass `lane`); mode.json only, no persona work — the dashboard/menu-bar quick toggle."),
|
|
1062
|
+
personal_brand: z
|
|
1063
|
+
.boolean()
|
|
1064
|
+
.optional()
|
|
1065
|
+
.describe("action:'set' — turn the personal-brand lane on/off. Defaults to true (the out-of-the-box lane)."),
|
|
1066
|
+
promotion: z
|
|
1067
|
+
.boolean()
|
|
1068
|
+
.optional()
|
|
1069
|
+
.describe("action:'set' — turn the product-promotion lane on/off. Defaults to false; set true when the user says they also want to promote a product."),
|
|
1070
|
+
lane: z
|
|
1071
|
+
.enum(["personal_brand", "promotion"])
|
|
1072
|
+
.optional()
|
|
1073
|
+
.describe("action:'toggle' — which single lane to flip."),
|
|
1060
1074
|
mode: z
|
|
1061
1075
|
.enum(["personal_brand", "promotion"])
|
|
1062
1076
|
.optional()
|
|
1063
|
-
.describe("
|
|
1064
|
-
"
|
|
1077
|
+
.describe("LEGACY (compat). Single-lane shorthand for action:'set': turns the named lane ON and the " +
|
|
1078
|
+
"other OFF. Prefer the explicit personal_brand/promotion booleans."),
|
|
1065
1079
|
description: z
|
|
1066
1080
|
.string()
|
|
1067
1081
|
.optional()
|
|
@@ -1081,37 +1095,62 @@ tool("engagement_mode", {
|
|
|
1081
1095
|
},
|
|
1082
1096
|
}, async (args) => {
|
|
1083
1097
|
const action = args.action || "get";
|
|
1098
|
+
const readFlags = async () => {
|
|
1099
|
+
const cur = await runPython("scripts/saps_mode.py", ["flags"], { timeoutMs: 15_000 });
|
|
1100
|
+
try {
|
|
1101
|
+
const f = JSON.parse((cur.stdout || "").trim());
|
|
1102
|
+
return { personal_brand: !!f.personal_brand, promotion: !!f.promotion };
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return { personal_brand: true, promotion: false };
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1084
1108
|
if (action === "get") {
|
|
1085
|
-
const
|
|
1086
|
-
const mode = (cur.stdout || "").trim() || "promotion";
|
|
1109
|
+
const flags = await readFlags();
|
|
1087
1110
|
const persona = findPersonaProject();
|
|
1088
|
-
|
|
1111
|
+
const mode = flags.personal_brand ? "personal_brand" : "promotion";
|
|
1112
|
+
return jsonContent({ flags, mode, persona: persona ? persona.name : null });
|
|
1089
1113
|
}
|
|
1090
|
-
// Lightweight flip (the dashboard/menu-bar quick toggle): just
|
|
1091
|
-
// mode.json via saps_mode.py — NO persona provisioning. Mirrors the
|
|
1092
|
-
// bar's pure-local
|
|
1114
|
+
// Lightweight flip of ONE lane (the dashboard/menu-bar quick toggle): just
|
|
1115
|
+
// rewrite mode.json via saps_mode.py — NO persona provisioning. Mirrors the
|
|
1116
|
+
// menu bar's pure-local _toggle_lane so flipping from either surface is cheap.
|
|
1093
1117
|
if (action === "toggle") {
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
const next = now === "personal_brand" ? "promotion" : "personal_brand";
|
|
1097
|
-
const res = await runPython("scripts/saps_mode.py", ["set", next], { timeoutMs: 15_000 });
|
|
1118
|
+
const lane = args.lane === "promotion" ? "promotion" : "personal_brand";
|
|
1119
|
+
const res = await runPython("scripts/saps_mode.py", ["toggle", lane], { timeoutMs: 15_000 });
|
|
1098
1120
|
if (res.code !== 0) {
|
|
1099
1121
|
const tail = (res.stderr || res.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
1100
|
-
return textContent(`Could not switch
|
|
1122
|
+
return textContent(`Could not switch lane: ${tail}`);
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
return jsonContent({ flags: JSON.parse((res.stdout || "").trim()) });
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
return jsonContent({ flags: await readFlags() });
|
|
1101
1129
|
}
|
|
1102
|
-
return jsonContent({ mode: next });
|
|
1103
1130
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1131
|
+
// action === 'set'. Resolve the two lane flags. Explicit booleans win; the
|
|
1132
|
+
// legacy `mode` shorthand maps to single-lane; default is personal ON.
|
|
1133
|
+
let personalBrand;
|
|
1134
|
+
let promotion;
|
|
1135
|
+
if (args.mode === "personal_brand" || args.mode === "promotion") {
|
|
1136
|
+
personalBrand = args.mode === "personal_brand";
|
|
1137
|
+
promotion = args.mode === "promotion";
|
|
1108
1138
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1139
|
+
else {
|
|
1140
|
+
personalBrand = args.personal_brand === undefined ? true : !!args.personal_brand;
|
|
1141
|
+
promotion = !!args.promotion;
|
|
1142
|
+
}
|
|
1143
|
+
if (!personalBrand && !promotion) {
|
|
1144
|
+
return textContent("At least one lane must be on. personal_brand is the default; set promotion:true if the user " +
|
|
1145
|
+
"also wants product promotion (both on -> the cycle splits 50/50).");
|
|
1146
|
+
}
|
|
1147
|
+
const mode = personalBrand ? "personal_brand" : "promotion";
|
|
1148
|
+
recordOnboardingAttempt("mode_chosen", { personal_brand: personalBrand, promotion });
|
|
1149
|
+
const setRes = await runPython("scripts/saps_mode.py", ["set-flags", personalBrand ? "1" : "0", promotion ? "1" : "0"], { timeoutMs: 15_000 });
|
|
1111
1150
|
if (setRes.code !== 0) {
|
|
1112
1151
|
const tail = (setRes.stderr || setRes.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
|
|
1113
|
-
blockOnboardingMilestone("mode_chosen", "mode_set_failed", tail, {
|
|
1114
|
-
return textContent(`Couldn't save the engagement
|
|
1152
|
+
blockOnboardingMilestone("mode_chosen", "mode_set_failed", tail, { personal_brand: personalBrand, promotion });
|
|
1153
|
+
return textContent(`Couldn't save the engagement lanes: ${tail}`);
|
|
1115
1154
|
}
|
|
1116
1155
|
// Provision the persona (grounded from the scan when supplied) regardless of
|
|
1117
1156
|
// mode, so the toggle always has a real persona to flip to.
|
|
@@ -1144,21 +1183,28 @@ tool("engagement_mode", {
|
|
|
1144
1183
|
personaTopicCount = m ? Number(m[1]) : 0;
|
|
1145
1184
|
personaTopicsSeeded = true;
|
|
1146
1185
|
}
|
|
1147
|
-
completeOnboardingMilestone("mode_chosen", {
|
|
1186
|
+
completeOnboardingMilestone("mode_chosen", { personal_brand: personalBrand, promotion, persona: personaName });
|
|
1187
|
+
const bothOn = personalBrand && promotion;
|
|
1188
|
+
const next_step = promotion
|
|
1189
|
+
? (bothOn
|
|
1190
|
+
? "Personal brand + product promotion are BOTH on (the cycle splits 50/50), and the persona " +
|
|
1191
|
+
"is provisioned + topic-seeded. "
|
|
1192
|
+
: "Product promotion is on and the persona is provisioned. ") +
|
|
1193
|
+
"NOW CONTINUE SETUP: configure the product project with project_config (research the product " +
|
|
1194
|
+
"site and fill description, icp, voice, search_topics)."
|
|
1195
|
+
: "Personal-brand lane is on (the default) and the persona is provisioned + topic-seeded. The " +
|
|
1196
|
+
"user did not ask for product promotion, so no product project is needed; they can turn on " +
|
|
1197
|
+
"the promotion lane any time from the menu-bar checkmark. Setup can finish here.";
|
|
1148
1198
|
return jsonContent({
|
|
1149
1199
|
ok: true,
|
|
1200
|
+
flags: { personal_brand: personalBrand, promotion },
|
|
1150
1201
|
mode,
|
|
1151
1202
|
persona: personaName,
|
|
1152
1203
|
persona_created: personaCreated,
|
|
1153
1204
|
persona_topics_seeded: personaTopicsSeeded,
|
|
1154
1205
|
persona_topic_count: personaTopicCount,
|
|
1155
1206
|
onboarding: onboardingSnapshot(),
|
|
1156
|
-
next_step
|
|
1157
|
-
? "Personal-brand mode is set and the persona is provisioned + topic-seeded. "
|
|
1158
|
-
: "Promotion mode is set (the default), and the persona is provisioned so the user can flip " +
|
|
1159
|
-
"to personal-brand from the menu-bar toggle any time. ") +
|
|
1160
|
-
"NOW CONTINUE SETUP: configure the product project with project_config (research the product " +
|
|
1161
|
-
"site and fill description, icp, voice, search_topics) — a product is set up regardless of mode.",
|
|
1207
|
+
next_step,
|
|
1162
1208
|
});
|
|
1163
1209
|
});
|
|
1164
1210
|
tool("project_config", {
|
|
@@ -1411,17 +1457,19 @@ tool("project_config", {
|
|
|
1411
1457
|
"as GROUND TRUTH and, per grounding_instructions, extract their profession/identity, " +
|
|
1412
1458
|
"voice & vibe (tone, phrasing, casing, tics), 2-4 verbatim golden-rule example replies, " +
|
|
1413
1459
|
"a phrase bank + things they avoid, their icp, and recurring themes -> search_topics. " +
|
|
1414
|
-
"SECOND (engagement
|
|
1415
|
-
"
|
|
1416
|
-
"
|
|
1417
|
-
"
|
|
1418
|
-
"
|
|
1419
|
-
"
|
|
1460
|
+
"SECOND (engagement lanes — ASK THE USER, do not infer): the PERSONAL BRAND lane (organic, " +
|
|
1461
|
+
"link-free engagement in their own voice) is ON by default, so ask the ONE question — do they " +
|
|
1462
|
+
"ALSO want to PROMOTE a PRODUCT (the marketing lane, link replies)? Both lanes can run (the " +
|
|
1463
|
+
"cycle splits 50/50). Call the `engagement_mode` tool action:'set' with personal_brand:true, " +
|
|
1464
|
+
"promotion:true|false AND the voice/description/topics you just extracted (this provisions a " +
|
|
1465
|
+
"persona grounded in this scan). " +
|
|
1466
|
+
"THIRD (product, ONLY if they wanted promotion): follow " +
|
|
1420
1467
|
"website_research_instructions — discover the product URL from config, context, profile " +
|
|
1421
1468
|
"links/posts, or public research and read 5+ of its pages to fill description, " +
|
|
1422
1469
|
"differentiator, icp, get_started_link, and content_guardrails, written in the voice you " +
|
|
1423
1470
|
"just captured. Save the best conservative supported fields without a confirmation " +
|
|
1424
|
-
"round-trip. Ask only if no product can be identified or a required field is unknowable."
|
|
1471
|
+
"round-trip. Ask only if no product can be identified or a required field is unknowable. If " +
|
|
1472
|
+
"they only want personal brand, SKIP the product step.",
|
|
1425
1473
|
});
|
|
1426
1474
|
}
|
|
1427
1475
|
// Status / discovery mode: no project name supplied, or explicitly asked.
|
|
@@ -1467,6 +1515,7 @@ tool("project_config", {
|
|
|
1467
1515
|
latest_version: ver.latest,
|
|
1468
1516
|
update_available: ver.update_available,
|
|
1469
1517
|
mode: currentMode(),
|
|
1518
|
+
flags: currentFlags(),
|
|
1470
1519
|
update_hint: ver.update_available
|
|
1471
1520
|
? `A newer version (${ver.latest}) is available — you're on ${ver.installed}. ` +
|
|
1472
1521
|
`Tell the user and offer to run the \`runtime\` tool with action:'update' ` +
|
|
@@ -2819,7 +2868,7 @@ async function buildSnapshot() {
|
|
|
2819
2868
|
auto_update_on: false, version: VERSION, latest_version: null,
|
|
2820
2869
|
update_available: false, runtime_ready: runtimeReady(),
|
|
2821
2870
|
runtime_provisioning: isProvisioning(), setup_complete: false,
|
|
2822
|
-
mode:
|
|
2871
|
+
mode: currentMode(), flags: currentFlags(), onboarding: onboardingSnapshot(),
|
|
2823
2872
|
};
|
|
2824
2873
|
}
|
|
2825
2874
|
// MCP-only side effects (snapshot.py is a pure reader and does none of these):
|
|
@@ -2994,17 +3043,31 @@ function modeChosen() {
|
|
|
2994
3043
|
return false;
|
|
2995
3044
|
}
|
|
2996
3045
|
}
|
|
2997
|
-
// The current engagement
|
|
2998
|
-
//
|
|
2999
|
-
//
|
|
3000
|
-
|
|
3046
|
+
// The current engagement lane flags, surfaced in the snapshot so the dashboard
|
|
3047
|
+
// AND menu bar read them from ONE place (mode.json, the same file saps_mode.py
|
|
3048
|
+
// writes). Mirrors saps_mode.py get_flags(): explicit flag keys win; else map a
|
|
3049
|
+
// legacy {"mode": ...} string; else default personal ON / promotion OFF.
|
|
3050
|
+
function currentFlags() {
|
|
3001
3051
|
try {
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3052
|
+
const d = JSON.parse(fs.readFileSync(path.join(sapsStateDir(), "mode.json"), "utf-8"));
|
|
3053
|
+
if ("personal_brand" in d || "promotion" in d) {
|
|
3054
|
+
return { personal_brand: !!d.personal_brand, promotion: !!d.promotion };
|
|
3055
|
+
}
|
|
3056
|
+
const m = (d.mode || "").trim();
|
|
3057
|
+
if (m === "personal_brand")
|
|
3058
|
+
return { personal_brand: true, promotion: false };
|
|
3059
|
+
if (m === "promotion")
|
|
3060
|
+
return { personal_brand: false, promotion: true };
|
|
3004
3061
|
}
|
|
3005
3062
|
catch {
|
|
3006
|
-
|
|
3063
|
+
/* fall through to default */
|
|
3007
3064
|
}
|
|
3065
|
+
return { personal_brand: true, promotion: false };
|
|
3066
|
+
}
|
|
3067
|
+
// Derived legacy single-mode string (personal wins when on). Defaults to
|
|
3068
|
+
// personal_brand when unset (2026-06-29 default flip).
|
|
3069
|
+
function currentMode() {
|
|
3070
|
+
return currentFlags().personal_brand ? "personal_brand" : "promotion";
|
|
3008
3071
|
}
|
|
3009
3072
|
// ---- Cross-instance "posting active" flag ----------------------------------
|
|
3010
3073
|
// posting-active.json in the shared state dir is the CROSS-MCP-INSTANCE version
|