social-autoposter 1.6.53 → 1.6.56

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 CHANGED
@@ -1205,6 +1205,24 @@ function doctor() {
1205
1205
  }
1206
1206
  }
1207
1207
 
1208
+ // Provision the owned Python/Chromium runtime from the terminal. This is the
1209
+ // panel-free path: it runs the EXACT same provisioning logic the panel's
1210
+ // "Install runtime" button and the install_runtime MCP tool use (mcp/src/
1211
+ // runtime.ts -> dist/runtime.js), via the thin ESM wrapper mcp/install-runtime.mjs.
1212
+ // Use it when the UI panel can't render (Claude Code/Cowork), on a bare VM, or
1213
+ // when an agent wants to install head-less. Idempotent: re-running repairs.
1214
+ function installRuntime() {
1215
+ const wrapper = path.join(__dirname, '..', 'mcp', 'install-runtime.mjs');
1216
+ if (!fs.existsSync(wrapper)) {
1217
+ console.error(`Cannot find ${wrapper}. Re-run \`npx social-autoposter update\` to repair the install.`);
1218
+ process.exit(1);
1219
+ }
1220
+ // process.execPath is the Node already running this CLI, so we reuse it
1221
+ // rather than hunting for a node on PATH.
1222
+ const res = spawnSync(process.execPath, [wrapper], { stdio: 'inherit' });
1223
+ process.exit(res.status == null ? 1 : res.status);
1224
+ }
1225
+
1208
1226
  const cmd = process.argv[2];
1209
1227
  if (cmd === 'init') {
1210
1228
  init();
@@ -1214,6 +1232,8 @@ if (cmd === 'init') {
1214
1232
  doctor();
1215
1233
  } else if (cmd === 'bootstrap-vm') {
1216
1234
  bootstrapVm();
1235
+ } else if (cmd === 'install-runtime') {
1236
+ installRuntime();
1217
1237
  } else if (cmd === 'export-cookies') {
1218
1238
  // Forward to cookie-helper with 'export' + remaining args
1219
1239
  process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
@@ -1243,6 +1263,7 @@ if (cmd === 'init') {
1243
1263
  console.log(' npx social-autoposter update update scripts, preserve config');
1244
1264
  console.log(' npx social-autoposter doctor probe install health (#6, 1.6.34+)');
1245
1265
  console.log(' npx social-autoposter bootstrap-vm AppMaker VM self-bootstrap (DB-driven)');
1266
+ console.log(' npx social-autoposter install-runtime provision owned Python + Chromium (panel-free)');
1246
1267
  console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
1247
1268
  console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
1248
1269
  }
package/mcp/dist/index.js CHANGED
@@ -16,9 +16,10 @@ import { z } from "zod";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
18
  import fs from "node:fs";
19
- import { REPO_DIR, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
20
- import { applySetup, resolveProject, hasReadyProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, CONFIG_PATH, } from "./setup.js";
19
+ import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
20
+ import { applySetup, resolveProject, hasReadyProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, } from "./setup.js";
21
21
  import { xStatus, xConnect, summarizeXAuth } from "./twitterAuth.js";
22
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, } from "./runtime.js";
22
23
  import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
23
24
  import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
24
25
  import { fileURLToPath } from "node:url";
@@ -69,7 +70,9 @@ ${args}
69
70
  \t\t<key>HOME</key>
70
71
  \t\t<string>${os.homedir()}</string>
71
72
  \t\t<key>SAPS_REPO_DIR</key>
72
- \t\t<string>${REPO_DIR}</string>
73
+ \t\t<string>${repoDir()}</string>
74
+ \t\t<key>SAPS_PYTHON</key>
75
+ \t\t<string>${resolvePython()}</string>
73
76
  \t</dict>
74
77
  \t<key>RunAtLoad</key>
75
78
  \t<${opts.runAtLoad ? "true" : "false"}/>
@@ -225,7 +228,7 @@ async function produceDrafts(project, onProgress) {
225
228
  // It lives right next to the cycle's own twitter-cycle-*.log. We append the
226
229
  // full live cycle output here (not just milestones) plus a clear run banner.
227
230
  // Best-effort: a logging failure must never break the cycle.
228
- const mcpLog = path.join(REPO_DIR, "skill", "logs", "draft_cycle-mcp.log");
231
+ const mcpLog = path.join(repoDir(), "skill", "logs", "draft_cycle-mcp.log");
229
232
  const appendLog = (s) => {
230
233
  try {
231
234
  fs.appendFileSync(mcpLog, s);
@@ -526,6 +529,7 @@ server.registerTool("setup", {
526
529
  projects,
527
530
  x_connected: x.connected,
528
531
  x_state: x.state,
532
+ x_handle: x.handle ?? null,
529
533
  mcp_version: ver.installed,
530
534
  latest_version: ver.latest,
531
535
  update_available: ver.update_available,
@@ -536,7 +540,7 @@ server.registerTool("setup", {
536
540
  : undefined,
537
541
  required_fields: REQUIRED_FIELDS,
538
542
  recommended_fields: RECOMMENDED_FIELDS,
539
- config_path: CONFIG_PATH,
543
+ config_path: configPath(),
540
544
  next_step: projects.length === 0
541
545
  ? "No projects yet. Ask the user about a product (website, what it does, who to target, " +
542
546
  "brand voice), then call setup with a short name plus those fields. Repeat per product." +
@@ -576,6 +580,30 @@ server.registerTool("setup", {
576
580
  const tail = (seed.stderr || seed.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
577
581
  seedNote = ` (Heads up: couldn't seed search topics into the DB yet — ${tail}. draft_cycle will tell you clearly if topics are missing.)`;
578
582
  }
583
+ // Cold-start QUERY supply: fan the seeded topics out into >=30 real X
584
+ // search queries (project_search_queries) so the deterministic Phase 1
585
+ // bank (qualified_query_bank.py) has something to run on day one.
586
+ // Without this, a freshly-configured project's bank is empty and the
587
+ // cycle falls back to ONE crude topic-as-query. Best-effort: a failure
588
+ // here never fails setup; the topic-as-query fallback still works, just
589
+ // narrower. Supply-test is auto (only if the harness is up), so this
590
+ // stays fast when X isn't connected yet. (2026-06-04)
591
+ if (seed.code === 0) {
592
+ try {
593
+ const qseed = await runPython("scripts/seed_search_queries.py", ["--project", result.project, "--supply-test", "auto"], { timeoutMs: 600_000 });
594
+ const qm = /seeded=(\d+)\s+inserted=(\d+)\s+updated=(\d+)/.exec(qseed.stdout);
595
+ if (qseed.code === 0 && qm) {
596
+ seedNote += ` Expanded them into ${qm[1]} search quer${qm[1] === "1" ? "y" : "ies"} so the cycle can fan out instead of running a single query.`;
597
+ }
598
+ else if (qseed.code !== 0) {
599
+ const qtail = (qseed.stderr || qseed.stdout).trim().split("\n").slice(-1)[0] || "unknown error";
600
+ seedNote += ` (Search queries not expanded yet — ${qtail}. The cycle still runs off the seeded topics.)`;
601
+ }
602
+ }
603
+ catch (e) {
604
+ seedNote += ` (Search-query expansion skipped — ${e.message}.)`;
605
+ }
606
+ }
579
607
  }
580
608
  return jsonContent({
581
609
  ok: true,
@@ -584,7 +612,7 @@ server.registerTool("setup", {
584
612
  ready: result.ready,
585
613
  missing_required: result.missing_required,
586
614
  topics_seeded: result.ready,
587
- config_path: CONFIG_PATH,
615
+ config_path: configPath(),
588
616
  note: result.ready
589
617
  ? `Project '${result.project}' is fully set up.${seedNote} Next: connect X so the autoposter can post — ` +
590
618
  `call setup with action:'connect_x' (it explains itself, then run again with confirm:true). ` +
@@ -803,7 +831,7 @@ server.registerTool("autopilot", {
803
831
  "not scoped to one project.");
804
832
  }
805
833
  const uid = process.getuid ? process.getuid() : 0;
806
- const logDir = path.join(REPO_DIR, "skill", "logs");
834
+ const logDir = path.join(repoDir(), "skill", "logs");
807
835
  if (action === "status") {
808
836
  const res = await run("launchctl", ["list"], { timeoutMs: 10_000 });
809
837
  const lines = res.stdout.split("\n");
@@ -821,7 +849,7 @@ server.registerTool("autopilot", {
821
849
  // plist exists yet; never overwrite a hand-tuned/dev plist.
822
850
  const createdCycle = ensurePlist(TWITTER_AUTOPILOT_PLIST, plistXml({
823
851
  label: TWITTER_AUTOPILOT_LABEL,
824
- programArgs: ["/bin/bash", path.join(REPO_DIR, "skill", "run-cycle-update-guard.sh")],
852
+ programArgs: ["/bin/bash", path.join(repoDir(), "skill", "run-cycle-update-guard.sh")],
825
853
  intervalSecs: 60,
826
854
  runAtLoad: false,
827
855
  stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
@@ -832,7 +860,7 @@ server.registerTool("autopilot", {
832
860
  // in the loop. RunAtLoad so it also checks shortly after enable.
833
861
  const createdUpdater = ensurePlist(UPDATER_PLIST, plistXml({
834
862
  label: UPDATER_LABEL,
835
- programArgs: ["/bin/bash", path.join(REPO_DIR, "skill", "social-autoposter-update.sh")],
863
+ programArgs: ["/bin/bash", path.join(repoDir(), "skill", "social-autoposter-update.sh")],
836
864
  intervalSecs: 86_400,
837
865
  runAtLoad: true,
838
866
  stdoutLog: path.join(logDir, "launchd-self-update-stdout.log"),
@@ -950,6 +978,113 @@ server.registerTool("version", {
950
978
  : "You are on the latest published version.",
951
979
  });
952
980
  });
981
+ // ---- runtime installer ----------------------------------------------------
982
+ // The pipeline runs Python locally. Rather than depend on the user's system
983
+ // Python (the #1 source of install failures), the first run provisions a fully
984
+ // OWNED uv runtime: standalone CPython + owned venv + deps + Chromium. These two
985
+ // tools drive it. They are plain (non-UI) tools so EVERY host can install — the
986
+ // panel's Install card is just a skin that calls install_runtime then polls
987
+ // install_status. See runtime.ts for the provisioning + progress contract.
988
+ function runtimeSnapshot() {
989
+ const rt = readRuntime();
990
+ const progress = readProgress();
991
+ return {
992
+ runtime_ready: runtimeReady(),
993
+ provisioning: isProvisioning(),
994
+ python: rt?.python ?? null,
995
+ python_version: rt?.python_version ?? null,
996
+ progress: progress ?? null,
997
+ };
998
+ }
999
+ server.registerTool("install_runtime", {
1000
+ title: "Install the Python runtime",
1001
+ description: "One-time setup that provisions the self-contained runtime the autoposter needs: a private " +
1002
+ "Python (via uv, not your system Python), its dependencies, and the Chromium browser. Runs in " +
1003
+ "the background and returns immediately; poll `install_status` for progress. Safe to call " +
1004
+ "repeatedly; it resumes/repairs and is a no-op once everything is installed. Use this the " +
1005
+ "first time the user sets up, or if other tools report the runtime isn't ready.",
1006
+ inputSchema: {},
1007
+ }, async () => {
1008
+ if (runtimeReady()) {
1009
+ return jsonContent({ already_installed: true, ...runtimeSnapshot() });
1010
+ }
1011
+ const progress = startProvisioning();
1012
+ return jsonContent({
1013
+ started: true,
1014
+ runtime_ready: false,
1015
+ note: "Runtime install started. Poll install_status every ~1.5s for progress.",
1016
+ progress,
1017
+ });
1018
+ });
1019
+ server.registerTool("install_status", {
1020
+ title: "Runtime install status",
1021
+ description: "Report whether the self-contained Python/Chromium runtime is installed and, if an install is " +
1022
+ "in progress, the per-step progress (uv, Python, venv, dependencies, Chromium). Poll this after " +
1023
+ "install_runtime to follow the install to completion.",
1024
+ inputSchema: {},
1025
+ }, async () => jsonContent(runtimeSnapshot()));
1026
+ // ---- config: read / edit the raw config.json ------------------------------
1027
+ // The panel renders the full config and lets the user edit it. Writing is
1028
+ // guarded: the new content must parse as JSON, and we always drop a timestamped
1029
+ // backup next to config.json before overwriting, so a bad paste is recoverable.
1030
+ server.registerTool("config", {
1031
+ title: "View or edit config.json",
1032
+ description: "Read or update the autoposter's config.json (the source of truth for every project, the X/" +
1033
+ "Reddit/LinkedIn account handles, topics, and exclusions). action:'get' (default) returns the " +
1034
+ "full raw JSON; action:'save' validates the supplied `content` as JSON, writes a timestamped " +
1035
+ "backup, then overwrites config.json. Use when the user asks to see, edit, or fix their config.",
1036
+ inputSchema: {
1037
+ action: z.enum(["get", "save"]).optional(),
1038
+ content: z.string().optional(),
1039
+ },
1040
+ }, async (args) => {
1041
+ const action = args.action || "get";
1042
+ const cfgPath = configPath();
1043
+ if (action === "get") {
1044
+ try {
1045
+ const content = fs.readFileSync(cfgPath, "utf-8");
1046
+ return jsonContent({ ok: true, path: cfgPath, bytes: content.length, content });
1047
+ }
1048
+ catch (e) {
1049
+ return jsonContent({ ok: false, path: cfgPath, error: String(e?.message || e) });
1050
+ }
1051
+ }
1052
+ // save
1053
+ const content = args.content;
1054
+ if (typeof content !== "string" || content.trim() === "") {
1055
+ return jsonContent({ ok: false, error: "Nothing to save: `content` was empty." });
1056
+ }
1057
+ let parsed;
1058
+ try {
1059
+ parsed = JSON.parse(content);
1060
+ }
1061
+ catch (e) {
1062
+ // Don't write a config that won't parse — every pipeline reads this file.
1063
+ return jsonContent({ ok: false, error: "Invalid JSON, not saved: " + String(e?.message || e) });
1064
+ }
1065
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1066
+ return jsonContent({ ok: false, error: "Top level of config.json must be a JSON object." });
1067
+ }
1068
+ try {
1069
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
1070
+ const backup = `${cfgPath}.bak-panel-${stamp}`;
1071
+ try {
1072
+ fs.copyFileSync(cfgPath, backup);
1073
+ }
1074
+ catch {
1075
+ /* first-write / missing original is non-fatal */
1076
+ }
1077
+ // Re-serialize the parsed object so what lands on disk is canonical,
1078
+ // 2-space-indented JSON with a trailing newline (matches the Python
1079
+ // writers), regardless of how the user formatted their paste.
1080
+ const out = JSON.stringify(parsed, null, 2) + "\n";
1081
+ fs.writeFileSync(cfgPath, out, "utf-8");
1082
+ return jsonContent({ ok: true, path: cfgPath, bytes: out.length, backup });
1083
+ }
1084
+ catch (e) {
1085
+ return jsonContent({ ok: false, error: "Write failed: " + String(e?.message || e) });
1086
+ }
1087
+ });
953
1088
  // ---- panel: MCP Apps control surface --------------------------------------
954
1089
  // A self-contained HTML view rendered by hosts that support MCP Apps (Claude
955
1090
  // desktop/web, etc.). It duplicates NO pipeline logic: each button calls one of
@@ -990,11 +1125,16 @@ async function buildSnapshot() {
990
1125
  projects_ready: projects.filter((p) => p.ready).length,
991
1126
  x_connected: !!x.connected,
992
1127
  x_state: x.state || "",
1128
+ x_handle: x.handle ?? null,
993
1129
  autopilot_on: ap.autopilot_on,
994
1130
  auto_update_on: ap.auto_update_on,
995
1131
  version: ver.installed || VERSION,
996
1132
  latest_version: ver.latest ?? null,
997
1133
  update_available: !!ver.update_available,
1134
+ // Runtime install gate: the panel shows the Install card (and disables the
1135
+ // action buttons) until the owned Python/Chromium runtime is provisioned.
1136
+ runtime_ready: runtimeReady(),
1137
+ runtime_provisioning: isProvisioning(),
998
1138
  };
999
1139
  }
1000
1140
  registerAppTool(server, "panel", {
@@ -1030,7 +1170,7 @@ registerAppResource(server, "Social Autoposter panel", PANEL_URI, { mimeType: RE
1030
1170
  async function main() {
1031
1171
  const transport = new StdioServerTransport();
1032
1172
  await server.connect(transport);
1033
- console.error(`[social-autoposter-mcp] connected. v=${VERSION} repo=${REPO_DIR}`);
1173
+ console.error(`[social-autoposter-mcp] connected. v=${VERSION} repo=${repoDir()}`);
1034
1174
  }
1035
1175
  main().catch((err) => {
1036
1176
  console.error("[social-autoposter-mcp] fatal:", err);