social-autoposter 1.6.69 → 1.6.70
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 +86 -99
- package/mcp/dist/panel.html +18 -28
- package/mcp/dist/runtime.js +175 -0
- package/mcp/dist/version.json +2 -2
- package/mcp/manifest.json +1 -5
- package/mcp/menubar/s4l_card.py +263 -0
- package/mcp/menubar/s4l_menubar.py +477 -0
- package/mcp/menubar/s4l_state.py +363 -0
- package/package.json +2 -1
- package/scripts/reset-test-machine.sh +18 -1
package/mcp/dist/index.js
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// draft_cycle - scan + draft, return all drafts as a numbered table for the
|
|
6
6
|
// user to review in chat (posts nothing).
|
|
7
7
|
// post_drafts - post the drafts the user chose by number from a batch.
|
|
8
|
-
// autopilot - one tool, action = enable | disable | status (launchd job).
|
|
9
8
|
// get_stats - read-only post + engagement stats.
|
|
10
9
|
//
|
|
11
10
|
// THIN wrapper. The pipeline brain (scan, score, drafting prompts, posting)
|
|
@@ -18,9 +17,9 @@ import os from "node:os";
|
|
|
18
17
|
import path from "node:path";
|
|
19
18
|
import fs from "node:fs";
|
|
20
19
|
import { repoDir, runPython, run, readPlan, writePlan, planPath, } from "./repo.js";
|
|
21
|
-
import { applySetup, resolveProject,
|
|
20
|
+
import { applySetup, resolveProject, listManagedProjectStatus, REQUIRED_FIELDS, RECOMMENDED_FIELDS, configPath, } from "./setup.js";
|
|
22
21
|
import { xStatus, xConnect, xDetectSources, xScanProfile, summarizeXAuth } from "./twitterAuth.js";
|
|
23
|
-
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, } from "./runtime.js";
|
|
22
|
+
import { startProvisioning, isProvisioning, readProgress, runtimeReady, readRuntime, resolvePython, resolveChrome, ensureMenubar, } from "./runtime.js";
|
|
24
23
|
import { blockOnboardingMilestone, completeOnboardingMilestone, ensureDoctorPhase, onboardingLedger, onboardingSnapshot, recordOnboardingAttempt, runDoctorPhase, } from "./onboarding.js";
|
|
25
24
|
import { VERSION, versionStatus, latestPublishedVersion } from "./version.js";
|
|
26
25
|
import { initSentry, sendHeartbeat, captureError, flushSentry } from "./telemetry.js";
|
|
@@ -177,12 +176,11 @@ const server = new McpServer({
|
|
|
177
176
|
"surfaces `update_available` and an `update_hint`.\n\n" +
|
|
178
177
|
"TYPICAL FLOW: `project_config` (configure OR edit the project, and connect X) -> `draft_cycle` " +
|
|
179
178
|
"(scan + review a batch; the user approves / edits / skips every draft in a single form) -> " +
|
|
180
|
-
"`autopilot` (enable to also turn on hands-free background posting AND daily auto-updates) -> " +
|
|
181
179
|
"`get_stats` (see performance). Run `project_config` first; the other tools refuse until a " +
|
|
182
180
|
"project is fully configured. To change anything about a project later, call `project_config` " +
|
|
183
181
|
"again with the project's name and just the changed fields — there is no separate config editor.\n\n" +
|
|
184
182
|
"RENDER THE DASHBOARD AFTER ACTIONS. After any state-changing or results-producing tool call " +
|
|
185
|
-
"(`draft_cycle`, `post_drafts`, `
|
|
183
|
+
"(`draft_cycle`, `post_drafts`, `get_stats`), end your turn by " +
|
|
186
184
|
"calling the `dashboard` tool so the user sees the updated state visually. Do NOT call " +
|
|
187
185
|
"`dashboard` after pure Q&A, config explanations, or status-only checks that changed nothing.",
|
|
188
186
|
});
|
|
@@ -502,6 +500,19 @@ async function postApproved(batchId, plan) {
|
|
|
502
500
|
catch {
|
|
503
501
|
/* keep raw */
|
|
504
502
|
}
|
|
503
|
+
// On a successful run, mark the posted candidates in the ORIGINAL plan so the
|
|
504
|
+
// other review surface (chat vs menu-bar pop-ups) skips them — cross-surface
|
|
505
|
+
// de-dup. Best-effort; the pipeline's own already-posted check is the backstop.
|
|
506
|
+
if (res.code === 0) {
|
|
507
|
+
for (const c of approved)
|
|
508
|
+
c.posted = true;
|
|
509
|
+
try {
|
|
510
|
+
writePlan(batchId, plan);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
/* best effort */
|
|
514
|
+
}
|
|
515
|
+
}
|
|
505
516
|
return {
|
|
506
517
|
attempted: approved.length,
|
|
507
518
|
exit_code: res.code,
|
|
@@ -604,7 +615,7 @@ tool("project_config", {
|
|
|
604
615
|
"Call with status:true (or no name) to list every configured project, its remaining fields, AND " +
|
|
605
616
|
"whether X is connected. Use config, conversation context, profile_scan, and website research " +
|
|
606
617
|
"before asking for fields. Ask only if no product can be identified or an interactive login is " +
|
|
607
|
-
"unavoidable. The draft_cycle
|
|
618
|
+
"unavoidable. The draft_cycle and get_stats tools refuse to run until a project is " +
|
|
608
619
|
"fully set up.",
|
|
609
620
|
inputSchema: {
|
|
610
621
|
status: z.boolean().optional(),
|
|
@@ -1071,6 +1082,15 @@ tool("draft_cycle", {
|
|
|
1071
1082
|
outcome: "review_batch",
|
|
1072
1083
|
draft_count: count,
|
|
1073
1084
|
});
|
|
1085
|
+
// Fire the menu-bar pop-up review (default). The chat-table review below is
|
|
1086
|
+
// unchanged; both surfaces can approve, de-duped by the plan `posted` flag.
|
|
1087
|
+
writeReviewRequest({
|
|
1088
|
+
batch_id: drafted.batchId,
|
|
1089
|
+
project: proj,
|
|
1090
|
+
count,
|
|
1091
|
+
plan_path: planPath(drafted.batchId),
|
|
1092
|
+
created_at: new Date().toISOString(),
|
|
1093
|
+
});
|
|
1074
1094
|
const table = renderDraftsTable(plan);
|
|
1075
1095
|
const message = `Drafted ${count} ${count === 1 ? "reply" : "replies"} for "${proj}" ` +
|
|
1076
1096
|
`(batch ${drafted.batchId}). NOTHING has been posted yet.\n\n` +
|
|
@@ -1175,6 +1195,18 @@ tool("post_drafts", {
|
|
|
1175
1195
|
else
|
|
1176
1196
|
warnings.push(`ignored #${n}: out of range (1-${total})`);
|
|
1177
1197
|
});
|
|
1198
|
+
// Cross-surface de-dup: chat and the menu-bar pop-ups can both approve, so
|
|
1199
|
+
// never re-post a candidate the other surface already posted.
|
|
1200
|
+
const alreadyPosted = [];
|
|
1201
|
+
for (const n of Array.from(approve)) {
|
|
1202
|
+
if (candidates[n - 1]?.posted === true) {
|
|
1203
|
+
approve.delete(n);
|
|
1204
|
+
alreadyPosted.push(n);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (alreadyPosted.length) {
|
|
1208
|
+
warnings.push(`already posted (skipped): ${alreadyPosted.sort((a, b) => a - b).join(", ")}`);
|
|
1209
|
+
}
|
|
1178
1210
|
candidates.forEach((c, i) => (c.approved = approve.has(i + 1)));
|
|
1179
1211
|
writePlan(batch_id, plan);
|
|
1180
1212
|
if (approve.size === 0) {
|
|
@@ -1199,92 +1231,16 @@ tool("post_drafts", {
|
|
|
1199
1231
|
warnings,
|
|
1200
1232
|
});
|
|
1201
1233
|
});
|
|
1202
|
-
// ---- autopilot:
|
|
1203
|
-
tool(
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}, async ({ action }) => {
|
|
1213
|
-
if (action !== "status" && !hasReadyProject()) {
|
|
1214
|
-
return textContent("No project is fully set up yet, so autopilot has nothing to post. Run the `project_config` tool " +
|
|
1215
|
-
"first. Note: autopilot runs the background cycle across all configured projects; it is " +
|
|
1216
|
-
"not scoped to one project.");
|
|
1217
|
-
}
|
|
1218
|
-
const uid = process.getuid ? process.getuid() : 0;
|
|
1219
|
-
const logDir = path.join(repoDir(), "skill", "logs");
|
|
1220
|
-
if (action === "status") {
|
|
1221
|
-
const res = await run("launchctl", ["list"], { timeoutMs: 10_000 });
|
|
1222
|
-
const lines = res.stdout.split("\n");
|
|
1223
|
-
const loaded = lines.some((l) => l.includes(TWITTER_AUTOPILOT_LABEL));
|
|
1224
|
-
const updaterLoaded = lines.some((l) => l.includes(UPDATER_LABEL));
|
|
1225
|
-
return jsonContent({
|
|
1226
|
-
label: TWITTER_AUTOPILOT_LABEL,
|
|
1227
|
-
loaded,
|
|
1228
|
-
auto_update_label: UPDATER_LABEL,
|
|
1229
|
-
auto_update_loaded: updaterLoaded,
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
if (action === "enable") {
|
|
1233
|
-
// Bring up the on-screen overlay watcher so background autopilot cycles
|
|
1234
|
-
// still paint the harness status/sidebar. Idempotent + detached.
|
|
1235
|
-
await ensureOverlayWatch();
|
|
1236
|
-
// 1) Cycle plist. Write one pointing at the self-update guard ONLY if no
|
|
1237
|
-
// plist exists yet; never overwrite a hand-tuned/dev plist.
|
|
1238
|
-
const createdCycle = ensurePlist(TWITTER_AUTOPILOT_PLIST, plistXml({
|
|
1239
|
-
label: TWITTER_AUTOPILOT_LABEL,
|
|
1240
|
-
programArgs: ["/bin/bash", path.join(repoDir(), "skill", "run-cycle-update-guard.sh")],
|
|
1241
|
-
intervalSecs: 60,
|
|
1242
|
-
runAtLoad: false,
|
|
1243
|
-
stdoutLog: path.join(logDir, "launchd-twitter-cycle-stdout.log"),
|
|
1244
|
-
stderrLog: path.join(logDir, "launchd-twitter-cycle-stderr.log"),
|
|
1245
|
-
}));
|
|
1246
|
-
const cycleRes = await loadPlist(TWITTER_AUTOPILOT_LABEL, TWITTER_AUTOPILOT_PLIST, uid);
|
|
1247
|
-
// 2) Daily self-updater. Keeps a headless install current with no human
|
|
1248
|
-
// in the loop. RunAtLoad so it also checks shortly after enable.
|
|
1249
|
-
const createdUpdater = ensurePlist(UPDATER_PLIST, plistXml({
|
|
1250
|
-
label: UPDATER_LABEL,
|
|
1251
|
-
programArgs: ["/bin/bash", path.join(repoDir(), "skill", "social-autoposter-update.sh")],
|
|
1252
|
-
intervalSecs: 86_400,
|
|
1253
|
-
runAtLoad: true,
|
|
1254
|
-
stdoutLog: path.join(logDir, "launchd-self-update-stdout.log"),
|
|
1255
|
-
stderrLog: path.join(logDir, "launchd-self-update-stderr.log"),
|
|
1256
|
-
}));
|
|
1257
|
-
const updaterRes = await loadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
|
|
1258
|
-
return jsonContent({
|
|
1259
|
-
action: "enable",
|
|
1260
|
-
autopilot: {
|
|
1261
|
-
loaded: cycleRes.code === 0,
|
|
1262
|
-
plist: TWITTER_AUTOPILOT_PLIST,
|
|
1263
|
-
created: createdCycle,
|
|
1264
|
-
error: cycleRes.code === 0 ? null : (cycleRes.stderr || cycleRes.stdout).trim(),
|
|
1265
|
-
},
|
|
1266
|
-
auto_update: {
|
|
1267
|
-
loaded: updaterRes.code === 0,
|
|
1268
|
-
plist: UPDATER_PLIST,
|
|
1269
|
-
created: createdUpdater,
|
|
1270
|
-
note: "Daily updater enabled. It self-updates real npm installs and is a no-op on dev/source " +
|
|
1271
|
-
"checkouts (refuses to clobber a .git working tree).",
|
|
1272
|
-
error: updaterRes.code === 0 ? null : (updaterRes.stderr || updaterRes.stdout).trim(),
|
|
1273
|
-
},
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
// disable — unload both jobs (leave the plist files in place for re-enable)
|
|
1277
|
-
const cycleOff = await unloadPlist(TWITTER_AUTOPILOT_LABEL, TWITTER_AUTOPILOT_PLIST, uid);
|
|
1278
|
-
const updaterOff = await unloadPlist(UPDATER_LABEL, UPDATER_PLIST, uid);
|
|
1279
|
-
return jsonContent({
|
|
1280
|
-
action: "disable",
|
|
1281
|
-
autopilot_unloaded: cycleOff.code === 0,
|
|
1282
|
-
auto_update_unloaded: updaterOff.code === 0,
|
|
1283
|
-
note: cycleOff.code === 0
|
|
1284
|
-
? "Autopilot and daily auto-update unloaded. Manual draft_cycle still works."
|
|
1285
|
-
: `Autopilot disable reported exit ${cycleOff.code}: ${(cycleOff.stderr || cycleOff.stdout).trim()}`,
|
|
1286
|
-
});
|
|
1287
|
-
});
|
|
1234
|
+
// ---- autopilot: MCP tool removed ------------------------------------------
|
|
1235
|
+
// The `autopilot` MCP tool (enable/disable/status) was intentionally removed:
|
|
1236
|
+
// hands-free background posting is no longer toggled from the agent/tool surface.
|
|
1237
|
+
// The underlying launchd cycle job + plist (com.m13v.social-twitter-cycle) and
|
|
1238
|
+
// the daily self-updater are NOT touched here — an already-loaded job keeps
|
|
1239
|
+
// running, and the plist files stay on disk. The plist helpers above
|
|
1240
|
+
// (ensurePlist / plistXml / loadPlist / unloadPlist) and the constants are kept
|
|
1241
|
+
// as the underlying source for that job; the `dashboard` snapshot still reports
|
|
1242
|
+
// the job's loaded state via autopilotLoaded(). To enable/disable the job now,
|
|
1243
|
+
// use launchctl directly or re-add a tool here.
|
|
1288
1244
|
// ---- get_stats: read-only -------------------------------------------------
|
|
1289
1245
|
tool("get_stats", {
|
|
1290
1246
|
title: "Get X/Twitter stats",
|
|
@@ -1452,7 +1408,7 @@ function runtimeSnapshot() {
|
|
|
1452
1408
|
// ---- panel: MCP Apps control surface --------------------------------------
|
|
1453
1409
|
// A self-contained HTML view rendered by hosts that support MCP Apps (Claude
|
|
1454
1410
|
// desktop/web, etc.). It duplicates NO pipeline logic: each button calls one of
|
|
1455
|
-
// the tools above (draft_cycle /
|
|
1411
|
+
// the tools above (draft_cycle / project_config / get_stats) through the host
|
|
1456
1412
|
// and re-reads status. The tool itself returns the first-paint snapshot so the
|
|
1457
1413
|
// view has data the instant it loads.
|
|
1458
1414
|
// Is either launchd job (cycle / daily updater) currently loaded?
|
|
@@ -1624,19 +1580,44 @@ function startLocalPanel() {
|
|
|
1624
1580
|
});
|
|
1625
1581
|
});
|
|
1626
1582
|
}
|
|
1627
|
-
// Publish the loopback URL to
|
|
1628
|
-
//
|
|
1629
|
-
//
|
|
1583
|
+
// Publish the loopback URL to stable files so out-of-process readers can find
|
|
1584
|
+
// the ephemeral port without scraping `lsof`:
|
|
1585
|
+
// - panel-url plain text, for the Claude Code side-panel reverse proxy.
|
|
1586
|
+
// - panel-endpoint.json richer (url + version + pid), for the menu bar app,
|
|
1587
|
+
// which POSTs /tool/<name> here for live data.
|
|
1588
|
+
// Best-effort: a write failure never blocks the panel (readers re-check /health).
|
|
1630
1589
|
function writePanelUrl(url) {
|
|
1631
1590
|
try {
|
|
1632
1591
|
const dir = path.join(process.env.HOME || os.homedir(), ".social-autoposter-mcp");
|
|
1633
1592
|
fs.mkdirSync(dir, { recursive: true });
|
|
1634
1593
|
fs.writeFileSync(path.join(dir, "panel-url"), url, "utf-8");
|
|
1594
|
+
fs.writeFileSync(path.join(dir, "panel-endpoint.json"), JSON.stringify({ url, pid: process.pid, version: VERSION, started_at: new Date().toISOString() }, null, 2) + "\n", "utf-8");
|
|
1635
1595
|
}
|
|
1636
1596
|
catch (e) {
|
|
1637
1597
|
console.error("[social-autoposter-mcp] writePanelUrl failed:", e?.message || e);
|
|
1638
1598
|
}
|
|
1639
1599
|
}
|
|
1600
|
+
// The owned state dir, honoring SAPS_STATE_DIR (matches menubar/s4l_state.py).
|
|
1601
|
+
function sapsStateDir() {
|
|
1602
|
+
return (process.env.SAPS_STATE_DIR ||
|
|
1603
|
+
path.join(process.env.HOME || os.homedir(), ".social-autoposter-mcp"));
|
|
1604
|
+
}
|
|
1605
|
+
// Signal the menu bar that a fresh draft batch is ready for pop-up review. The
|
|
1606
|
+
// chat-table review path is unchanged and still works; this just ALSO lets the
|
|
1607
|
+
// corner cards drive review (both surfaces de-dup via the plan's `posted` flag).
|
|
1608
|
+
// The menu bar reads review-request.json, presents the cards, posts via the
|
|
1609
|
+
// loopback post_drafts tool, then clears the file. Best-effort: a write failure
|
|
1610
|
+
// just means no pop-ups this batch (chat review still works).
|
|
1611
|
+
function writeReviewRequest(req) {
|
|
1612
|
+
try {
|
|
1613
|
+
const dir = sapsStateDir();
|
|
1614
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1615
|
+
fs.writeFileSync(path.join(dir, "review-request.json"), JSON.stringify(req, null, 2) + "\n", "utf-8");
|
|
1616
|
+
}
|
|
1617
|
+
catch (e) {
|
|
1618
|
+
console.error("[social-autoposter-mcp] writeReviewRequest failed:", e?.message || e);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1640
1621
|
// Open a URL in the user's default browser, cross-platform. Opening is OPT-IN:
|
|
1641
1622
|
// by default we do NOT pop a browser tab. The dashboard already surfaces in-host
|
|
1642
1623
|
// (MCP Apps inline) or via the Claude Code side panel / returned loopback URL, so
|
|
@@ -1658,10 +1639,10 @@ async function openInBrowser(url) {
|
|
|
1658
1639
|
appTool("dashboard", {
|
|
1659
1640
|
title: "Social Autoposter dashboard",
|
|
1660
1641
|
description: "Render the Social Autoposter dashboard in chat: a visual surface showing project setup, X " +
|
|
1661
|
-
"connection, autopilot state, and 7-day stats, with buttons to run a draft cycle,
|
|
1662
|
-
"
|
|
1642
|
+
"connection, autopilot state, and 7-day stats, with buttons to run a draft cycle, connect X, " +
|
|
1643
|
+
"and refresh. Use when the user asks to see the dashboard, panel, " +
|
|
1663
1644
|
"status, or controls. ALSO call this at the end of any state-changing or results-producing " +
|
|
1664
|
-
"action (draft_cycle, post_drafts,
|
|
1645
|
+
"action (draft_cycle, post_drafts, get_stats) so the user sees the " +
|
|
1665
1646
|
"updated dashboard. Hosts without UI support get the same data as text.",
|
|
1666
1647
|
inputSchema: {},
|
|
1667
1648
|
// fallback_url is set only when the host can't render the ui:// resource and
|
|
@@ -1802,6 +1783,12 @@ async function main() {
|
|
|
1802
1783
|
void startLocalPanel()
|
|
1803
1784
|
.then((url) => console.error(`[social-autoposter-mcp] panel loopback ready at ${url}`))
|
|
1804
1785
|
.catch((e) => console.error("[social-autoposter-mcp] panel loopback start failed:", e?.message || e));
|
|
1786
|
+
// Ensure the macOS menu bar mini-dashboard is installed + running. Idempotent
|
|
1787
|
+
// and cheap when already present, so existing installs pick it up on the next
|
|
1788
|
+
// Claude restart without re-provisioning. Best-effort: never blocks boot.
|
|
1789
|
+
void ensureMenubar()
|
|
1790
|
+
.then((r) => console.error(`[social-autoposter-mcp] menubar: ${r.skipped ? "skip" : r.ok ? "ok" : "fail"} (${r.detail})`))
|
|
1791
|
+
.catch((e) => console.error("[social-autoposter-mcp] menubar ensure failed:", e?.message || e));
|
|
1805
1792
|
// Phone home so this .mcpb install is visible in the install-lane digest
|
|
1806
1793
|
// (parity with the npx launchd heartbeat). Once on startup, then every 15m
|
|
1807
1794
|
// while the desktop app keeps the server alive. unref() so it never holds the
|