@cmo-so/mcp 0.12.3 → 0.12.5

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 +151 -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.3",
7202
+ version: "0.12.5",
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(
@@ -16282,7 +16287,11 @@ var pageTools = [
16282
16287
  description: str2(""),
16283
16288
  keywords: str2("")
16284
16289
  }),
16285
- cta: obj("Sidebar booking card: title, text.", { title: str2(""), text: str2("") }),
16290
+ cta: obj("Sidebar booking card: title, text, button label (default Book Now).", {
16291
+ title: str2(""),
16292
+ text: str2(""),
16293
+ button: str2("")
16294
+ }),
16286
16295
  related: bool(
16287
16296
  "Add a related-services band from sibling pages (default true when the page has a reading column)."
16288
16297
  ),
@@ -16327,7 +16336,8 @@ var pageTools = [
16327
16336
  const cta = a.cta ?? {};
16328
16337
  const blocks = composePage(sections, kit, {
16329
16338
  ctaTitle: cta.title || `Book ${String(a.title)}`,
16330
- ctaText: cta.text
16339
+ ctaText: cta.text,
16340
+ ctaButton: cta.button
16331
16341
  });
16332
16342
  const outline = brief2(blocks, kit.syncedTitles);
16333
16343
  if (a.preview) return { preview: true, title: a.title, sections: outline };
@@ -16991,6 +17001,17 @@ var childTools = [
16991
17001
  slug: str2("The child theme (default the active one)."),
16992
17002
  colors: obj('e.g. { "olive": "#4F5B48" }.'),
16993
17003
  fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
17004
+ font_files: arr(
17005
+ "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).",
17006
+ obj("", {
17007
+ slug: str2("Family slug in theme.json: display, body, \u2026"),
17008
+ family: str2('Family name, e.g. "Roboto Slab".'),
17009
+ fallback: str2('Fallback stack, e.g. "Georgia, serif".'),
17010
+ url: str2("https link to the font file."),
17011
+ weight: str2('e.g. "400", "800" or "100 900".'),
17012
+ style: str2('"normal" or "italic".')
17013
+ })
17014
+ ),
16994
17015
  styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
16995
17016
  css: str2("Replace the child's CSS (plain CSS)."),
16996
17017
  css_append: str2("Add CSS at the end."),
@@ -17017,9 +17038,54 @@ var childTools = [
17017
17038
  const cur = info.files ?? {};
17018
17039
  const files = {};
17019
17040
  const notes = [];
17041
+ if (Array.isArray(a.font_files) && a.font_files.length) {
17042
+ const tj0 = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17043
+ if (!tj0.settings || Array.isArray(tj0.settings)) tj0.settings = {};
17044
+ const typo = tj0.settings.typography ?? {};
17045
+ const fams = typo.fontFamilies ?? [];
17046
+ for (const f of a.font_files) {
17047
+ if (!/^[a-z0-9-]{2,30}$/.test(f.slug ?? ""))
17048
+ throw new Error(`Font slug "${f.slug}": lowercase letters, digits and dashes.`);
17049
+ if (!/^https:\/\//.test(f.url ?? "")) throw new Error(`${f.family}: the font's link must be https.`);
17050
+ const r = await fetch(f.url);
17051
+ if (!r.ok) throw new Error(`${f.family}: the font's link answered HTTP ${r.status}.`);
17052
+ const bytes = new Uint8Array(await r.arrayBuffer());
17053
+ const head = String.fromCharCode(...bytes.subarray(0, 4));
17054
+ const ext = head === "wOF2" ? "woff2" : head === "wOFF" ? "woff" : head === "OTTO" ? "otf" : head === "\0\0\0" || head === "true" ? "ttf" : "";
17055
+ if (!ext) throw new Error(`${f.family}: that link isn't a font file.`);
17056
+ if (bytes.length > 4 * 1024 * 1024) throw new Error(`${f.family}: the font file is larger than 4 MB.`);
17057
+ const weight = f.weight || "400";
17058
+ const style = f.style || "normal";
17059
+ const name = `${f.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${weight.replace(/\s+/g, "-")}${style === "italic" ? "-italic" : ""}.${ext}`;
17060
+ let bin = "";
17061
+ for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
17062
+ files[`assets/fonts/${name}`] = btoa(bin);
17063
+ let fam = fams.find((x) => x.slug === f.slug);
17064
+ const stack = `"${f.family}", ${f.fallback || "sans-serif"}`;
17065
+ if (!fam || !fam.fontFamily.includes(`"${f.family}"`)) {
17066
+ const keep = fam ? fams.indexOf(fam) : -1;
17067
+ fam = { slug: f.slug, name: f.family, fontFamily: stack, fontFace: [] };
17068
+ if (keep >= 0) fams[keep] = fam;
17069
+ else fams.push(fam);
17070
+ }
17071
+ fam.fontFace = [
17072
+ ...(fam.fontFace ?? []).filter((x) => !(x.fontWeight === weight && x.fontStyle === style)),
17073
+ {
17074
+ fontFamily: f.family,
17075
+ fontWeight: weight,
17076
+ fontStyle: style,
17077
+ fontDisplay: "swap",
17078
+ src: [`file:./assets/fonts/${name}`]
17079
+ }
17080
+ ];
17081
+ }
17082
+ tj0.settings.typography = { ...typo, fontFamilies: fams };
17083
+ files["theme.json"] = JSON.stringify(tj0, null, " ");
17084
+ }
17020
17085
  if (a.colors || a.fonts || a.styles) {
17021
- const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
17022
- tj.settings = tj.settings ?? {};
17086
+ const tj = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
17087
+ if (!tj.settings || Array.isArray(tj.settings)) tj.settings = {};
17088
+ if (Array.isArray(tj.styles)) tj.styles = {};
17023
17089
  if (a.colors) {
17024
17090
  let palette = tj.settings.color?.palette ?? [];
17025
17091
  if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
@@ -17148,6 +17214,81 @@ var childTools = [
17148
17214
  })
17149
17215
  })
17150
17216
  ];
17217
+ var themeTools = [
17218
+ tool({
17219
+ name: "wp_theme_updates",
17220
+ description: "Is there a newer Maggie Core for this site? The installed and latest versions, release notes link, whether this host lets it update itself, auto-updates on or off, and the earlier versions kept for rollback.",
17221
+ parameters: { type: "object", properties: {} },
17222
+ group: "site",
17223
+ need: READ,
17224
+ risk: "R0",
17225
+ gates: ["WP"],
17226
+ run: (ctx) => call(siteOf(ctx), { path: "/maggie-core/v1/update", query: { fresh: 1 } })
17227
+ }),
17228
+ tool({
17229
+ name: "wp_theme_update",
17230
+ description: "Update Maggie Core on the site to the latest version (checked against its published checksum; the current version is kept for rollback), then compare the homepage before and after (words, fonts, brand colours). If the site breaks (an error page or most of the homepage gone) it rolls back by itself. rollback_to puts an earlier kept version back instead. The site's look lives in the database and its child theme, so an update doesn't change it.",
17231
+ parameters: {
17232
+ type: "object",
17233
+ properties: { rollback_to: str2("Put this kept version back instead (from wp_theme_updates backups).") }
17234
+ },
17235
+ group: "site",
17236
+ need: WRITE,
17237
+ risk: "W3",
17238
+ gates: [...GATES],
17239
+ confirm: (a, lang) => a.rollback_to ? say(
17240
+ lang,
17241
+ `Put Maggie Core ${String(a.rollback_to)} back on your website.`,
17242
+ `\u628A\u7DB2\u7AD9\u7684 Maggie Core \u9084\u539F\u5230 ${String(a.rollback_to)}\u3002`
17243
+ ) : say(
17244
+ lang,
17245
+ "Update Maggie Core on your website to the latest version (rolled back by itself if the site breaks).",
17246
+ "\u628A\u7DB2\u7AD9\u7684 Maggie Core \u66F4\u65B0\u5230\u6700\u65B0\u7248\u672C\uFF08\u7DB2\u7AD9\u51FA\u932F\u6703\u81EA\u52D5\u9084\u539F\uFF09\u3002"
17247
+ ),
17248
+ run: async (ctx, a) => {
17249
+ const site = siteOf(ctx);
17250
+ const before = await homeShape(site);
17251
+ if (a.rollback_to) {
17252
+ const r2 = await call(site, {
17253
+ method: "POST",
17254
+ path: "/maggie-core/v1/update/rollback",
17255
+ body: { version: a.rollback_to }
17256
+ });
17257
+ return { ...r2, check: compareShapes(before, await homeShape(site)) };
17258
+ }
17259
+ const info = await call(site, { path: "/maggie-core/v1/update", query: { fresh: 1 } });
17260
+ if (!info.available)
17261
+ return {
17262
+ updated: false,
17263
+ current: info.current,
17264
+ latest: info.latest,
17265
+ note: info.check_error ?? "Already the latest Maggie Core."
17266
+ };
17267
+ if (!info.can_update)
17268
+ throw new Error(info.update_issue ?? "This host doesn't let WordPress update themes itself.");
17269
+ const r = await call(site, {
17270
+ method: "POST",
17271
+ path: "/maggie-core/v1/update"
17272
+ });
17273
+ const check2 = compareShapes(before, await homeShape(site));
17274
+ const broken = check2.checked && (check2.problems.some((p) => p.startsWith("the homepage answers")) || (check2.same_text_percent ?? 100) < 60);
17275
+ if (broken) {
17276
+ const back = await call(site, {
17277
+ method: "POST",
17278
+ path: "/maggie-core/v1/update/rollback",
17279
+ body: { version: r.before }
17280
+ }).catch((e) => ({ error: e.message }));
17281
+ return {
17282
+ ...r,
17283
+ check: check2,
17284
+ rolled_back: back,
17285
+ note: `The homepage looked broken after the update, so Maggie Core ${r.before} was put back.`
17286
+ };
17287
+ }
17288
+ return { ...r, check: check2, notes: info.notes, undo: `wp_theme_update rollback_to ${r.before}` };
17289
+ }
17290
+ })
17291
+ ];
17151
17292
  function deepMerge(a, b) {
17152
17293
  const out = { ...a };
17153
17294
  for (const [k, v] of Object.entries(b)) {
@@ -17417,6 +17558,7 @@ var wordpressTools = [
17417
17558
  ...sectionTools,
17418
17559
  ...mediaTools,
17419
17560
  ...childTools,
17561
+ ...themeTools,
17420
17562
  ...seoTools,
17421
17563
  ...elementTools
17422
17564
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
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",