@cmo-so/mcp 0.12.13 → 0.12.15

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.
Files changed (2) hide show
  1. package/dist/cmo-mcp.mjs +89 -18
  2. package/package.json +1 -1
package/dist/cmo-mcp.mjs CHANGED
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
7199
7199
  // package.json
7200
7200
  var package_default = {
7201
7201
  name: "@cmo-so/cli",
7202
- version: "0.12.13",
7202
+ version: "0.12.15",
7203
7203
  description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
7204
7204
  type: "module",
7205
7205
  license: "MIT",
@@ -15165,6 +15165,47 @@ async function partSet(site, slug, change) {
15165
15165
  counts.social = (counts.social ?? 0) + links2.length;
15166
15166
  }
15167
15167
  }
15168
+ if (change.footer_links && slug === "footer") {
15169
+ const isRow = (b) => String(b.attrs.className ?? "").split(/\s+/).includes("maggie-footer-links");
15170
+ const items3 = change.footer_links.filter((l) => l?.label && l?.url).slice(0, 20).map(
15171
+ (l) => `<!-- wp:paragraph {"fontSize":"small"} -->
15172
+ <p class="has-small-font-size"><a href="${escAttr(l.url)}">${escAttr(l.label)}</a></p>
15173
+ <!-- /wp:paragraph -->`
15174
+ ).join("\n\n");
15175
+ const row = items3 ? blockOf(
15176
+ `<!-- wp:group {"className":"maggie-footer-links","style":{"spacing":{"margin":{"bottom":"var:preset|spacing|40"}}},"layout":{"type":"flex","flexWrap":"wrap"}} -->
15177
+ <div class="wp-block-group maggie-footer-links" style="margin-bottom:var(--wp--preset--spacing--40)">${items3}</div>
15178
+ <!-- /wp:group -->`
15179
+ ) : null;
15180
+ const old = find(blocks, isRow);
15181
+ if (old) {
15182
+ const top = blocks.indexOf(old);
15183
+ if (top >= 0) blocks.splice(top, 1, ...row ? [row] : []);
15184
+ else {
15185
+ const parent = find(blocks, (b) => b.inner.includes(old));
15186
+ const k = parent.inner.indexOf(old);
15187
+ if (row) parent.inner[k] = row;
15188
+ else {
15189
+ parent.inner.splice(k, 1);
15190
+ let seen = -1;
15191
+ const at = parent.content.findIndex((c) => c === null && ++seen === k);
15192
+ parent.content.splice(at, parent.content[at + 1] === "\n\n" ? 2 : 1);
15193
+ }
15194
+ touchPath(blocks, parent);
15195
+ }
15196
+ } else if (row) {
15197
+ const outer = blocks.find((b) => b.name === "core/group");
15198
+ if (outer?.inner.length) {
15199
+ const k = outer.inner.length - 1;
15200
+ outer.inner.splice(k, 0, row);
15201
+ let seen = -1;
15202
+ const at = outer.content.findIndex((c) => c === null && ++seen === k);
15203
+ outer.content.splice(at, 0, null, "\n\n");
15204
+ touchPath(blocks, outer);
15205
+ } else blocks.push(row);
15206
+ } else throw new Error("footer_links: give at least one { label, url }.");
15207
+ counts.footer_links = change.footer_links.length;
15208
+ }
15168
15209
  const content = serialize(blocks);
15169
15210
  if (content === p.content) return { slug, changed: false, counts };
15170
15211
  await call(site, { method: "POST", path: `/wp/v2/template-parts/${p.id}`, body: { content } });
@@ -17316,6 +17357,11 @@ async function linksPlan(site, o = {}) {
17316
17357
  );
17317
17358
  const footer = await partGet(site, "footer").catch(() => null);
17318
17359
  const footerPaths = footer ? contentLinks(footer.content, base) : /* @__PURE__ */ new Set();
17360
+ const rowHtml = /maggie-footer-links[^>]*>([\s\S]*?)<\/div>\s*<!-- \/wp:group -->/.exec(footer?.content ?? "")?.[1] ?? "";
17361
+ const footerRow = [...rowHtml.matchAll(/<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)].map((m) => ({
17362
+ url: m[1],
17363
+ label: strip(m[2])
17364
+ }));
17319
17365
  const header = await partGet(site, "header").catch(() => null);
17320
17366
  const headerPaths = header ? contentLinks(header.content, base) : /* @__PURE__ */ new Set();
17321
17367
  const chrome = /* @__PURE__ */ new Set([...menuPaths, ...footerPaths, ...headerPaths]);
@@ -17378,14 +17424,16 @@ async function linksPlan(site, o = {}) {
17378
17424
  type: "menu",
17379
17425
  why: "Main pages every visitor should reach in one click.",
17380
17426
  add: wantMenu.slice(0, 6).map((d) => ({ label: d.title, url: d.url })),
17381
- tool: "wp_menu_set"
17427
+ tool: "wp_menu_set",
17428
+ args: { add: wantMenu.slice(0, 6).map((d) => ({ label: d.title, url: d.path })) }
17382
17429
  });
17383
17430
  if (postDocs.length && blogPage && !chrome.has(blogPage.path))
17384
17431
  actions.push({
17385
17432
  type: "menu",
17386
17433
  why: "The blog isn't reachable from the menu.",
17387
17434
  add: [{ label: blogPage.title || "Blog", url: blogPage.url }],
17388
- tool: "wp_menu_set"
17435
+ tool: "wp_menu_set",
17436
+ args: { add: [{ label: blogPage.title || "Blog", url: blogPage.path }] }
17389
17437
  });
17390
17438
  const legal = pageDocs.filter(
17391
17439
  (d) => /privacy|terms|cookie|legal|impressum|datenschutz|accessib/i.test(`${d.path} ${d.title}`)
@@ -17397,7 +17445,14 @@ async function linksPlan(site, o = {}) {
17397
17445
  type: "footer",
17398
17446
  why: "Legal, contact and about pages belong in the footer of every page (trust, and Google's quality raters look for them).",
17399
17447
  add: footerMissing.slice(0, 8).map((d) => ({ label: d.title, url: d.url })),
17400
- tool: "wp_footer_set"
17448
+ tool: "wp_footer_set",
17449
+ // The footer's own row (if this tool made one) plus the missing pages, ready for wp_footer_set.
17450
+ args: {
17451
+ footer_links: [
17452
+ ...footerRow,
17453
+ ...footerMissing.slice(0, 8).filter((d) => !footerRow.some((r) => sitePath(r.url, base) === d.path)).map((d) => ({ label: d.title, url: d.path }))
17454
+ ]
17455
+ }
17401
17456
  });
17402
17457
  for (const hub of hubs) {
17403
17458
  const kids = pageDocs.filter((c) => c.parent === hub.id);
@@ -17520,8 +17575,8 @@ function relatedBlock(heading2, links2) {
17520
17575
  const items3 = links2.map((l) => `<!-- wp:list-item -->
17521
17576
  <li><a href="${esc(l.url)}">${esc(l.label)}</a></li>
17522
17577
  <!-- /wp:list-item -->`).join("\n");
17523
- return `<!-- wp:group {"metadata":{"name":"Related links","maggie":{"type":"related-links","version":1}},"align":"wide","className":"maggie-related-links","layout":{"type":"constrained"}} -->
17524
- <div class="wp-block-group alignwide maggie-related-links"><!-- wp:heading {"level":2} -->
17578
+ return `<!-- wp:group {"metadata":{"name":"Related links","maggie":{"type":"related-links","version":1}},"align":"wide","className":"maggie-related-links","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60"}}},"layout":{"type":"constrained","contentSize":"var(--wp--style--global--wide-size)"}} -->
17579
+ <div class="wp-block-group alignwide maggie-related-links" style="padding-top:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60)"><!-- wp:heading {"level":2} -->
17525
17580
  <h2 class="wp-block-heading">${esc(heading2)}</h2>
17526
17581
  <!-- /wp:heading -->
17527
17582
 
@@ -17539,7 +17594,7 @@ async function llmsTxt(site, o = {}) {
17539
17594
  const base = (site.url ?? "").replace(/\/+$/, "");
17540
17595
  const s = await site.request({
17541
17596
  path: "/wp/v2/settings",
17542
- query: { _fields: "title,description,page_on_front,page_for_posts" }
17597
+ query: { _fields: "title,description,page_on_front,page_for_posts,blog_public" }
17543
17598
  }).then((r) => r.status === 200 ? r.data : {});
17544
17599
  const pages = await listPages(site, { status: "publish" });
17545
17600
  const posts = await all(
@@ -17548,10 +17603,20 @@ async function llmsTxt(site, o = {}) {
17548
17603
  { status: "publish", per_page: 100, _fields: "link,title,excerpt,meta", orderby: "date", order: "desc" },
17549
17604
  o.posts ?? 25
17550
17605
  );
17551
- const desc = (m, fallback = "") => String(m?.maggie_seo_description || fallback).replace(/\s+/g, " ").trim();
17606
+ const clip3 = (t, n2 = 200) => {
17607
+ const x = t.replace(/\s+/g, " ").trim();
17608
+ if (x.length <= n2) return x;
17609
+ const cut = x.slice(0, n2);
17610
+ return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
17611
+ };
17612
+ const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
17613
+ const isSample = (link, title) => /\/(sample-page|hello-world)\/?$/.test(link) || title === "Sample Page" || title === "Hello world!";
17552
17614
  const home = pages.find((p) => p.id === s.page_on_front);
17553
17615
  const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
17554
- const visible = pages.filter((p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link));
17616
+ const visible = pages.filter(
17617
+ (p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link) && !isSample(p.link, titleOf(p.title))
17618
+ );
17619
+ const realPosts = posts.filter((p) => !isSample(p.link, strip(p.title.rendered ?? "")));
17555
17620
  const byParent = /* @__PURE__ */ new Map();
17556
17621
  for (const p of visible) byParent.set(p.parent, [...byParent.get(p.parent) ?? [], p]);
17557
17622
  const line = (title, url, d) => `- [${title.replace(/[[\]]/g, "")}](${url})${d ? `: ${d}` : ""}`;
@@ -17559,25 +17624,27 @@ async function llmsTxt(site, o = {}) {
17559
17624
  const summary = desc(home?.meta, s.description ?? "");
17560
17625
  if (summary) out.push(`> ${summary}`, "");
17561
17626
  const top = byParent.get(0) ?? [];
17562
- if (top.length) {
17627
+ if (top.length || home) {
17563
17628
  out.push("## Pages", "");
17629
+ if (home) out.push(line(titleOf(home.title) || "Home", home.link, summary));
17564
17630
  for (const p of top) {
17565
17631
  out.push(line(titleOf(p.title), p.link, desc(p.meta)));
17566
17632
  for (const c of byParent.get(p.id) ?? []) out.push(` ${line(titleOf(c.title), c.link, desc(c.meta))}`);
17567
17633
  }
17568
17634
  out.push("");
17569
17635
  }
17570
- if (posts.length) {
17636
+ if (realPosts.length) {
17571
17637
  out.push("## Articles", "");
17572
- for (const p of posts)
17573
- out.push(
17574
- line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? "").slice(0, 160)))
17575
- );
17638
+ for (const p of realPosts)
17639
+ out.push(line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? ""))));
17576
17640
  out.push("");
17577
17641
  }
17578
17642
  const legal = pages.filter((p) => /privacy|terms/i.test(p.link));
17579
- out.push("## Optional", "", line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article"));
17580
- for (const p of legal) out.push(line(titleOf(p.title), p.link, ""));
17643
+ const optional2 = [
17644
+ ...s.blog_public === 0 ? [] : [line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article")],
17645
+ ...legal.map((p) => line(titleOf(p.title), p.link, ""))
17646
+ ];
17647
+ if (optional2.length) out.push("## Optional", "", ...optional2);
17581
17648
  return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
17582
17649
  `;
17583
17650
  }
@@ -19472,12 +19539,16 @@ var elementTools = [
19472
19539
  }),
19473
19540
  tool({
19474
19541
  name: "wp_footer_set",
19475
- description: "Change the footer: any text (address, hours, copyright), link or picture, social profile links by service; reset goes back to the theme's own footer.",
19542
+ description: "Change the footer: any text (address, hours, copyright), link or picture, social profile links by service, the row of page links (footer_links); reset goes back to the theme's own footer.",
19476
19543
  parameters: {
19477
19544
  type: "object",
19478
19545
  properties: {
19479
19546
  ...EDITS.properties ?? {},
19480
19547
  social: obj('Social links by service: { "facebook": "https://\u2026", "instagram": "" (remove) }.'),
19548
+ footer_links: arr(
19549
+ "The footer's row of page links, in order (replaces the row set before; [] removes it): legal, contact, about, main pages.",
19550
+ obj("", { label: str2(""), url: str2("Relative for own pages: /contact/.") })
19551
+ ),
19481
19552
  reset: bool("Drop the site's changes: back to the theme's footer.")
19482
19553
  }
19483
19554
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.13",
3
+ "version": "0.12.15",
4
4
  "description": "CMO.so's MCP server for Claude Desktop, Cursor, VS Code and other MCP clients: manage your websites, blog posts, LinkedIn queue and results with Maggie.",
5
5
  "type": "module",
6
6
  "license": "MIT",