@cmo-so/mcp 0.12.2 → 0.12.4

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 +368 -1
  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.2",
7202
+ version: "0.12.4",
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",
@@ -16866,6 +16866,371 @@ var mediaTools = [
16866
16866
  }
16867
16867
  })
16868
16868
  ];
16869
+ var childInfo = (site, slug) => call(site, { path: "/maggie-core/v1/child-theme", query: { slug } });
16870
+ async function childSlug(site, slug) {
16871
+ if (typeof slug === "string" && slug) return slug;
16872
+ const info = await childInfo(site);
16873
+ const active = info.children.find((c) => c.active);
16874
+ if (!active)
16875
+ throw new Error("No Maggie child theme is active here: name one (slug), or create one with wp_child_theme_create.");
16876
+ return active.slug;
16877
+ }
16878
+ async function homeShape(site) {
16879
+ if (!site.fetchPage || !site.url) return null;
16880
+ const r = await site.fetchPage(`${site.url}/?maggie-check=${Date.now()}`).catch(() => null);
16881
+ if (!r) return null;
16882
+ const text3 = r.html.replace(/<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>/g, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
16883
+ const fonts = [
16884
+ ...new Set([...r.html.matchAll(/font-family:\s*([^;"}]+)/g)].map((m) => m[1].trim()))
16885
+ ].sort();
16886
+ const colors = [...r.html.matchAll(/--wp--preset--color--([a-z0-9-]+):\s*([^;]+);/g)].map((m) => `${m[1]}=${m[2].trim()}`).sort();
16887
+ return { status: r.status, words: new Set(text3.toLowerCase().split(" ")), fonts, colors };
16888
+ }
16889
+ function compareShapes(a, b) {
16890
+ if (!a || !b) return { checked: false };
16891
+ const shared = [...a.words].filter((w) => b.words.has(w)).length;
16892
+ const text3 = Math.round(shared / Math.max(1, Math.max(a.words.size, b.words.size)) * 100);
16893
+ const fontsSame = a.fonts.join("|") === b.fonts.join("|");
16894
+ const colorsSame = a.colors.join("|") === b.colors.join("|");
16895
+ const problems = [
16896
+ b.status >= 400 ? `the homepage answers HTTP ${b.status}` : "",
16897
+ text3 < 90 ? `only ${text3}% of the homepage's words are the same` : "",
16898
+ fontsSame ? "" : "the fonts changed",
16899
+ colorsSame ? "" : "the brand colours changed"
16900
+ ].filter(Boolean);
16901
+ return {
16902
+ checked: true,
16903
+ same_text_percent: text3,
16904
+ fonts_same: fontsSame,
16905
+ colors_same: colorsSame,
16906
+ ok: !problems.length,
16907
+ problems
16908
+ };
16909
+ }
16910
+ var PATTERN_SCHEMA = obj("A pattern for the editor's inserter.", {
16911
+ slug: str2("File name: lowercase, dashes (e.g. spring-offer)."),
16912
+ title: str2("Name shown in the inserter."),
16913
+ content: str2("Block markup (e.g. a section from wp_section_library written out, or copied from a page)."),
16914
+ description: str2(""),
16915
+ categories: arr("Pattern categories (default maggie).", { type: "string" })
16916
+ });
16917
+ var childTools = [
16918
+ tool({
16919
+ name: "wp_child_theme_status",
16920
+ description: "The site's Maggie child themes (Maggie Core is the parent): which is active, their files and backups, and whether files can be written on this host. With slug: that child's text files in full (theme.json, style.css, parts, patterns) to read before changing them.",
16921
+ parameters: { type: "object", properties: { slug: str2("A child theme to read in full, e.g. maggie-moonlake.") } },
16922
+ group: "site",
16923
+ need: READ,
16924
+ risk: "R0",
16925
+ gates: ["WP"],
16926
+ run: async (ctx, a) => {
16927
+ const info = await childInfo(siteOf(ctx), a.slug || void 0);
16928
+ const files = info.files ? Object.fromEntries(
16929
+ Object.entries(info.files).map(([k, v]) => [k, v.length > 2e4 ? `${v.slice(0, 2e4)}\u2026` : v])
16930
+ ) : void 0;
16931
+ return {
16932
+ ...info,
16933
+ children: info.children.map((c) => ({
16934
+ ...c,
16935
+ files: Object.keys(c.files).length,
16936
+ file_list: Object.keys(c.files)
16937
+ })),
16938
+ ...files ? { files } : {}
16939
+ };
16940
+ }
16941
+ }),
16942
+ tool({
16943
+ name: "wp_child_theme_create",
16944
+ description: "Create a child theme of Maggie Core for this site (slug maggie-<name>), by default from the site's current look: its Global Styles (colours, fonts with their files, styles), edited header and footer. Data files only, never code. activate: true switches the site to it and checks the homepage looks the same. Switching keeps the look only because it is copied into the child; Global Styles and edited parts are kept per theme in WordPress.",
16945
+ parameters: {
16946
+ type: "object",
16947
+ properties: {
16948
+ slug: str2("maggie-<name>, e.g. maggie-moonlake."),
16949
+ name: str2("Theme name, e.g. Moon Lake Spa."),
16950
+ from_site: bool("Copy the site's current look into it (default true)."),
16951
+ activate: bool("Switch the site to it now (default false).")
16952
+ },
16953
+ required: ["slug", "name"]
16954
+ },
16955
+ group: "site",
16956
+ need: WRITE,
16957
+ risk: "W3",
16958
+ gates: [...GATES],
16959
+ confirm: (a, lang) => say(
16960
+ lang,
16961
+ `Create the child theme ${q2(a.slug, "")} on your website${a.activate ? " and switch to it" : ""}.`,
16962
+ `\u5728\u7DB2\u7AD9\u4E0A\u5EFA\u7ACB\u5B50\u4E3B\u984C ${q2(a.slug, "")}${a.activate ? "\u4E26\u5207\u63DB\u904E\u53BB" : ""}\u3002`
16963
+ ),
16964
+ run: async (ctx, a) => {
16965
+ const site = siteOf(ctx);
16966
+ const out = await call(site, {
16967
+ method: "POST",
16968
+ path: "/maggie-core/v1/child-theme",
16969
+ body: { slug: a.slug, name: a.name, from_site: a.from_site !== false }
16970
+ });
16971
+ if (!a.activate)
16972
+ return {
16973
+ ...out,
16974
+ next: "Switch to it with wp_child_theme_activate when ready (the tool checks the homepage looks the same)."
16975
+ };
16976
+ const before = await homeShape(site);
16977
+ const act = await call(site, {
16978
+ method: "POST",
16979
+ path: "/maggie-core/v1/child-theme/activate",
16980
+ body: { slug: out.slug }
16981
+ });
16982
+ return { ...out, activated: act, check: compareShapes(before, await homeShape(site)) };
16983
+ }
16984
+ }),
16985
+ tool({
16986
+ name: "wp_child_theme_update",
16987
+ description: "Change a Maggie child theme's files (default: the active child): brand colours (by name, hex) and fonts (by name, CSS font-family of fonts already in it) in theme.json, extra theme.json styles, plain CSS (replace or append), the header or footer (block markup), templates, and the site's own patterns (shown in the editor's inserter). Data only, never code. Each change keeps a backup (wp_child_theme_restore). Changes made with wp_style_set / wp_header_set are stored in the database and win over the child's files.",
16988
+ parameters: {
16989
+ type: "object",
16990
+ properties: {
16991
+ slug: str2("The child theme (default the active one)."),
16992
+ colors: obj('e.g. { "olive": "#4F5B48" }.'),
16993
+ fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
16994
+ styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
16995
+ css: str2("Replace the child's CSS (plain CSS)."),
16996
+ css_append: str2("Add CSS at the end."),
16997
+ header: str2("Header block markup (parts/header.html)."),
16998
+ footer: str2("Footer block markup (parts/footer.html)."),
16999
+ templates: obj('Templates by slug \u2192 block markup, e.g. { "page-landing": "\u2026" }.'),
17000
+ patterns: arr("Patterns to add or replace.", PATTERN_SCHEMA),
17001
+ remove_patterns: arr("Pattern slugs to remove.", { type: "string" })
17002
+ }
17003
+ },
17004
+ group: "site",
17005
+ need: WRITE,
17006
+ risk: "W3",
17007
+ gates: [...GATES],
17008
+ confirm: (a, lang) => say(
17009
+ lang,
17010
+ `Change the child theme ${q2(a.slug, "(the active one)")} on your website.`,
17011
+ `\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002`
17012
+ ),
17013
+ run: async (ctx, a) => {
17014
+ const site = siteOf(ctx);
17015
+ const slug = await childSlug(site, a.slug);
17016
+ const info = await childInfo(site, slug);
17017
+ const cur = info.files ?? {};
17018
+ const files = {};
17019
+ const notes = [];
17020
+ if (a.colors || a.fonts || a.styles) {
17021
+ const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
17022
+ tj.settings = tj.settings ?? {};
17023
+ if (a.colors) {
17024
+ let palette = tj.settings.color?.palette ?? [];
17025
+ if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
17026
+ for (const [slugC, color] of Object.entries(a.colors)) {
17027
+ if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color))
17028
+ throw new Error(`${slugC}: "${color}" isn't a hex colour.`);
17029
+ const c = palette.find((x) => x.slug === slugC);
17030
+ if (c) c.color = color;
17031
+ else
17032
+ palette.push({
17033
+ slug: slugC,
17034
+ color,
17035
+ name: slugC.replace(/-/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())
17036
+ });
17037
+ }
17038
+ const map = Object.fromEntries(palette.map((c) => [c.slug, c.color]));
17039
+ const bad = contrastReport(map).filter((r) => !r.ok);
17040
+ if (bad.length)
17041
+ notes.push(`Low contrast: ${bad.map((r) => `${r.text} on ${r.background} ${r.ratio}:1`).join(", ")}.`);
17042
+ tj.settings.color = { ...tj.settings.color ?? {}, palette };
17043
+ }
17044
+ if (a.fonts) {
17045
+ const fams = tj.settings.typography?.fontFamilies ?? [];
17046
+ if (!fams.length)
17047
+ throw new Error(
17048
+ "This child has no font list yet (fonts come from the parent): create it from the site first, then change fonts."
17049
+ );
17050
+ for (const [slugF, family] of Object.entries(a.fonts)) {
17051
+ const f = fams.find((x) => x.slug === slugF);
17052
+ if (!f) throw new Error(`No font "${slugF}" in the child. Fonts: ${fams.map((x) => x.slug).join(", ")}`);
17053
+ f.fontFamily = family;
17054
+ }
17055
+ }
17056
+ if (a.styles)
17057
+ tj.styles = deepMerge(tj.styles ?? {}, a.styles);
17058
+ files["theme.json"] = JSON.stringify(tj, null, " ");
17059
+ }
17060
+ if (typeof a.css === "string") files["style.css"] = a.css;
17061
+ if (typeof a.css_append === "string")
17062
+ files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a.css_append}
17063
+ `;
17064
+ const markup = (path2, m) => {
17065
+ if (typeof m !== "string" || !m.trim()) return;
17066
+ if (!parse(m).some((b) => b.name)) throw new Error(`${path2}: give block markup (<!-- wp:\u2026 -->).`);
17067
+ files[path2] = m;
17068
+ };
17069
+ markup("parts/header.html", a.header);
17070
+ markup("parts/footer.html", a.footer);
17071
+ for (const [t, m] of Object.entries(a.templates ?? {}))
17072
+ markup(`templates/${t}.html`, m);
17073
+ for (const p of a.patterns ?? []) {
17074
+ if (!/^[a-z0-9][a-z0-9-]{0,60}$/.test(p.slug ?? ""))
17075
+ throw new Error(`Pattern slug "${p.slug}": lowercase letters, digits and dashes.`);
17076
+ files[`maggie-patterns/${p.slug}.json`] = JSON.stringify({
17077
+ title: p.title,
17078
+ content: p.content,
17079
+ description: p.description ?? "",
17080
+ categories: p.categories?.length ? p.categories : ["maggie"]
17081
+ });
17082
+ }
17083
+ const del = (a.remove_patterns ?? []).map((x) => `maggie-patterns/${x}.json`);
17084
+ if (!Object.keys(files).length && !del.length) throw new Error("Nothing to change.");
17085
+ const out = await call(site, {
17086
+ method: "POST",
17087
+ path: "/maggie-core/v1/child-theme",
17088
+ body: { slug, files, delete: del }
17089
+ });
17090
+ return {
17091
+ slug,
17092
+ ...out,
17093
+ ...notes.length ? { notes } : {},
17094
+ ...out.active ? {} : { note: "This child isn't active: switch with wp_child_theme_activate to see it." }
17095
+ };
17096
+ }
17097
+ }),
17098
+ tool({
17099
+ name: "wp_child_theme_activate",
17100
+ description: "Switch the site to a Maggie child theme, or back to Maggie Core (slug maggie-core). Checks the homepage before and after (words, fonts, brand colours) and reports differences. WordPress keeps Global Styles and edited header/footer per theme: a child made with wp_child_theme_create from the site carries them; switching back to Maggie Core brings its own back.",
17101
+ parameters: {
17102
+ type: "object",
17103
+ properties: { slug: str2("maggie-<name>, or maggie-core to go back.") },
17104
+ required: ["slug"]
17105
+ },
17106
+ group: "site",
17107
+ need: WRITE,
17108
+ risk: "W3",
17109
+ gates: [...GATES],
17110
+ confirm: (a, lang) => say(lang, `Switch your website's theme to ${q2(a.slug, "")}.`, `\u628A\u7DB2\u7AD9\u7684\u4E3B\u984C\u5207\u63DB\u70BA ${q2(a.slug, "")}\u3002`),
17111
+ run: async (ctx, a) => {
17112
+ const site = siteOf(ctx);
17113
+ const before = await homeShape(site);
17114
+ const out = await call(site, {
17115
+ method: "POST",
17116
+ path: "/maggie-core/v1/child-theme/activate",
17117
+ body: { slug: a.slug }
17118
+ });
17119
+ const check2 = compareShapes(before, await homeShape(site));
17120
+ return {
17121
+ ...out,
17122
+ check: check2,
17123
+ ...check2.checked && !check2.ok ? { undo: `Switch back with wp_child_theme_activate slug ${out.before}.` } : {}
17124
+ };
17125
+ }
17126
+ }),
17127
+ tool({
17128
+ name: "wp_child_theme_restore",
17129
+ description: "Put a child theme's files back as they were before one change (backup ids from wp_child_theme_status). The current files are backed up first, so a restore can be undone too.",
17130
+ parameters: {
17131
+ type: "object",
17132
+ properties: { slug: str2("The child theme."), backup: str2("Backup id, e.g. 20261104-183208-728.") },
17133
+ required: ["slug", "backup"]
17134
+ },
17135
+ group: "site",
17136
+ need: WRITE,
17137
+ risk: "W3",
17138
+ gates: [...GATES],
17139
+ confirm: (a, lang) => say(
17140
+ lang,
17141
+ `Restore the child theme ${q2(a.slug, "")} to backup ${String(a.backup ?? "")}.`,
17142
+ `\u628A\u5B50\u4E3B\u984C ${q2(a.slug, "")} \u9084\u539F\u5230\u5099\u4EFD ${String(a.backup ?? "")}\u3002`
17143
+ ),
17144
+ run: (ctx, a) => call(siteOf(ctx), {
17145
+ method: "POST",
17146
+ path: "/maggie-core/v1/child-theme/restore",
17147
+ body: { slug: a.slug, backup: a.backup }
17148
+ })
17149
+ })
17150
+ ];
17151
+ var themeTools = [
17152
+ tool({
17153
+ name: "wp_theme_updates",
17154
+ 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.",
17155
+ parameters: { type: "object", properties: {} },
17156
+ group: "site",
17157
+ need: READ,
17158
+ risk: "R0",
17159
+ gates: ["WP"],
17160
+ run: (ctx) => call(siteOf(ctx), { path: "/maggie-core/v1/update", query: { fresh: 1 } })
17161
+ }),
17162
+ tool({
17163
+ name: "wp_theme_update",
17164
+ 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.",
17165
+ parameters: {
17166
+ type: "object",
17167
+ properties: { rollback_to: str2("Put this kept version back instead (from wp_theme_updates backups).") }
17168
+ },
17169
+ group: "site",
17170
+ need: WRITE,
17171
+ risk: "W3",
17172
+ gates: [...GATES],
17173
+ confirm: (a, lang) => a.rollback_to ? say(
17174
+ lang,
17175
+ `Put Maggie Core ${String(a.rollback_to)} back on your website.`,
17176
+ `\u628A\u7DB2\u7AD9\u7684 Maggie Core \u9084\u539F\u5230 ${String(a.rollback_to)}\u3002`
17177
+ ) : say(
17178
+ lang,
17179
+ "Update Maggie Core on your website to the latest version (rolled back by itself if the site breaks).",
17180
+ "\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"
17181
+ ),
17182
+ run: async (ctx, a) => {
17183
+ const site = siteOf(ctx);
17184
+ const before = await homeShape(site);
17185
+ if (a.rollback_to) {
17186
+ const r2 = await call(site, {
17187
+ method: "POST",
17188
+ path: "/maggie-core/v1/update/rollback",
17189
+ body: { version: a.rollback_to }
17190
+ });
17191
+ return { ...r2, check: compareShapes(before, await homeShape(site)) };
17192
+ }
17193
+ const info = await call(site, { path: "/maggie-core/v1/update", query: { fresh: 1 } });
17194
+ if (!info.available)
17195
+ return {
17196
+ updated: false,
17197
+ current: info.current,
17198
+ latest: info.latest,
17199
+ note: info.check_error ?? "Already the latest Maggie Core."
17200
+ };
17201
+ if (!info.can_update)
17202
+ throw new Error(info.update_issue ?? "This host doesn't let WordPress update themes itself.");
17203
+ const r = await call(site, {
17204
+ method: "POST",
17205
+ path: "/maggie-core/v1/update"
17206
+ });
17207
+ const check2 = compareShapes(before, await homeShape(site));
17208
+ const broken = check2.checked && (check2.problems.some((p) => p.startsWith("the homepage answers")) || (check2.same_text_percent ?? 100) < 60);
17209
+ if (broken) {
17210
+ const back = await call(site, {
17211
+ method: "POST",
17212
+ path: "/maggie-core/v1/update/rollback",
17213
+ body: { version: r.before }
17214
+ }).catch((e) => ({ error: e.message }));
17215
+ return {
17216
+ ...r,
17217
+ check: check2,
17218
+ rolled_back: back,
17219
+ note: `The homepage looked broken after the update, so Maggie Core ${r.before} was put back.`
17220
+ };
17221
+ }
17222
+ return { ...r, check: check2, notes: info.notes, undo: `wp_theme_update rollback_to ${r.before}` };
17223
+ }
17224
+ })
17225
+ ];
17226
+ function deepMerge(a, b) {
17227
+ const out = { ...a };
17228
+ for (const [k, v] of Object.entries(b)) {
17229
+ const cur = out[k];
17230
+ out[k] = v && typeof v === "object" && !Array.isArray(v) && cur && typeof cur === "object" && !Array.isArray(cur) ? deepMerge(cur, v) : v;
17231
+ }
17232
+ return out;
17233
+ }
16869
17234
  var seoFields = {
16870
17235
  title: str2("SEO title (\u224860 characters; the site name is added by the theme)."),
16871
17236
  description: str2("Meta description (\u2248150-160 characters)."),
@@ -17126,6 +17491,8 @@ var wordpressTools = [
17126
17491
  ...pageTools,
17127
17492
  ...sectionTools,
17128
17493
  ...mediaTools,
17494
+ ...childTools,
17495
+ ...themeTools,
17129
17496
  ...seoTools,
17130
17497
  ...elementTools
17131
17498
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
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",