social-autoposter 1.6.72 → 1.6.74

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,15 @@ 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";
38
+ // The Desktop scheduled task onboarding creates for the autopilot. Its presence on
39
+ // disk is the single "autopilot is set up" signal the dashboard + menu bar share
40
+ // (the legacy launchd autopilot is retired).
41
+ const AUTOPILOT_TASK_ID = "social-autoposter-autopilot";
33
42
  const TWITTER_AUTOPILOT_LABEL = "com.m13v.social-twitter-cycle";
34
43
  const TWITTER_AUTOPILOT_PLIST = path.join(os.homedir(), "Library", "LaunchAgents", `${TWITTER_AUTOPILOT_LABEL}.plist`);
35
44
  // Daily self-updater. Enabled alongside autopilot so a hands-free (headless)
@@ -152,8 +161,9 @@ const server = new McpServer({
152
161
  "ONBOARDING IS A TERMINAL GOAL. When the user asks to set up, install, configure, or onboard " +
153
162
  "social-autoposter, keep taking the next safe action until the owned runtime is ready, a " +
154
163
  "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 " +
164
+ "the draft path (`scan_candidates` -> draft -> `submit_drafts`) has verified the pipeline " +
165
+ "without posting, AND the recurring autopilot scheduled task has been created and run once " +
166
+ "(see SCHEDULE THE AUTOPILOT below). Do not ask whether to inspect " +
157
167
  "status, install or repair owned dependencies, choose an auto-detected browser profile, connect " +
158
168
  "X, scan the profile, research the product website, save conservative inferred fields, seed " +
159
169
  "topics, retry a recoverable failure, or run draft-only verification. Do those things. The " +
@@ -161,12 +171,24 @@ const server = new McpServer({
161
171
  "twitter.com session cookies into the managed browser: warn that macOS keychain prompts may " +
162
172
  "appear, then proceed without a separate yes/no turn. Ask at most one bundled question, only " +
163
173
  "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 " +
174
+ "or pause for an unavoidable interactive login or OS prompt. Never auto-POST during setup, but " +
175
+ "DO schedule the draft-only autopilot as the final step (it only queues drafts for approval, it " +
176
+ "never posts on its own). When setup reaches done (" +
177
+ "runtime ready, a project configured with seeded topics, X connected, the draft path verified, " +
178
+ "and the autopilot scheduled task created and run once), call the `dashboard` tool so the user " +
168
179
  "sees the finished setup rendered, then give " +
169
180
  "the completion summary.\n\n" +
181
+ "SCHEDULE THE AUTOPILOT (final onboarding step). Once the draft path is verified, start the " +
182
+ "autopilot by calling the HOST tool `create_scheduled_task` with: taskId " +
183
+ "'social-autoposter-autopilot'; cronExpression '* * * * *' (continuous, like the pipeline; the " +
184
+ "host runs one at a time and skips overlapping fires, so cycles run back-to-back); and a " +
185
+ "self-contained prompt telling the run to call scan_candidates, draft ONE on-brand reply " +
186
+ "(<=250 chars, same language as the thread, adds real value) for each candidate worth engaging, " +
187
+ "then call submit_drafts with those drafts, and NOT post (drafts pile up in the menu-bar " +
188
+ "approval cards for the user to approve). Then run ONE cycle yourself now (scan_candidates -> " +
189
+ "draft -> submit_drafts) to seed the cards and pre-approve the tools, so unattended fires do not " +
190
+ "stall on a permission prompt. If the host exposes no create_scheduled_task tool, tell the user " +
191
+ "the autopilot could not be scheduled here and to run a draft cycle manually instead.\n\n" +
170
192
  "BE PROACTIVE ABOUT MARKETING MOMENTS. Whenever the user ships, finishes, merges, or releases " +
171
193
  "something worth talking about in this session (a new feature, a launch, a long-awaited fix, a " +
172
194
  "milestone), don't wait to be asked: offer to tell the world by running a draft cycle " +
@@ -469,8 +491,11 @@ async function produceDrafts(project, onProgress) {
469
491
  function renderDraftsTable(plan) {
470
492
  const candidates = plan.candidates || [];
471
493
  return candidates
472
- .map((c, i) => {
473
- const n = i + 1;
494
+ // Number by FULL-array index (matches post_drafts + the menu bar), then drop
495
+ // already-posted entries so the cards only show what's still pending.
496
+ .map((c, i) => ({ c, n: i + 1 }))
497
+ .filter((e) => e.c.posted !== true)
498
+ .map(({ c, n }) => {
474
499
  const author = c.thread_author ? `@${c.thread_author}` : "(unknown thread)";
475
500
  const style = c.engagement_style ?? "?";
476
501
  const reply = c.reply_text ?? "(empty)";
@@ -922,7 +947,7 @@ tool("project_config", {
922
947
  (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
948
  : projects.every((p) => p.ready)
924
949
  ? (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."
950
+ ? "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
951
  : "All configured projects are ready, but X is NOT connected — posting needs a logged-in " +
927
952
  "x.com session. Detect sources and run project_config action:'connect_x', confirm:true; do not ask whether to proceed.")
928
953
  : "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 +1059,9 @@ tool("project_config", {
1034
1059
  note: (result.ready
1035
1060
  ? `Project '${result.project}' is fully configured.${seedNote} Next: if X is not connected, ` +
1036
1061
  `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.`
1062
+ `action:'connect_x', confirm:true immediately. Once X is connected, run scan_candidates -> submit_drafts ` +
1063
+ `to verify without posting, then schedule the draft-only autopilot (create_scheduled_task, cron '* * * * *', ` +
1064
+ `prompt = scan_candidates -> draft -> submit_drafts) and run one cycle now.`
1039
1065
  : `Saved what you provided for '${result.project}'. Still need: ${result.missing_required.join(", ")}. ` +
1040
1066
  `First derive those fields from existing context, profile_scan, and website research, then ` +
1041
1067
  `call project_config again with name='${result.project}'. Ask only if a required field is genuinely unknowable.`) +
@@ -1332,18 +1358,28 @@ function runtimeSnapshot() {
1332
1358
  // and re-reads status. The tool itself returns the first-paint snapshot so the
1333
1359
  // view has data the instant it loads.
1334
1360
  // Is either launchd job (cycle / daily updater) currently loaded?
1361
+ // "Autopilot" is now the Claude Desktop scheduled task `social-autoposter-autopilot`
1362
+ // (created during onboarding via create_scheduled_task), NOT the legacy launchd job.
1363
+ // We can't read the host's enabled/paused flag, but the task's presence on disk is the
1364
+ // single signal the dashboard AND the menu bar key off of, so they stay aligned.
1335
1365
  async function autopilotLoaded() {
1366
+ let autopilot_on = false;
1367
+ try {
1368
+ const cfg = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
1369
+ autopilot_on = fs.existsSync(path.join(cfg, "scheduled-tasks", AUTOPILOT_TASK_ID, "SKILL.md"));
1370
+ }
1371
+ catch {
1372
+ /* leave false */
1373
+ }
1374
+ let auto_update_on = false;
1336
1375
  try {
1337
1376
  const res = await run("launchctl", ["list"], { timeoutMs: 10_000 });
1338
- const lines = res.stdout.split("\n");
1339
- return {
1340
- autopilot_on: lines.some((l) => l.includes(TWITTER_AUTOPILOT_LABEL)),
1341
- auto_update_on: lines.some((l) => l.includes(UPDATER_LABEL)),
1342
- };
1377
+ auto_update_on = res.stdout.split("\n").some((l) => l.includes(UPDATER_LABEL));
1343
1378
  }
1344
1379
  catch {
1345
- return { autopilot_on: false, auto_update_on: false };
1380
+ /* leave false */
1346
1381
  }
1382
+ return { autopilot_on, auto_update_on };
1347
1383
  }
1348
1384
  // Assemble everything the panel needs in one shot (projects + X + autopilot +
1349
1385
  // version). Resilient: any probe that throws degrades to a safe default rather
@@ -1719,11 +1755,10 @@ tool("submit_drafts", {
1719
1755
  const project = (candidates.map((c) => c.matched_project).find((p) => !!p) ||
1720
1756
  firstSc?.matched_project ||
1721
1757
  "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.
1758
+ // Stage the new drafts under the scan batch id and bake link targets into them
1759
+ // (sub-second at TWITTER_PAGE_GEN_RATE=0). Best-effort: posting falls back to the
1760
+ // plain project URL per-candidate if gen is skipped.
1761
+ writePlan(args.batch_id, { candidates });
1727
1762
  try {
1728
1763
  await runPython("scripts/twitter_gen_links.py", ["--plan", planPath(args.batch_id)], {
1729
1764
  timeoutMs: 120_000,
@@ -1733,25 +1768,45 @@ tool("submit_drafts", {
1733
1768
  catch {
1734
1769
  /* best effort — plan still posts with a plain-URL fallback */
1735
1770
  }
1736
- const finalPlan = readPlan(args.batch_id) ?? plan;
1737
- const count = (finalPlan.candidates || []).length;
1771
+ const staged = readPlan(args.batch_id)?.candidates ?? candidates;
1772
+ // Accumulate into ONE persistent review queue so a continuous autopilot's drafts
1773
+ // PILE UP in the menu-bar cards instead of each run overwriting the last. New
1774
+ // drafts are appended; a thread already in the queue (by URL) is skipped (one
1775
+ // draft per thread). Posted entries are KEPT in place so the 1-based card
1776
+ // numbering stays stable across runs — the menu bar, the chat table, and
1777
+ // post_drafts all index the full array and filter on the `posted` flag.
1778
+ const queue = [
1779
+ ...(readPlan(REVIEW_QUEUE_ID)?.candidates ?? []),
1780
+ ];
1781
+ const seen = new Set(queue.map((c) => c.candidate_url).filter((u) => !!u));
1782
+ let added = 0;
1783
+ for (const nc of staged) {
1784
+ if (nc.candidate_url && seen.has(nc.candidate_url))
1785
+ continue;
1786
+ queue.push(nc);
1787
+ if (nc.candidate_url)
1788
+ seen.add(nc.candidate_url);
1789
+ added++;
1790
+ }
1791
+ writePlan(REVIEW_QUEUE_ID, { candidates: queue });
1792
+ const pending = queue.filter((c) => c.posted !== true);
1738
1793
  // Drafts queued = the pipeline verified end-to-end without posting. This is the
1739
1794
  // 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).
1795
+ if (added > 0)
1796
+ completeOnboardingMilestone("draft_verified", { outcome: "review_batch", draft_count: added });
1797
+ // Point the menu-bar review cards at the accumulated queue.
1743
1798
  writeReviewRequest({
1744
- batch_id: args.batch_id,
1799
+ batch_id: REVIEW_QUEUE_ID,
1745
1800
  project,
1746
- count,
1747
- plan_path: planPath(args.batch_id),
1801
+ count: pending.length,
1802
+ plan_path: planPath(REVIEW_QUEUE_ID),
1748
1803
  created_at: new Date().toISOString(),
1749
1804
  });
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.`);
1805
+ return textContent(`Queued ${added} new draft(s); ${pending.length} now awaiting approval in the menu-bar cards ` +
1806
+ `(review queue "${REVIEW_QUEUE_ID}"). Nothing posts until approved.\n\n` +
1807
+ renderDraftsTable({ candidates: queue }) +
1808
+ `\n\nTo post: the user approves in the menu bar, or call post_drafts with batch_id ` +
1809
+ `"${REVIEW_QUEUE_ID}" and the numbers to post.`);
1755
1810
  });
1756
1811
  appTool("dashboard", {
1757
1812
  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.73",
3
+ "installedAt": "2026-06-20T18:16:22.534Z"
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.73",
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",
@@ -191,12 +191,8 @@ class S4LMenuBar(rumps.App):
191
191
  except Exception:
192
192
  pass
193
193
 
194
- def _toggle_ap(self, sender):
195
- desired = not bool(sender.state)
196
- if st.set_autopilot(desired):
197
- sender.state = 1 if desired else 0
198
- else:
199
- self._notify("S4L", "Open Claude Desktop to change autopilot.")
194
+ # _toggle_ap removed: autopilot is the Claude Desktop scheduled task now, managed
195
+ # in the Scheduled tab. The menu bar mirrors the dashboard (no launchd toggle).
200
196
 
201
197
  # ---- activity spinner -------------------------------------------------
202
198
  # The server writes activity.json while a tool runs (scanning/drafting/
@@ -469,9 +465,6 @@ class S4LMenuBar(rumps.App):
469
465
  out.append(self._label("7d stats — open dashboard"))
470
466
 
471
467
  out.append(rumps.separator)
472
- ap = rumps.MenuItem("Autopilot", callback=self._toggle_ap)
473
- ap.state = 1 if snap.get("autopilot_on") else 0
474
- out.append(ap)
475
468
  out.append(
476
469
  rumps.MenuItem("Run draft cycle in Claude", callback=self._draft)
477
470
  )
@@ -103,7 +103,11 @@ def _launchctl_list() -> str:
103
103
 
104
104
 
105
105
  def autopilot_loaded() -> bool:
106
- return AUTOPILOT_LABEL in _launchctl_list()
106
+ # Autopilot is now the Claude Desktop scheduled task, not the legacy launchd job.
107
+ cfg = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.join(str(Path.home()), ".claude")
108
+ return os.path.exists(
109
+ os.path.join(cfg, "scheduled-tasks", "social-autoposter-autopilot", "SKILL.md")
110
+ )
107
111
 
108
112
 
109
113
  # ---- loopback panel server (live, when Claude Desktop is running) ----------
@@ -207,35 +211,9 @@ def stats_7d():
207
211
  }
208
212
 
209
213
 
210
- def set_autopilot(enable: bool) -> bool:
211
- """Toggle background posting. Prefer the loopback tool (it creates the plist
212
- correctly on first enable); fall back to launchctl against the existing
213
- plist when Claude Desktop is closed. Returns False if it couldn't act."""
214
- res = loopback_tool("autopilot", {"action": "enable" if enable else "disable"})
215
- if res is not None:
216
- return True
217
- plist = str(
218
- Path.home() / "Library" / "LaunchAgents" / (AUTOPILOT_LABEL + ".plist")
219
- )
220
- uid = os.getuid()
221
- try:
222
- if enable:
223
- if not os.path.exists(plist):
224
- return False # first enable needs the server to write the plist
225
- subprocess.run(
226
- ["launchctl", "bootstrap", f"gui/{uid}", plist],
227
- capture_output=True,
228
- timeout=15,
229
- )
230
- else:
231
- subprocess.run(
232
- ["launchctl", "bootout", f"gui/{uid}/{AUTOPILOT_LABEL}"],
233
- capture_output=True,
234
- timeout=15,
235
- )
236
- return True
237
- except Exception:
238
- return False
214
+ # set_autopilot() (the launchd toggle) was removed: the autopilot is now the Claude
215
+ # Desktop scheduled task `social-autoposter-autopilot`, managed in the Scheduled tab,
216
+ # so the menu bar no longer toggles a launchd job (it mirrors the dashboard instead).
239
217
 
240
218
 
241
219
  def panel_url():
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.73",
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.74",
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.