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 +21 -0
- package/mcp/dist/index.js +150 -10
- package/mcp/dist/panel.html +50 -14
- package/mcp/dist/repo.js +16 -7
- package/mcp/dist/runtime.js +345 -0
- package/mcp/dist/setup.js +15 -9
- package/mcp/dist/version.js +2 -2
- package/mcp/dist/version.json +4 -1
- package/mcp/install-runtime.mjs +70 -0
- package/mcp/manifest.json +2 -20
- package/mcp/package.json +2 -0
- package/package.json +4 -1
- package/scripts/discover_linkedin_candidates.py +32 -0
- package/scripts/engagement_styles.py +9 -41
- package/scripts/github_tools.py +5 -3
- package/scripts/http_api.py +43 -1
- package/scripts/linkedin_exclusions.py +234 -0
- package/scripts/log_post.py +5 -9
- package/scripts/preflight.sh +6 -13
- package/scripts/qualified_query_bank.py +99 -10
- package/scripts/release-mcpb.sh +140 -0
- package/scripts/scan_dm_candidates.py +16 -0
- package/scripts/score_linkedin_candidates.py +17 -0
- package/scripts/scrape_linkedin_comment_stats.py +16 -3
- package/scripts/seed_search_queries.py +303 -0
- package/scripts/setup_twitter_auth.py +99 -1
- package/scripts/twitter_post_plan.py +3 -66
- package/skill/run-instagram-render.sh +2 -2
- package/skill/run-twitter-cycle.sh +6 -38
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 {
|
|
20
|
-
import { applySetup, resolveProject, hasReadyProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS,
|
|
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>${
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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=${
|
|
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);
|