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 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, ASK the user: grow their personal brand or promote a product, and set it this " +
275
- "provisions the persona) -> `project_config` (configure the product project; always, regardless " +
276
- "of mode) -> `queue_setup` + " +
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 mode (personal brand vs product)",
1046
- description: "Set or read the engagement MODE the autopilot drafts in. This is a SETUP step: AFTER X is " +
1047
- "connected and the profile is scanned, and BEFORE configuring the product, ASK the user which " +
1048
- "they want (a) grow their PERSONAL BRAND (organic, link-free engagement in their own voice) or " +
1049
- "(b) PROMOTE a PRODUCT (the default marketing pipeline) then call action:'set' with their " +
1050
- "choice. Pass the voice/description/topics you extracted from the profile scan so the persona is " +
1051
- "grounded in who they actually are (do this even for promotion, so the menu-bar toggle can flip " +
1052
- "to a good persona later). EITHER way, continue to configure the product project as usual " +
1053
- "afterward; a product is always set up. The user flips this mode any time from the menu-bar " +
1054
- "toggle.",
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 mode + persona status. set = record the user's chosen mode (provisions the persona). toggle = lightweight flip promotion<->personal_brand (mode.json only, no persona work) — the dashboard/menu-bar quick toggle."),
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("Required for action:'set'. personal_brand = organic brand growth (link-free, the user's " +
1064
- "own voice); promotion = market the configured product (default)."),
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 cur = await runPython("scripts/saps_mode.py", ["get"], { timeoutMs: 15_000 });
1086
- const mode = (cur.stdout || "").trim() || "promotion";
1109
+ const flags = await readFlags();
1087
1110
  const persona = findPersonaProject();
1088
- return jsonContent({ mode, persona: persona ? persona.name : null });
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 rewrite
1091
- // mode.json via saps_mode.py — NO persona provisioning. Mirrors the menu
1092
- // bar's pure-local _toggle_mode so flipping from either surface is cheap.
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 cur = await runPython("scripts/saps_mode.py", ["get"], { timeoutMs: 15_000 });
1095
- const now = (cur.stdout || "").trim() || "promotion";
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 mode: ${tail}`);
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
- const mode = args.mode;
1105
- if (mode !== "personal_brand" && mode !== "promotion") {
1106
- return textContent("Ask the user which they want, then call again with mode:'personal_brand' (organic brand " +
1107
- "growth, link-free) or mode:'promotion' (market the product, the default).");
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
- recordOnboardingAttempt("mode_chosen", { mode });
1110
- const setRes = await runPython("scripts/saps_mode.py", ["set", mode], { timeoutMs: 15_000 });
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, { mode });
1114
- return textContent(`Couldn't save the engagement mode: ${tail}`);
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", { mode, persona: personaName });
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: (mode === "personal_brand"
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 mode — ASK THE USER, do not infer): ask whether they want to grow their " +
1415
- "PERSONAL BRAND (organic, link-free engagement in their own voice) or PROMOTE a PRODUCT (the " +
1416
- "default marketing pipeline), then call the `engagement_mode` tool action:'set' with their " +
1417
- "choice AND the voice/description/topics you just extracted (this provisions a persona " +
1418
- "grounded in this scan; pass the grounding even for promotion). " +
1419
- "THIRD (product, from their website — always, regardless of mode): follow " +
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: "promotion", onboarding: onboardingSnapshot(),
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 mode ('promotion' | 'personal_brand'), surfaced in the
2998
- // snapshot so the dashboard AND menu bar read it from ONE place (mode.json, the
2999
- // same file saps_mode.py writes). Defaults to promotion when unset.
3000
- function currentMode() {
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 m = (JSON.parse(fs.readFileSync(path.join(sapsStateDir(), "mode.json"), "utf-8")).mode || "").trim();
3003
- return m === "personal_brand" ? "personal_brand" : "promotion";
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
- return "promotion";
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