@cmo-so/mcp 0.12.4 → 0.12.6

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 +193 -9
  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.4",
7202
+ version: "0.12.6",
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",
@@ -14220,7 +14220,7 @@ var SECTIONS2 = [
14220
14220
  [
14221
14221
  S(s.heading) || S(s.intro) ? prose([...headed(s), para(S(s.intro))]) : "",
14222
14222
  group(boxes, {
14223
- cls: `maggie-stagger grid gap-5 ${boxes.length >= 4 ? "grid-cols-2 md:grid-cols-4" : "sm:grid-cols-3"}`
14223
+ cls: `maggie-stagger grid gap-5 ${boxes.length === 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"}`
14224
14224
  })
14225
14225
  ],
14226
14226
  "flex flex-col gap-8"
@@ -14443,6 +14443,7 @@ var SECTIONS2 = [
14443
14443
  slots: {
14444
14444
  title: str('e.g. "Book your HydraFacial".'),
14445
14445
  text: str("One sentence."),
14446
+ button_label: str('Default "Book Now".'),
14446
14447
  button_url: str("Default: the booking link.")
14447
14448
  },
14448
14449
  required: ["title"],
@@ -14453,7 +14454,7 @@ var SECTIONS2 = [
14453
14454
  buttons(
14454
14455
  [
14455
14456
  button(
14456
- "Book Now",
14457
+ S(s.button_label) || "Book Now",
14457
14458
  S(s.button_url) || kit.bookUrl || "/contact-us/",
14458
14459
  "w-full [&_a]:w-full [&_a]:bg-white [&_a]:text-ink hover:[&_a]:bg-panel"
14459
14460
  )
@@ -15001,7 +15002,7 @@ async function menuSet(site, ops, id) {
15001
15002
  const blocks = parse(nav.content.raw);
15002
15003
  const before = toItems(blocks);
15003
15004
  const after = applyMenuOps(before, ops);
15004
- const rest = blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
15005
+ const rest = ops.items ? [] : blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
15005
15006
  const content = [fromItems(after), serialize(rest)].filter(Boolean).join("\n");
15006
15007
  await call(site, {
15007
15008
  method: "POST",
@@ -15495,7 +15496,11 @@ function composePage(sections, kit, aside = {}) {
15495
15496
  const asideParts = [
15496
15497
  kit.synced?.share ? group([syncedRef(kit.synced.share)], { cls: "px-2" }) : "",
15497
15498
  serialize([
15498
- buildSection("cta-card", { title: aside.ctaTitle || "Book your visit", text: aside.ctaText ?? "" }, kit)
15499
+ buildSection(
15500
+ "cta-card",
15501
+ { title: aside.ctaTitle || "Book your visit", text: aside.ctaText ?? "", button_label: aside.ctaButton },
15502
+ kit
15503
+ )
15499
15504
  ])
15500
15505
  ];
15501
15506
  const body = band(
@@ -16059,6 +16064,27 @@ async function changePage(ctx, a, fn) {
16059
16064
  undo: "wp_page_undo goes back to the version before this change."
16060
16065
  };
16061
16066
  }
16067
+ var findAllBlocks = (list3) => list3.flatMap((b) => [b, ...findAllBlocks(b.inner)]);
16068
+ async function setHome(site, id, postsPage) {
16069
+ const before = await call(site, {
16070
+ path: "/wp/v2/settings",
16071
+ query: { _fields: "show_on_front,page_on_front,page_for_posts" }
16072
+ });
16073
+ const body = { show_on_front: "page", page_on_front: id };
16074
+ if (postsPage !== void 0) body.page_for_posts = postsPage;
16075
+ if (postsPage === id) throw new Error("The homepage and the posts page can't be the same page.");
16076
+ const after = await call(site, {
16077
+ method: "POST",
16078
+ path: "/wp/v2/settings",
16079
+ query: { _fields: "show_on_front,page_on_front,page_for_posts" },
16080
+ body
16081
+ });
16082
+ return {
16083
+ before,
16084
+ after,
16085
+ undo: before.show_on_front === "page" && before.page_on_front ? `wp_site_home_set page ${before.page_on_front}` : "wp_site_home_set latest_posts true"
16086
+ };
16087
+ }
16062
16088
  function position(a) {
16063
16089
  return {
16064
16090
  after: a.after || void 0,
@@ -16282,7 +16308,11 @@ var pageTools = [
16282
16308
  description: str2(""),
16283
16309
  keywords: str2("")
16284
16310
  }),
16285
- cta: obj("Sidebar booking card: title, text.", { title: str2(""), text: str2("") }),
16311
+ cta: obj("Sidebar booking card: title, text, button label (default Book Now).", {
16312
+ title: str2(""),
16313
+ text: str2(""),
16314
+ button: str2("")
16315
+ }),
16286
16316
  related: bool(
16287
16317
  "Add a related-services band from sibling pages (default true when the page has a reading column)."
16288
16318
  ),
@@ -16327,7 +16357,8 @@ var pageTools = [
16327
16357
  const cta = a.cta ?? {};
16328
16358
  const blocks = composePage(sections, kit, {
16329
16359
  ctaTitle: cta.title || `Book ${String(a.title)}`,
16330
- ctaText: cta.text
16360
+ ctaText: cta.text,
16361
+ ctaButton: cta.button
16331
16362
  });
16332
16363
  const outline = brief2(blocks, kit.syncedTitles);
16333
16364
  if (a.preview) return { preview: true, title: a.title, sections: outline };
@@ -16426,6 +16457,103 @@ var pageTools = [
16426
16457
  };
16427
16458
  }
16428
16459
  }),
16460
+ tool({
16461
+ name: "wp_page_publish",
16462
+ description: "Publish a page (a draft or private page) so visitors and search engines see it. Checks it first: one H1, an SEO title and description (warns when missing). homepage: true also makes it the site's homepage. Afterwards the live page is opened to confirm it works. To take it down again, wp_page_update status draft.",
16463
+ parameters: {
16464
+ type: "object",
16465
+ properties: {
16466
+ page: PAGE,
16467
+ homepage: bool("Also make it the site's homepage."),
16468
+ expect_modified: EXPECT
16469
+ },
16470
+ required: ["page"]
16471
+ },
16472
+ group: "site",
16473
+ need: WRITE,
16474
+ risk: "W3",
16475
+ gates: [...GATES],
16476
+ confirm: (a, lang) => say(
16477
+ lang,
16478
+ `Publish page ${q2(a.page, "")} on your website${a.homepage ? " and make it the homepage" : ""}.`,
16479
+ `\u767C\u4F48\u7DB2\u7AD9\u4E0A\u7684\u9801\u9762 ${q2(a.page, "")}${a.homepage ? "\uFF0C\u4E26\u8A2D\u70BA\u9996\u9801" : ""}\u3002`
16480
+ ),
16481
+ run: async (ctx, a) => {
16482
+ const site = siteOf(ctx);
16483
+ const p = await findPage(site, a.page, true);
16484
+ checkFresh(p, a.expect_modified);
16485
+ const m = p.meta ?? {};
16486
+ const blocks = parse(p.content?.raw ?? "");
16487
+ const h1 = findAllBlocks(blocks).filter((b) => b.name === "core/heading" && Number(b.attrs.level) === 1).length;
16488
+ const warnings = [
16489
+ h1 === 1 ? "" : h1 === 0 ? "The page has no H1 in its content (a template may add one)." : `The page has ${h1} H1 headings.`,
16490
+ m.maggie_seo_title ? "" : "No SEO title: the page title is used.",
16491
+ m.maggie_seo_description ? "" : "No meta description: set one with wp_seo_set."
16492
+ ].filter(Boolean);
16493
+ const wasStatus = p.status;
16494
+ const saved = p.status === "publish" ? p : await savePage(site, p.id, null, { status: "publish" });
16495
+ let home = null;
16496
+ if (a.homepage) home = await setHome(site, p.id);
16497
+ const live = await check(site, saved.link, "publish");
16498
+ return {
16499
+ published: true,
16500
+ id: p.id,
16501
+ path: path(saved.link),
16502
+ before: wasStatus,
16503
+ homepage: home,
16504
+ check: live,
16505
+ warnings
16506
+ };
16507
+ }
16508
+ }),
16509
+ tool({
16510
+ name: "wp_site_home_set",
16511
+ description: "Choose the site's homepage (a published page) and, optionally, the page that lists the blog posts. Returns the previous setting so it can be put back. latest_posts: true shows the newest posts as the homepage instead.",
16512
+ parameters: {
16513
+ type: "object",
16514
+ properties: {
16515
+ page: str2("The page to show as the homepage (path, slug or id)."),
16516
+ posts_page: str2('The page that lists the blog posts (optional; "" to clear).'),
16517
+ latest_posts: bool("Show the newest posts as the homepage instead of a page.")
16518
+ }
16519
+ },
16520
+ group: "site",
16521
+ need: WRITE,
16522
+ risk: "W3",
16523
+ gates: [...GATES],
16524
+ confirm: (a, lang) => a.latest_posts ? say(lang, "Show your newest posts as your website's homepage.", "\u628A\u7DB2\u7AD9\u9996\u9801\u6539\u70BA\u986F\u793A\u6700\u65B0\u6587\u7AE0\u3002") : say(
16525
+ lang,
16526
+ `Make ${q2(a.page, "that page")} your website's homepage.`,
16527
+ `\u628A ${q2(a.page, "\u8A72\u9801\u9762")} \u8A2D\u70BA\u7DB2\u7AD9\u9996\u9801\u3002`
16528
+ ),
16529
+ run: async (ctx, a) => {
16530
+ const site = siteOf(ctx);
16531
+ if (a.latest_posts) {
16532
+ const before = await call(site, {
16533
+ path: "/wp/v2/settings",
16534
+ query: { _fields: "show_on_front,page_on_front,page_for_posts" }
16535
+ });
16536
+ await call(site, {
16537
+ method: "POST",
16538
+ path: "/wp/v2/settings",
16539
+ query: { _fields: "show_on_front" },
16540
+ body: { show_on_front: "posts" }
16541
+ });
16542
+ return { before, after: { show_on_front: "posts" } };
16543
+ }
16544
+ if (!a.page) throw new Error("Which page should be the homepage?");
16545
+ const p = await findPage(site, a.page);
16546
+ if (p.status !== "publish")
16547
+ throw new Error(
16548
+ `${path(p.link)} isn't published yet: publish it first (wp_page_publish homepage: true does both).`
16549
+ );
16550
+ let postsPage;
16551
+ if (typeof a.posts_page === "string")
16552
+ postsPage = a.posts_page === "" ? 0 : (await findPage(site, a.posts_page)).id;
16553
+ const r = await setHome(site, p.id, postsPage);
16554
+ return { ...r, check: await check(site, `${site.url ?? ""}/`, "publish") };
16555
+ }
16556
+ }),
16429
16557
  tool({
16430
16558
  name: "wp_page_delete",
16431
16559
  description: "Move a page to the WordPress trash (restorable for 30 days) and redirect its address (301) to another page, by default its parent or the homepage.",
@@ -16991,6 +17119,17 @@ var childTools = [
16991
17119
  slug: str2("The child theme (default the active one)."),
16992
17120
  colors: obj('e.g. { "olive": "#4F5B48" }.'),
16993
17121
  fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
17122
+ font_files: arr(
17123
+ "Install fonts in the child from https links to font files (woff2/woff/ttf), e.g. Google Fonts' fonts.gstatic.com files: each becomes a fontFace of its family (slug display / body / any).",
17124
+ obj("", {
17125
+ slug: str2("Family slug in theme.json: display, body, \u2026"),
17126
+ family: str2('Family name, e.g. "Roboto Slab".'),
17127
+ fallback: str2('Fallback stack, e.g. "Georgia, serif".'),
17128
+ url: str2("https link to the font file."),
17129
+ weight: str2('e.g. "400", "800" or "100 900".'),
17130
+ style: str2('"normal" or "italic".')
17131
+ })
17132
+ ),
16994
17133
  styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
16995
17134
  css: str2("Replace the child's CSS (plain CSS)."),
16996
17135
  css_append: str2("Add CSS at the end."),
@@ -17017,9 +17156,54 @@ var childTools = [
17017
17156
  const cur = info.files ?? {};
17018
17157
  const files = {};
17019
17158
  const notes = [];
17159
+ if (Array.isArray(a.font_files) && a.font_files.length) {
17160
+ const tj0 = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17161
+ if (!tj0.settings || Array.isArray(tj0.settings)) tj0.settings = {};
17162
+ const typo = tj0.settings.typography ?? {};
17163
+ const fams = typo.fontFamilies ?? [];
17164
+ for (const f of a.font_files) {
17165
+ if (!/^[a-z0-9-]{2,30}$/.test(f.slug ?? ""))
17166
+ throw new Error(`Font slug "${f.slug}": lowercase letters, digits and dashes.`);
17167
+ if (!/^https:\/\//.test(f.url ?? "")) throw new Error(`${f.family}: the font's link must be https.`);
17168
+ const r = await fetch(f.url);
17169
+ if (!r.ok) throw new Error(`${f.family}: the font's link answered HTTP ${r.status}.`);
17170
+ const bytes = new Uint8Array(await r.arrayBuffer());
17171
+ const head = String.fromCharCode(...bytes.subarray(0, 4));
17172
+ const ext = head === "wOF2" ? "woff2" : head === "wOFF" ? "woff" : head === "OTTO" ? "otf" : head === "\0\0\0" || head === "true" ? "ttf" : "";
17173
+ if (!ext) throw new Error(`${f.family}: that link isn't a font file.`);
17174
+ if (bytes.length > 4 * 1024 * 1024) throw new Error(`${f.family}: the font file is larger than 4 MB.`);
17175
+ const weight = f.weight || "400";
17176
+ const style = f.style || "normal";
17177
+ const name = `${f.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${weight.replace(/\s+/g, "-")}${style === "italic" ? "-italic" : ""}.${ext}`;
17178
+ let bin = "";
17179
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
17180
+ files[`assets/fonts/${name}`] = btoa(bin);
17181
+ let fam = fams.find((x) => x.slug === f.slug);
17182
+ const stack = `"${f.family}", ${f.fallback || "sans-serif"}`;
17183
+ if (!fam || !fam.fontFamily.includes(`"${f.family}"`)) {
17184
+ const keep = fam ? fams.indexOf(fam) : -1;
17185
+ fam = { slug: f.slug, name: f.family, fontFamily: stack, fontFace: [] };
17186
+ if (keep >= 0) fams[keep] = fam;
17187
+ else fams.push(fam);
17188
+ }
17189
+ fam.fontFace = [
17190
+ ...(fam.fontFace ?? []).filter((x) => !(x.fontWeight === weight && x.fontStyle === style)),
17191
+ {
17192
+ fontFamily: f.family,
17193
+ fontWeight: weight,
17194
+ fontStyle: style,
17195
+ fontDisplay: "swap",
17196
+ src: [`file:./assets/fonts/${name}`]
17197
+ }
17198
+ ];
17199
+ }
17200
+ tj0.settings.typography = { ...typo, fontFamilies: fams };
17201
+ files["theme.json"] = JSON.stringify(tj0, null, " ");
17202
+ }
17020
17203
  if (a.colors || a.fonts || a.styles) {
17021
- const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
17022
- tj.settings = tj.settings ?? {};
17204
+ const tj = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17205
+ if (!tj.settings || Array.isArray(tj.settings)) tj.settings = {};
17206
+ if (Array.isArray(tj.styles)) tj.styles = {};
17023
17207
  if (a.colors) {
17024
17208
  let palette = tj.settings.color?.palette ?? [];
17025
17209
  if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
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",