social-autoposter 1.6.72 → 1.6.73

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
@@ -30,6 +30,11 @@ import http from "node:http";
30
30
  // (vite-plugin-singlefile) into dist/panel.html alongside this compiled file.
31
31
  const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
32
32
  const PANEL_URI = "ui://social-autoposter/panel.html";
33
+ // Stable id for the accumulating draft review queue. submit_drafts appends each
34
+ // run's drafts here (dedup by tweet URL) so the menu-bar cards PILE UP across a
35
+ // continuous autopilot instead of each run overwriting the last; post_drafts posts
36
+ // the approved subset and marks them posted (filtered out of the cards thereafter).
37
+ const REVIEW_QUEUE_ID = "review-queue";
33
38
  const TWITTER_AUTOPILOT_LABEL = "com.m13v.social-twitter-cycle";
34
39
  const TWITTER_AUTOPILOT_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${TWITTER_AUTOPILOT_LABEL}.plist`);
35
40
  // Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
@@ -152,8 +157,9 @@ const server = new McpServer({
152
157
  "ONBOARDING IS A TERMINAL GOAL. When the user asks to set up, install, configure, or onboard " +
153
158
  "social-autoposter, keep taking the next safe action until the owned runtime is ready, a " +
154
159
  "project is fully configured with seeded search topics, X is connected with its real handle, " +
155
- "and the draft path (`scan_candidates` -> draft -> `submit_drafts`) has verified the pipeline " +
156
- "without posting. Do not ask whether to inspect " +
160
+ "the draft path (`scan_candidates` -> draft -> `submit_drafts`) has verified the pipeline " +
161
+ "without posting, AND the recurring autopilot scheduled task has been created and run once " +
162
+ "(see SCHEDULE THE AUTOPILOT below). Do not ask whether to inspect " +
157
163
  "status, install or repair owned dependencies, choose an auto-detected browser profile, connect " +
158
164
  "X, scan the profile, research the product website, save conservative inferred fields, seed " +
159
165
  "topics, retry a recoverable failure, or run draft-only verification. Do those things. The " +
@@ -161,12 +167,24 @@ const server = new McpServer({
161
167
  "twitter.com session cookies into the managed browser: warn that macOS keychain prompts may " +
162
168
  "appear, then proceed without a separate yes/no turn. Ask at most one bundled question, only " +
163
169
  "when no product can be identified from config, context, the X profile, or public research, " +
164
- "or pause for an unavoidable interactive login or OS prompt. Never post drafts or enable " +
165
- "autopilot during setup unless the user explicitly requested that. When setup reaches done " +
166
- "runtime ready, a project configured with seeded topics, X connected, and the draft path " +
167
- "(scan_candidates -> draft -> submit_drafts) verified call the `dashboard` tool so the user " +
170
+ "or pause for an unavoidable interactive login or OS prompt. Never auto-POST during setup, but " +
171
+ "DO schedule the draft-only autopilot as the final step (it only queues drafts for approval, it " +
172
+ "never posts on its own). When setup reaches done (" +
173
+ "runtime ready, a project configured with seeded topics, X connected, the draft path verified, " +
174
+ "and the autopilot scheduled task created and run once), call the `dashboard` tool so the user " +
168
175
  "sees the finished setup rendered, then give " +
169
176
  "the completion summary.\n\n" +
177
+ "SCHEDULE THE AUTOPILOT (final onboarding step). Once the draft path is verified, start the " +
178
+ "autopilot by calling the HOST tool `create_scheduled_task` with: taskId " +
179
+ "'social-autoposter-autopilot'; cronExpression '* * * * *' (continuous, like the pipeline; the " +
180
+ "host runs one at a time and skips overlapping fires, so cycles run back-to-back); and a " +
181
+ "self-contained prompt telling the run to call scan_candidates, draft ONE on-brand reply " +
182
+ "(<=250 chars, same language as the thread, adds real value) for each candidate worth engaging, " +
183
+ "then call submit_drafts with those drafts, and NOT post (drafts pile up in the menu-bar " +
184
+ "approval cards for the user to approve). Then run ONE cycle yourself now (scan_candidates -> " +
185
+ "draft -> submit_drafts) to seed the cards and pre-approve the tools, so unattended fires do not " +
186
+ "stall on a permission prompt. If the host exposes no create_scheduled_task tool, tell the user " +
187
+ "the autopilot could not be scheduled here and to run a draft cycle manually instead.\n\n" +
170
188
  "BE PROACTIVE ABOUT MARKETING MOMENTS. Whenever the user ships, finishes, merges, or releases " +
171
189
  "something worth talking about in this session (a new feature, a launch, a long-awaited fix, a " +
172
190
  "milestone), don't wait to be asked: offer to tell the world by running a draft cycle " +
@@ -469,8 +487,11 @@ async function produceDrafts(project, onProgress) {
469
487
  function renderDraftsTable(plan) {
470
488
  const candidates = plan.candidates || [];
471
489
  return candidates
472
- .map((c, i) => {
473
- const n = i + 1;
490
+ // Number by FULL-array index (matches post_drafts + the menu bar), then drop
491
+ // already-posted entries so the cards only show what's still pending.
492
+ .map((c, i) => ({ c, n: i + 1 }))
493
+ .filter((e) => e.c.posted !== true)
494
+ .map(({ c, n }) => {
474
495
  const author = c.thread_author ? `@${c.thread_author}` : "(unknown thread)";
475
496
  const style = c.engagement_style ?? "?";
476
497
  const reply = c.reply_text ?? "(empty)";
@@ -922,7 +943,7 @@ tool("project_config", {
922
943
  (x.connected ? "" : " X is not connected yet either — detect_x_sources, warn about keychain prompts, then run connect_x with confirm:true without a separate permission turn.")
923
944
  : projects.every((p) => p.ready)
924
945
  ? (x.connected
925
- ? "All configured projects are ready and X is connected. Run scan_candidates, draft a reply or two, and submit_drafts now to verify end to end without posting. After it verifies, call the `dashboard` tool so the user sees the finished setup."
946
+ ? "All configured projects are ready and X is connected. Run scan_candidates, draft a reply or two, and submit_drafts now to verify end to end without posting. Then SCHEDULE THE AUTOPILOT: call create_scheduled_task (taskId 'social-autoposter-autopilot', cron '* * * * *', prompt = scan_candidates -> draft -> submit_drafts, draft-only) and run one cycle now. Then call the `dashboard` tool so the user sees the finished setup."
926
947
  : "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
927
948
  "x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
928
949
  : "Some projects are missing required fields (see each project's missing_required). Derive them from config, context, profile_scan, and website research, then call project_config again. Ask only if a required field is genuinely unknowable." +
@@ -1034,8 +1055,9 @@ tool("project_config", {
1034
1055
  note: (result.ready
1035
1056
  ? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
1036
1057
  `detect sources, warn about keychain prompts, and call project_config with ` +
1037
- `action:'connect_x', confirm:true immediately. Once X is connected, run scan_candidates -> submit_drafts to` +
1038
- `verify without posting. Do not enable autopilot unless explicitly requested.`
1058
+ `action:'connect_x', confirm:true immediately. Once X is connected, run scan_candidates -> submit_drafts ` +
1059
+ `to verify without posting, then schedule the draft-only autopilot (create_scheduled_task, cron '* * * * *', ` +
1060
+ `prompt = scan_candidates -> draft -> submit_drafts) and run one cycle now.`
1039
1061
  : `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
1040
1062
  `First derive those fields from existing context, profile_scan, and website research, then ` +
1041
1063
  `call project_config again with name='${result.project}'. Ask only if a required field is genuinely unknowable.`) +
@@ -1719,11 +1741,10 @@ tool("submit_drafts", {
1719
1741
  const project = (candidates.map((c) => c.matched_project).find((p) => !!p) ||
1720
1742
  firstSc?.matched_project ||
1721
1743
  "default");
1722
- const plan = { candidates };
1723
- writePlan(args.batch_id, plan);
1724
- // Bake link targets into the plan (sub-second at TWITTER_PAGE_GEN_RATE=0), the
1725
- // same prep step DRAFT_ONLY does before handing off. Best-effort: posting
1726
- // falls back to the plain project URL per-candidate if this is skipped.
1744
+ // Stage the new drafts under the scan batch id and bake link targets into them
1745
+ // (sub-second at TWITTER_PAGE_GEN_RATE=0). Best-effort: posting falls back to the
1746
+ // plain project URL per-candidate if gen is skipped.
1747
+ writePlan(args.batch_id, { candidates });
1727
1748
  try {
1728
1749
  await runPython("scripts/twitter_gen_links.py", ["--plan", planPath(args.batch_id)], {
1729
1750
  timeoutMs: 120_000,
@@ -1733,25 +1754,45 @@ tool("submit_drafts", {
1733
1754
  catch {
1734
1755
  /* best effort — plan still posts with a plain-URL fallback */
1735
1756
  }
1736
- const finalPlan = readPlan(args.batch_id) ?? plan;
1737
- const count = (finalPlan.candidates || []).length;
1757
+ const staged = readPlan(args.batch_id)?.candidates ?? candidates;
1758
+ // Accumulate into ONE persistent review queue so a continuous autopilot's drafts
1759
+ // PILE UP in the menu-bar cards instead of each run overwriting the last. New
1760
+ // drafts are appended; a thread already in the queue (by URL) is skipped (one
1761
+ // draft per thread). Posted entries are KEPT in place so the 1-based card
1762
+ // numbering stays stable across runs — the menu bar, the chat table, and
1763
+ // post_drafts all index the full array and filter on the `posted` flag.
1764
+ const queue = [
1765
+ ...(readPlan(REVIEW_QUEUE_ID)?.candidates ?? []),
1766
+ ];
1767
+ const seen = new Set(queue.map((c) => c.candidate_url).filter((u) => !!u));
1768
+ let added = 0;
1769
+ for (const nc of staged) {
1770
+ if (nc.candidate_url && seen.has(nc.candidate_url))
1771
+ continue;
1772
+ queue.push(nc);
1773
+ if (nc.candidate_url)
1774
+ seen.add(nc.candidate_url);
1775
+ added++;
1776
+ }
1777
+ writePlan(REVIEW_QUEUE_ID, { candidates: queue });
1778
+ const pending = queue.filter((c) => c.posted !== true);
1738
1779
  // Drafts queued = the pipeline verified end-to-end without posting. This is the
1739
1780
  // onboarding "draft_verified" terminal goal (formerly completed by draft_cycle).
1740
- if (count > 0)
1741
- completeOnboardingMilestone("draft_verified", { outcome: "review_batch", draft_count: count });
1742
- // Surface to the menu-bar review cards (same review plan draft_cycle used).
1781
+ if (added > 0)
1782
+ completeOnboardingMilestone("draft_verified", { outcome: "review_batch", draft_count: added });
1783
+ // Point the menu-bar review cards at the accumulated queue.
1743
1784
  writeReviewRequest({
1744
- batch_id: args.batch_id,
1785
+ batch_id: REVIEW_QUEUE_ID,
1745
1786
  project,
1746
- count,
1747
- plan_path: planPath(args.batch_id),
1787
+ count: pending.length,
1788
+ plan_path: planPath(REVIEW_QUEUE_ID),
1748
1789
  created_at: new Date().toISOString(),
1749
1790
  });
1750
- return textContent(`${count} draft(s) queued for review (batch ${args.batch_id}). They're now in the menu-bar ` +
1751
- `approval cards and the table below; nothing posts until approved.\n\n` +
1752
- renderDraftsTable(finalPlan) +
1753
- `\n\nTo post the approved ones: the user approves in the menu bar, or call post_drafts with the ` +
1754
- `numbers to post.`);
1791
+ return textContent(`Queued ${added} new draft(s); ${pending.length} now awaiting approval in the menu-bar cards ` +
1792
+ `(review queue "${REVIEW_QUEUE_ID}"). Nothing posts until approved.\n\n` +
1793
+ renderDraftsTable({ candidates: queue }) +
1794
+ `\n\nTo post: the user approves in the menu bar, or call post_drafts with batch_id ` +
1795
+ `"${REVIEW_QUEUE_ID}" and the numbers to post.`);
1755
1796
  });
1756
1797
  appTool("dashboard", {
1757
1798
  title: "Social Autoposter dashboard",
@@ -2,10 +2,12 @@
2
2
  //
3
3
  // The "real" version is the top-level `social-autoposter` npm package version
4
4
  // (e.g. 1.6.x) — that is what actually bundles this MCP's prebuilt dist/. The
5
- // MCP's own package.json (0.0.1) and manifest (0.0.1) are decoupled artifacts
6
- // that nobody bumps, so they are NOT a reliable signal. This module resolves the
7
- // true version from the most authoritative source available at runtime, and can
8
- // check npm for a newer published release so we can deliver updates on demand.
5
+ // MCP's own package.json and manifest are stamped to the same version at release
6
+ // time (scripts/release-mcpb.sh step 3b), but historically they were frozen at
7
+ // 0.0.1, so this module still resolves the true version from the most
8
+ // authoritative source available at runtime (and tolerates a stale co-located
9
+ // package.json on an old bundle). It can also check npm for a newer published
10
+ // release so we can deliver updates on demand.
9
11
  import fs from "node:fs";
10
12
  import path from "node:path";
11
13
  import { fileURLToPath } from "node:url";
@@ -26,7 +28,8 @@ function readJsonVersion(p) {
26
28
  // from the npm package version at every init/update. Authoritative on a
27
29
  // real user install, where the top-level package.json is NOT copied.
28
30
  // 2. <repo>/package.json — git checkout / dev machine: the meaningful 1.6.x.
29
- // 3. mcp/package.json — co-located last resort (currently 0.0.1).
31
+ // 3. mcp/package.json — co-located last resort (release-stamped to match,
32
+ // but may be stale on an older bundle).
30
33
  export function resolveVersion() {
31
34
  return (readJsonVersion(path.join(__dirname, "version.json")) ||
32
35
  readJsonVersion(path.join(repoDir(), "package.json")) ||
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.6.71",
3
- "installedAt": "2026-06-20T16:29:20.789Z"
2
+ "version": "1.6.72",
3
+ "installedAt": "2026-06-20T17:17:17.565Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "Social Autoposter",
5
- "version": "0.0.1",
5
+ "version": "1.6.72",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts. Thin desktop client over the social-autoposter pipeline.",
7
7
  "long_description": "A guided assistant that drafts, reviews, and autopilots X/Twitter posts.\nTo get started:\n1. Fully quit and restart Claude (quit and reopen, not just close the window).\n2. Choose **Set up social-autoposter** from the prompt menu or type **Set me up on social-autoposter end to end**. The agent installs the owned runtime, connects X, discovers and configures your product, and verifies with a draft-only cycle.",
8
8
  "author": {
@@ -15,7 +15,9 @@
15
15
  "entry_point": "dist/index.js",
16
16
  "mcp_config": {
17
17
  "command": "node",
18
- "args": ["${__dirname}/dist/index.js"],
18
+ "args": [
19
+ "${__dirname}/dist/index.js"
20
+ ],
19
21
  "env": {
20
22
  "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
21
23
  }
@@ -43,7 +45,13 @@
43
45
  "description": "Render the visual dashboard (MCP Apps UI): project setup, X connection, autopilot state, and 7-day stats, with buttons to run a draft cycle, connect X, and refresh. Also rendered automatically after state-changing actions."
44
46
  }
45
47
  ],
46
- "keywords": ["twitter", "x", "social", "autoposter", "marketing"],
48
+ "keywords": [
49
+ "twitter",
50
+ "x",
51
+ "social",
52
+ "autoposter",
53
+ "marketing"
54
+ ],
47
55
  "license": "UNLICENSED",
48
56
  "repository": {
49
57
  "type": "git",
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "0.0.1",
3
+ "version": "1.6.72",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-autoposter",
3
- "version": "1.6.72",
3
+ "version": "1.6.73",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js"
@@ -75,6 +75,31 @@ fs.writeFileSync(p, JSON.stringify({version:'$VERSION',installedAt:new Date().to
75
75
  console.log(' '+fs.readFileSync(p,'utf8').trim());
76
76
  "
77
77
 
78
+ # ---- 3b. Stamp manifest.json + mcp/package.json + lockfile version ----------
79
+ # Claude Desktop's extension "Details" panel reads version from manifest.json,
80
+ # so it must track the release version too (not the frozen 0.0.1 placeholder).
81
+ # mcp/package.json and its lockfile are stamped in lockstep so the three stay
82
+ # consistent (npm errors if package.json and package-lock.json disagree).
83
+ say "Stamping mcp/manifest.json + mcp/package.json + mcp/package-lock.json -> $VERSION"
84
+ node -e "
85
+ const fs=require('fs');
86
+ const V='$VERSION';
87
+ for (const p of ['$MCP_DIR/manifest.json','$MCP_DIR/package.json']) {
88
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
89
+ j.version=V;
90
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
91
+ console.log(' '+p.replace('$MCP_DIR/','mcp/')+' -> '+j.version);
92
+ }
93
+ const lp='$MCP_DIR/package-lock.json';
94
+ if (fs.existsSync(lp)) {
95
+ const l=JSON.parse(fs.readFileSync(lp,'utf8'));
96
+ l.version=V;
97
+ if (l.packages && l.packages['']) l.packages[''].version=V;
98
+ fs.writeFileSync(lp, JSON.stringify(l,null,2)+'\n');
99
+ console.log(' mcp/package-lock.json -> '+l.version);
100
+ }
101
+ "
102
+
78
103
  # ---- 4. Pack the .mcpb ------------------------------------------------------
79
104
  say "Packing $BUNDLE"
80
105
  rm -f "$BUNDLE"
@@ -99,6 +124,10 @@ BUNDLE_VER=$(unzip -p "$BUNDLE" dist/version.json 2>/dev/null | node -p "JSON.pa
99
124
  [[ "$BUNDLE_VER" == "$VERSION" ]] || die "bundle version.json=$BUNDLE_VER != $VERSION"
100
125
  echo " version.json: $BUNDLE_VER ok"
101
126
 
127
+ MANIFEST_VER=$(unzip -p "$BUNDLE" manifest.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
128
+ [[ "$MANIFEST_VER" == "$VERSION" ]] || die "bundle manifest.json=$MANIFEST_VER != $VERSION (Desktop Details panel would show the wrong version)"
129
+ echo " manifest.json: $MANIFEST_VER ok"
130
+
102
131
  for f in "dist/index.js" "dist/runtime.js" "manifest.json"; do
103
132
  # grep -c reads all input (no SIGPIPE); anchor on the time column + 3-space
104
133
  # gutter so node_modules/.../dist/index.js does not false-match the top-level.