social-autoposter 1.6.52 → 1.6.54

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
@@ -520,31 +520,43 @@ function installBrowserHarness() {
520
520
  }
521
521
 
522
522
  // Step 2 + 3: clone + `uv tool install -e .` browser-harness.
523
+ //
524
+ // PINNED to a known-good upstream commit instead of tracking origin/HEAD.
525
+ // The installer used to fetch+reset --hard to HEAD on every run, so any
526
+ // upstream change shipped to users untested (this is how the two-blank-tab
527
+ // regression in upstream daemon.py attach behavior could reach users). Our
528
+ // launch-at-real-URL fix in server.py/twitter-backend.sh neutralizes that
529
+ // class of bug regardless, but pinning stops surprise upstream drift. Bump
530
+ // BROWSER_HARNESS_PIN deliberately after validating a newer upstream against
531
+ // the shipped server.py contract.
532
+ const BROWSER_HARNESS_PIN = '6d20866664ea3d9691b27bbf64f42ae097437dc3';
523
533
  const harnessDir = path.join(HOME, 'Developer', 'browser-harness');
534
+ const pinHarness = () => {
535
+ // Fetch the exact pinned commit (GitHub serves arbitrary SHAs) and hard-
536
+ // reset onto it. Works for a fresh clone and an existing checkout alike.
537
+ const fetch = spawnSync('git', ['-C', harnessDir, 'fetch', '--depth', '1', 'origin', BROWSER_HARNESS_PIN], { stdio: 'inherit' });
538
+ if (fetch.status !== 0) {
539
+ console.warn(` WARNING: could not fetch pinned browser-harness commit ${BROWSER_HARNESS_PIN.slice(0, 9)}; using existing checkout.`);
540
+ return;
541
+ }
542
+ const reset = spawnSync('git', ['-C', harnessDir, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'inherit' });
543
+ if (reset.status !== 0) {
544
+ console.warn(' WARNING: could not reset browser-harness clone to pinned commit; using existing checkout.');
545
+ }
546
+ };
524
547
  if (!fs.existsSync(harnessDir)) {
525
548
  fs.mkdirSync(path.dirname(harnessDir), { recursive: true });
526
549
  console.log(' cloning browser-harness from GitHub...');
527
550
  const clone = spawnSync('git', ['clone', '--depth', '1', 'https://github.com/browser-use/browser-harness', harnessDir], { stdio: 'inherit' });
528
551
  if (clone.status !== 0) {
529
552
  console.warn(' WARNING: git clone failed; twitter-harness will not work until you clone manually.');
530
- }
531
- } else {
532
- // Refresh the existing clone instead of silently reusing it. server.py
533
- // invokes `browser-harness -c <script>`; a stale checkout that predates the
534
- // `-c` interface (or otherwise drifted from upstream) makes every bh_run
535
- // return the CLI usage string while looking "installed". fetch+reset --hard
536
- // to current upstream so the installed CLI always matches the shipped
537
- // server.py contract.
538
- console.log(` browser-harness clone exists -> ${harnessDir}; updating to latest...`);
539
- const fetch = spawnSync('git', ['-C', harnessDir, 'fetch', '--depth', '1', 'origin', 'HEAD'], { stdio: 'inherit' });
540
- if (fetch.status === 0) {
541
- const reset = spawnSync('git', ['-C', harnessDir, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'inherit' });
542
- if (reset.status !== 0) {
543
- console.warn(' WARNING: could not reset browser-harness clone to latest; using existing checkout.');
544
- }
545
553
  } else {
546
- console.warn(' WARNING: could not fetch browser-harness updates; using existing checkout.');
554
+ console.log(` pinning browser-harness to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
555
+ pinHarness();
547
556
  }
557
+ } else {
558
+ console.log(` browser-harness clone exists -> ${harnessDir}; pinning to ${BROWSER_HARNESS_PIN.slice(0, 9)}...`);
559
+ pinHarness();
548
560
  }
549
561
 
550
562
  if (uvBin && fs.existsSync(harnessDir)) {
@@ -1193,6 +1205,24 @@ function doctor() {
1193
1205
  }
1194
1206
  }
1195
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
+
1196
1226
  const cmd = process.argv[2];
1197
1227
  if (cmd === 'init') {
1198
1228
  init();
@@ -1202,6 +1232,8 @@ if (cmd === 'init') {
1202
1232
  doctor();
1203
1233
  } else if (cmd === 'bootstrap-vm') {
1204
1234
  bootstrapVm();
1235
+ } else if (cmd === 'install-runtime') {
1236
+ installRuntime();
1205
1237
  } else if (cmd === 'export-cookies') {
1206
1238
  // Forward to cookie-helper with 'export' + remaining args
1207
1239
  process.argv = [process.argv[0], process.argv[1], 'export', ...process.argv.slice(3)];
@@ -1231,6 +1263,7 @@ if (cmd === 'init') {
1231
1263
  console.log(' npx social-autoposter update update scripts, preserve config');
1232
1264
  console.log(' npx social-autoposter doctor probe install health (#6, 1.6.34+)');
1233
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)');
1234
1267
  console.log(' npx social-autoposter export-cookies [dir] export browser cookies');
1235
1268
  console.log(' npx social-autoposter import-cookies [dir] import browser cookies');
1236
1269
  }
package/mcp/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import fs from "node:fs";
19
19
  import { REPO_DIR, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
20
20
  import { applySetup, resolveProject, hasReadyProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, CONFIG_PATH, } from "./setup.js";
21
21
  import { xStatus, xConnect, summarizeXAuth } from "./twitterAuth.js";
22
+ import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, } 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";
@@ -526,6 +527,7 @@ server.registerTool("setup", {
526
527
  projects,
527
528
  x_connected: x.connected,
528
529
  x_state: x.state,
530
+ x_handle: x.handle ?? null,
529
531
  mcp_version: ver.installed,
530
532
  latest_version: ver.latest,
531
533
  update_available: ver.update_available,
@@ -950,6 +952,112 @@ server.registerTool("version", {
950
952
  : "You are on the latest published version.",
951
953
  });
952
954
  });
955
+ // ---- runtime installer ----------------------------------------------------
956
+ // The pipeline runs Python locally. Rather than depend on the user's system
957
+ // Python (the #1 source of install failures), the first run provisions a fully
958
+ // OWNED uv runtime: standalone CPython + owned venv + deps + Chromium. These two
959
+ // tools drive it. They are plain (non-UI) tools so EVERY host can install — the
960
+ // panel's Install card is just a skin that calls install_runtime then polls
961
+ // install_status. See runtime.ts for the provisioning + progress contract.
962
+ function runtimeSnapshot() {
963
+ const rt = readRuntime();
964
+ const progress = readProgress();
965
+ return {
966
+ runtime_ready: runtimeReady(),
967
+ provisioning: isProvisioning(),
968
+ python: rt?.python ?? null,
969
+ python_version: rt?.python_version ?? null,
970
+ progress: progress ?? null,
971
+ };
972
+ }
973
+ server.registerTool("install_runtime", {
974
+ title: "Install the Python runtime",
975
+ description: "One-time setup that provisions the self-contained runtime the autoposter needs: a private " +
976
+ "Python (via uv, not your system Python), its dependencies, and the Chromium browser. Runs in " +
977
+ "the background and returns immediately; poll `install_status` for progress. Safe to call " +
978
+ "repeatedly; it resumes/repairs and is a no-op once everything is installed. Use this the " +
979
+ "first time the user sets up, or if other tools report the runtime isn't ready.",
980
+ inputSchema: {},
981
+ }, async () => {
982
+ if (runtimeReady()) {
983
+ return jsonContent({ already_installed: true, ...runtimeSnapshot() });
984
+ }
985
+ const progress = startProvisioning();
986
+ return jsonContent({
987
+ started: true,
988
+ runtime_ready: false,
989
+ note: "Runtime install started. Poll install_status every ~1.5s for progress.",
990
+ progress,
991
+ });
992
+ });
993
+ server.registerTool("install_status", {
994
+ title: "Runtime install status",
995
+ description: "Report whether the self-contained Python/Chromium runtime is installed and, if an install is " +
996
+ "in progress, the per-step progress (uv, Python, venv, dependencies, Chromium). Poll this after " +
997
+ "install_runtime to follow the install to completion.",
998
+ inputSchema: {},
999
+ }, async () => jsonContent(runtimeSnapshot()));
1000
+ // ---- config: read / edit the raw config.json ------------------------------
1001
+ // The panel renders the full config and lets the user edit it. Writing is
1002
+ // guarded: the new content must parse as JSON, and we always drop a timestamped
1003
+ // backup next to config.json before overwriting, so a bad paste is recoverable.
1004
+ server.registerTool("config", {
1005
+ title: "View or edit config.json",
1006
+ description: "Read or update the autoposter's config.json (the source of truth for every project, the X/" +
1007
+ "Reddit/LinkedIn account handles, topics, and exclusions). action:'get' (default) returns the " +
1008
+ "full raw JSON; action:'save' validates the supplied `content` as JSON, writes a timestamped " +
1009
+ "backup, then overwrites config.json. Use when the user asks to see, edit, or fix their config.",
1010
+ inputSchema: {
1011
+ action: z.enum(["get", "save"]).optional(),
1012
+ content: z.string().optional(),
1013
+ },
1014
+ }, async (args) => {
1015
+ const action = args.action || "get";
1016
+ if (action === "get") {
1017
+ try {
1018
+ const content = fs.readFileSync(CONFIG_PATH, "utf-8");
1019
+ return jsonContent({ ok: true, path: CONFIG_PATH, bytes: content.length, content });
1020
+ }
1021
+ catch (e) {
1022
+ return jsonContent({ ok: false, path: CONFIG_PATH, error: String(e?.message || e) });
1023
+ }
1024
+ }
1025
+ // save
1026
+ const content = args.content;
1027
+ if (typeof content !== "string" || content.trim() === "") {
1028
+ return jsonContent({ ok: false, error: "Nothing to save: `content` was empty." });
1029
+ }
1030
+ let parsed;
1031
+ try {
1032
+ parsed = JSON.parse(content);
1033
+ }
1034
+ catch (e) {
1035
+ // Don't write a config that won't parse — every pipeline reads this file.
1036
+ return jsonContent({ ok: false, error: "Invalid JSON, not saved: " + String(e?.message || e) });
1037
+ }
1038
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1039
+ return jsonContent({ ok: false, error: "Top level of config.json must be a JSON object." });
1040
+ }
1041
+ try {
1042
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
1043
+ const backup = `${CONFIG_PATH}.bak-panel-${stamp}`;
1044
+ try {
1045
+ fs.copyFileSync(CONFIG_PATH, backup);
1046
+ }
1047
+ catch {
1048
+ /* first-write / missing original is non-fatal */
1049
+ }
1050
+ // Re-serialize the parsed object so what lands on disk is canonical,
1051
+ // 2-space-indented JSON with a trailing newline (matches the Python
1052
+ // writers), regardless of how the user formatted their paste.
1053
+ const out = JSON.stringify(parsed, null, 2) + "\n";
1054
+ fs.writeFileSync(CONFIG_PATH, out, "utf-8");
1055
+ return jsonContent({ ok: true, path: CONFIG_PATH, bytes: out.length, backup });
1056
+ }
1057
+ catch (e) {
1058
+ return jsonContent({ ok: false, error: "Write failed: " + String(e?.message || e) });
1059
+ }
1060
+ });
953
1061
  // ---- panel: MCP Apps control surface --------------------------------------
954
1062
  // A self-contained HTML view rendered by hosts that support MCP Apps (Claude
955
1063
  // desktop/web, etc.). It duplicates NO pipeline logic: each button calls one of
@@ -990,11 +1098,16 @@ async function buildSnapshot() {
990
1098
  projects_ready: projects.filter((p) => p.ready).length,
991
1099
  x_connected: !!x.connected,
992
1100
  x_state: x.state || "",
1101
+ x_handle: x.handle ?? null,
993
1102
  autopilot_on: ap.autopilot_on,
994
1103
  auto_update_on: ap.auto_update_on,
995
1104
  version: ver.installed || VERSION,
996
1105
  latest_version: ver.latest ?? null,
997
1106
  update_available: !!ver.update_available,
1107
+ // Runtime install gate: the panel shows the Install card (and disables the
1108
+ // action buttons) until the owned Python/Chromium runtime is provisioned.
1109
+ runtime_ready: runtimeReady(),
1110
+ runtime_provisioning: isProvisioning(),
998
1111
  };
999
1112
  }
1000
1113
  registerAppTool(server, "panel", {