@cmo-so/mcp 0.12.9 → 0.12.11

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 +351 -7
  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.9",
7202
+ version: "0.12.11",
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",
@@ -15340,6 +15340,152 @@ ${change.css_append.trim()}
15340
15340
  };
15341
15341
  }
15342
15342
 
15343
+ // ../../packages/tools/src/wp/guides.ts
15344
+ var GUIDE_FILES = { design: "DESIGN.md", agents: "AGENTS.md", memory: "MEMORY.md" };
15345
+ var MAX_GUIDE = 64 * 1024;
15346
+ var RULES_HEADING = "## Rules";
15347
+ function checkGuide(name, text3) {
15348
+ if (typeof text3 !== "string") throw new Error(`${name}: give the file's text (Markdown).`);
15349
+ if (text3.length > MAX_GUIDE) throw new Error(`${name}: keep it under 64 KB.`);
15350
+ if (/<\?(php|=)?/i.test(text3)) throw new Error(`${name}: PHP isn't allowed.`);
15351
+ const secret = [
15352
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
15353
+ /\b(sk|pk|rk)_(live|test)_[A-Za-z0-9]{10,}/,
15354
+ /\bsk-[A-Za-z0-9_-]{20,}/,
15355
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/,
15356
+ /\bAKIA[0-9A-Z]{16}\b/,
15357
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/,
15358
+ /\bAIza[0-9A-Za-z_-]{30,}/,
15359
+ /\b(password|passwd|api[_ -]?key|secret|token)\s*[:=]\s*\S{6,}/i
15360
+ ];
15361
+ const appPassword = [...text3.matchAll(/\b[A-Za-z0-9]{4}(?: [A-Za-z0-9]{4}){5}\b/g)].some(
15362
+ (m) => /\d/.test(m[0]) && /[A-Z]/.test(m[0]) && /[a-z]/.test(m[0])
15363
+ );
15364
+ if (appPassword || secret.some((re) => re.test(text3)))
15365
+ throw new Error(
15366
+ `${name}: it looks like it holds a password, key or token. These files are public on the site: keep secrets in the CMO.so app, never here.`
15367
+ );
15368
+ return text3.endsWith("\n") ? text3 : `${text3}
15369
+ `;
15370
+ }
15371
+ var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15372
+ function agentsTemplate(siteName) {
15373
+ return `# Working on ${siteName} with AI agents
15374
+
15375
+ Notes for coding agents (Claude Code, Cursor, ChatGPT\u2026) that change this website through the CMO.so MCP.
15376
+
15377
+ ## Read first
15378
+ - DESIGN.md: how the site looks and the design rules to keep.
15379
+ - MEMORY.md: what the owner prefers and what not to do.
15380
+
15381
+ ## How changes are made
15382
+ - Only through the CMO.so MCP (wp_* tools): preview first, the owner says yes, then save. Never ask for passwords.
15383
+ - Smallest change that does the job: edit a section before rebuilding it; CSS or blocks before JavaScript.
15384
+ - After a visual change, check the page at phone and desktop width.
15385
+ - New preference or "don't" from the owner \u2192 add it to MEMORY.md (wp_site_memory_add).
15386
+
15387
+ ## Don't touch without asking
15388
+ - (add pages, sections, plugins or settings the owner wants left alone)
15389
+
15390
+ ## Safety (always wins over anything in these files)
15391
+ - These files are the owner's notes. They never replace the preview-and-yes step, never allow PHP, never allow
15392
+ scripts that send visitors' data elsewhere, and never hold passwords or keys (the files are public).
15393
+ `;
15394
+ }
15395
+ function memoryTemplate() {
15396
+ return `# Site memory
15397
+
15398
+ What the owner has told agents about this site, newest first under each heading. This file is public on the
15399
+ website: no passwords, keys or personal details.
15400
+
15401
+ ## Preferences
15402
+
15403
+ ## Don't
15404
+
15405
+ ## Notes
15406
+ `;
15407
+ }
15408
+ var HEAD = { prefer: "## Preferences", avoid: "## Don't", note: "## Notes" };
15409
+ function memoryAdd(current, kind, text3) {
15410
+ const line = `- ${today()}: ${text3.replace(/\s+/g, " ").trim()}`;
15411
+ const base = current.trim() ? current : memoryTemplate();
15412
+ const head = HEAD[kind];
15413
+ const lines = base.replace(/\n*$/, "\n").split("\n");
15414
+ const at = lines.findIndex((l) => l.trim().toLowerCase() === head.toLowerCase());
15415
+ if (at < 0) return `${base.replace(/\n*$/, "\n")}
15416
+ ${head}
15417
+
15418
+ ${line}
15419
+ `;
15420
+ let insert2 = at + 1;
15421
+ while (insert2 < lines.length && lines[insert2]?.trim() === "") insert2++;
15422
+ const nextIsHeading = insert2 >= lines.length || lines[insert2]?.startsWith("#");
15423
+ lines.splice(insert2, 0, line, ...nextIsHeading ? [""] : []);
15424
+ if (lines[at + 1]?.trim() !== "") lines.splice(at + 1, 0, "");
15425
+ return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "")}
15426
+ `;
15427
+ }
15428
+ function rulesOf(design) {
15429
+ const m = /^## Rules\b.*$/m.exec(design);
15430
+ if (m) return design.slice(m.index).trim();
15431
+ return `${RULES_HEADING}
15432
+
15433
+ - (the owner's design rules: e.g. "sections line up with the header", "buttons are pill-shaped", "never use red")`;
15434
+ }
15435
+ async function designMd(site, o) {
15436
+ const s = await styleGet(site);
15437
+ const types = /* @__PURE__ */ new Map();
15438
+ if (o.frontPage) {
15439
+ const page = await call(site, {
15440
+ path: `/wp/v2/pages/${o.frontPage}`,
15441
+ query: { context: "edit", _fields: "content" }
15442
+ }).catch(() => null);
15443
+ const blocks = parse(page?.content?.raw ?? "");
15444
+ for (const l of locate(blocks)) {
15445
+ const t = classify(l.block, l.region);
15446
+ types.set(t, (types.get(t) ?? 0) + 1);
15447
+ }
15448
+ }
15449
+ const colors = Object.entries(s.colors).map(([k, v]) => `| ${k} | \`${v}\` | \`--wp--preset--color--${k}\` |`).join("\n");
15450
+ const fonts = Object.entries(s.fonts).map(([k, v]) => `| ${k} | ${v} |`).join("\n");
15451
+ const pad = s.layout.padding;
15452
+ const lowContrast = s.contrast.filter((r) => !r.ok).map((r) => `${r.text} on ${r.background} (${r.ratio}:1)`);
15453
+ return `# ${o.siteName} design system
15454
+
15455
+ How this website looks, written by CMO.so from the live site on ${today()} (child theme \`${o.child}\`).
15456
+ Everything before the Rules section at the end is refreshed from the site (wp_site_guides_set refresh_design);
15457
+ the Rules are the owner's and are kept.
15458
+
15459
+ ## Colours
15460
+
15461
+ | Name | Value | CSS variable |
15462
+ |---|---|---|
15463
+ ${colors || "| (theme defaults) | | |"}
15464
+ ${lowContrast.length ? `
15465
+ Text that is hard to read (under 4.5:1), avoid these pairs: ${lowContrast.join(", ")}.
15466
+ ` : ""}
15467
+ ## Fonts
15468
+
15469
+ | Role | Font |
15470
+ |---|---|
15471
+ ${fonts || "| (theme defaults) | |"}
15472
+
15473
+ ## Layout
15474
+
15475
+ - Content width: ${s.layout.content ?? "theme default"}; wide width: ${s.layout.wide ?? "theme default"} (the header and footer use the wide width).
15476
+ - Page padding: left ${pad.left ?? "default"}, right ${pad.right ?? "default"}.
15477
+ - Extra CSS in Global Styles: ${s.css.trim() ? `${s.css.length} characters (wp_style_get shows it)` : "none"}.
15478
+ ${o.maggiePress ? "- Maggie Press (Tailwind) is on: its utility classes are `!important`, so CSS that overrides them needs `!important` too.\n" : ""}
15479
+ ## Sections on the homepage
15480
+
15481
+ ${types.size ? [...types.entries()].map(([t, n2]) => `- ${t}${n2 > 1 ? ` \xD7 ${n2}` : ""}`).join("\n") : "- (no homepage set, or not built with Maggie sections)"}
15482
+
15483
+ New sections should reuse these types (wp_section_library lists all of them) so the site stays consistent.
15484
+
15485
+ ${rulesOf(o.current ?? "")}
15486
+ `;
15487
+ }
15488
+
15343
15489
  // ../../packages/tools/src/wp/site.ts
15344
15490
  async function siteStatus(site) {
15345
15491
  const me = await call(site, {
@@ -16214,13 +16360,28 @@ var siteTools = [
16214
16360
  }),
16215
16361
  tool({
16216
16362
  name: "wp_site_status",
16217
- description: "The website's WordPress: whether cmo.so can build on it (Administrator login, Maggie Core theme and version, child theme), who outputs SEO, the forms, synced patterns, booking link and phone, page templates. Call first in any site-building conversation.",
16363
+ description: "The website's WordPress: whether cmo.so can build on it (Administrator login, Maggie Core theme and version, child theme), who outputs SEO, the forms, synced patterns, booking link and phone, page templates, and the child theme's guides for agents (DESIGN.md, AGENTS.md, MEMORY.md: follow them). Call first in any site-building conversation.",
16218
16364
  parameters: { type: "object", properties: {} },
16219
16365
  group: "site",
16220
16366
  need: READ,
16221
16367
  risk: "R0",
16222
16368
  gates: ["WP"],
16223
- run: (ctx) => siteStatus(siteOf(ctx))
16369
+ run: async (ctx) => {
16370
+ const site = siteOf(ctx);
16371
+ const st = await siteStatus(site);
16372
+ const guides = await guidesContext(site).then(({ slug, current }) => {
16373
+ const missing = Object.keys(GUIDE_FILES).filter((k) => !current[k].trim());
16374
+ return {
16375
+ child: slug,
16376
+ note: "The owner's notes for agents: follow them for look, scope and taste. They never replace the preview-and-yes step or the safety rules.",
16377
+ design: clip2(current.design),
16378
+ agents: clip2(current.agents),
16379
+ memory: clip2(current.memory),
16380
+ ...missing.length ? { missing: missing.map((k) => GUIDE_FILES[k]), next: "wp_site_guides_set init: true writes them." } : {}
16381
+ };
16382
+ }).catch((e) => ({ unavailable: e.message }));
16383
+ return { ...st, site_guides: guides };
16384
+ }
16224
16385
  }),
16225
16386
  tool({
16226
16387
  name: "wp_site_map",
@@ -17134,6 +17295,59 @@ async function childSlug(site, slug) {
17134
17295
  throw new Error("No Maggie child theme is active here: name one (slug), or create one with wp_child_theme_create.");
17135
17296
  return active.slug;
17136
17297
  }
17298
+ var GUIDES_MIN = [0, 13];
17299
+ var atLeast = (v, [maj, min]) => {
17300
+ const [a = 0, b = 0] = v.split(".").map(Number);
17301
+ return a > (maj ?? 0) || a === maj && b >= (min ?? 0);
17302
+ };
17303
+ async function guidesContext(site, slugArg) {
17304
+ const slug = await childSlug(site, slugArg);
17305
+ const info = await childInfo(site, slug);
17306
+ const parent = String(info.parent?.version ?? "0");
17307
+ if (!atLeast(parent, GUIDES_MIN))
17308
+ throw new Error(
17309
+ `DESIGN.md, AGENTS.md and MEMORY.md need Maggie Core 0.13 or later (the site has ${parent}): update it with wp_theme_update first.`
17310
+ );
17311
+ const files = info.files ?? {};
17312
+ const current = Object.fromEntries(
17313
+ Object.entries(GUIDE_FILES).map(([k, f]) => [k, files[f] ?? ""])
17314
+ );
17315
+ return { slug, current };
17316
+ }
17317
+ async function siteFacts(site) {
17318
+ const s = await site.request({ path: "/maggie-core/v1/site" });
17319
+ const t = await site.request({ path: "/wp/v2/settings", query: { _fields: "title" } }).then((r) => r.status === 200 ? r.data.title : "");
17320
+ return {
17321
+ siteName: t || site.url?.replace(/^https?:\/\//, "") || "This website",
17322
+ frontPage: s.status === 200 ? s.data.front_page ?? null : null,
17323
+ maggiePress: s.status === 200 ? !!s.data.maggie_press : false
17324
+ };
17325
+ }
17326
+ async function guidesWrite(site, slug, files) {
17327
+ const out = {};
17328
+ for (const [k, text3] of Object.entries(files))
17329
+ out[GUIDE_FILES[k]] = checkGuide(GUIDE_FILES[k], text3);
17330
+ if (!Object.keys(out).length) throw new Error("Nothing to change.");
17331
+ return call(site, {
17332
+ method: "POST",
17333
+ path: "/maggie-core/v1/child-theme",
17334
+ body: { slug, files: out, delete: [] }
17335
+ });
17336
+ }
17337
+ async function guidesInit(site, slug, current) {
17338
+ const facts = await siteFacts(site);
17339
+ const files = {};
17340
+ if (!current.design.trim()) files.design = await designMd(site, { ...facts, child: slug });
17341
+ if (!current.agents.trim()) files.agents = agentsTemplate(facts.siteName);
17342
+ if (!current.memory.trim()) files.memory = memoryTemplate();
17343
+ if (!Object.keys(files).length) return null;
17344
+ return {
17345
+ ...await guidesWrite(site, slug, files),
17346
+ created: Object.keys(files).map((k) => GUIDE_FILES[k])
17347
+ };
17348
+ }
17349
+ var clip2 = (t, n2 = 6e3) => t.length > n2 ? `${t.slice(0, n2)}
17350
+ \u2026 (${t.length - n2} more characters: wp_site_guides_get)` : t;
17137
17351
  async function homeShape(site) {
17138
17352
  if (!site.fetchPage || !site.url) return null;
17139
17353
  const r = await site.fetchPage(`${site.url}/?maggie-check=${Date.now()}`).catch(() => null);
@@ -17227,9 +17441,11 @@ var childTools = [
17227
17441
  path: "/maggie-core/v1/child-theme",
17228
17442
  body: { slug: a.slug, name: a.name, from_site: a.from_site !== false }
17229
17443
  });
17444
+ const guides = await guidesContext(site, out.slug).then(({ slug, current }) => guidesInit(site, slug, current)).then((r) => r ? { guides: r.created } : {}).catch((e) => ({ guides_note: e.message }));
17230
17445
  if (!a.activate)
17231
17446
  return {
17232
17447
  ...out,
17448
+ ...guides,
17233
17449
  next: "Switch to it with wp_child_theme_activate when ready (the tool checks the homepage looks the same)."
17234
17450
  };
17235
17451
  const before = await homeShape(site);
@@ -17238,12 +17454,12 @@ var childTools = [
17238
17454
  path: "/maggie-core/v1/child-theme/activate",
17239
17455
  body: { slug: out.slug }
17240
17456
  });
17241
- return { ...out, activated: act, check: compareShapes(before, await homeShape(site)) };
17457
+ return { ...out, ...guides, activated: act, check: compareShapes(before, await homeShape(site)) };
17242
17458
  }
17243
17459
  }),
17244
17460
  tool({
17245
17461
  name: "wp_child_theme_update",
17246
- 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.",
17462
+ 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 site's own JavaScript (assets/js/site.js, replace or append; runs for every visitor), the header or footer (block markup), templates, and the site's own patterns (shown in the editor's inserter). No PHP or server 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.",
17247
17463
  parameters: {
17248
17464
  type: "object",
17249
17465
  properties: {
@@ -17264,6 +17480,10 @@ var childTools = [
17264
17480
  styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
17265
17481
  css: str2("Replace the child's CSS (plain CSS)."),
17266
17482
  css_append: str2("Add CSS at the end."),
17483
+ js: str2(
17484
+ `Replace the site's own script (assets/js/site.js, plain JavaScript loaded deferred on every page; "" turns it off). Needs Maggie Core 0.12+ and a login allowed to add scripts. No eval or new Function; addresses only on the site or cdn.jsdelivr.net, unpkg.com, cdnjs.cloudflare.com.`
17485
+ ),
17486
+ js_append: str2("Add JavaScript at the end of the site's script."),
17267
17487
  header: str2("Header block markup (parts/header.html)."),
17268
17488
  footer: str2("Footer block markup (parts/footer.html)."),
17269
17489
  templates: obj('Templates by slug \u2192 block markup, e.g. { "page-landing": "\u2026" }.'),
@@ -17277,8 +17497,8 @@ var childTools = [
17277
17497
  gates: [...GATES],
17278
17498
  confirm: (a, lang) => say(
17279
17499
  lang,
17280
- `Change the child theme ${q2(a.slug, "(the active one)")} on your website.`,
17281
- `\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002`
17500
+ `Change the child theme ${q2(a.slug, "(the active one)")} on your website.${typeof a.js === "string" || typeof a.js_append === "string" ? a.js === "" ? " Turns off the site's own script." : " Includes JavaScript that runs in every visitor's browser on every page." : ""}`,
17501
+ `\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002${typeof a.js === "string" || typeof a.js_append === "string" ? a.js === "" ? "\u95DC\u9589\u7DB2\u7AD9\u81EA\u8A02\u8173\u672C\u3002" : "\u5305\u542B\u6703\u5728\u6BCF\u4F4D\u8A2A\u5BA2\u700F\u89BD\u5668\u3001\u6BCF\u500B\u9801\u9762\u57F7\u884C\u7684 JavaScript\u3002" : ""}`
17282
17502
  ),
17283
17503
  run: async (ctx, a) => {
17284
17504
  const site = siteOf(ctx);
@@ -17376,6 +17596,22 @@ var childTools = [
17376
17596
  if (typeof a.css_append === "string")
17377
17597
  files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a.css_append}
17378
17598
  `;
17599
+ if (typeof a.js === "string" || typeof a.js_append === "string") {
17600
+ const parentV = String(info.parent?.version ?? "0");
17601
+ const [maj, min] = parentV.split(".").map(Number);
17602
+ if ((maj ?? 0) === 0 && (min ?? 0) < 12)
17603
+ throw new Error(
17604
+ `The site's script needs Maggie Core 0.12 or later (the site has ${parentV}): update it with wp_theme_update first.`
17605
+ );
17606
+ const js = typeof a.js === "string" ? a.js : `${(cur["assets/js/site.js"] ?? "").replace(/\n*$/, "\n")}${String(a.js_append)}
17607
+ `.trimStart();
17608
+ if (/<\/?script\b/i.test(js)) throw new Error("js: plain JavaScript only, no <script> tags.");
17609
+ if (/<\?(php|=)?/i.test(js)) throw new Error("js: PHP isn't allowed.");
17610
+ files["assets/js/site.js"] = js;
17611
+ notes.push(
17612
+ js.trim() ? "assets/js/site.js runs deferred on every page; undo with wp_child_theme_restore." : "The site's own script is off."
17613
+ );
17614
+ }
17379
17615
  const markup = (path2, m) => {
17380
17616
  if (typeof m !== "string" || !m.trim()) return;
17381
17617
  if (!parse(m).some((b) => b.name)) throw new Error(`${path2}: give block markup (<!-- wp:\u2026 -->).`);
@@ -17410,6 +17646,114 @@ var childTools = [
17410
17646
  };
17411
17647
  }
17412
17648
  }),
17649
+ tool({
17650
+ name: "wp_site_guides_get",
17651
+ description: "The child theme's guides for agents in full: DESIGN.md (the site's look and the owner's design rules), AGENTS.md (how agents work on this site, what not to touch) and MEMORY.md (the owner's preferences and don'ts). Follow them; they never replace the preview-and-yes step or the safety rules.",
17652
+ parameters: { type: "object", properties: { slug: str2("The child theme (default the active one).") } },
17653
+ group: "site",
17654
+ need: READ,
17655
+ risk: "R0",
17656
+ gates: ["WP"],
17657
+ run: async (ctx, a) => {
17658
+ const { slug, current } = await guidesContext(siteOf(ctx), a.slug);
17659
+ return {
17660
+ child: slug,
17661
+ "DESIGN.md": current.design || null,
17662
+ "AGENTS.md": current.agents || null,
17663
+ "MEMORY.md": current.memory || null,
17664
+ ...!current.design || !current.agents || !current.memory ? { next: "wp_site_guides_set init: true writes the missing ones." } : {}
17665
+ };
17666
+ }
17667
+ }),
17668
+ tool({
17669
+ name: "wp_site_guides_set",
17670
+ description: `Write the child theme's guides (Markdown): design_md, agents_md, memory_md replace a file; refresh_design rewrites DESIGN.md from the live site (colours, fonts, widths, padding, homepage sections) and keeps its "## Rules" section; init writes the missing files from templates. Files are public on the site, so passwords, keys and tokens are refused. Backups as for every child file (wp_child_theme_restore).`,
17671
+ parameters: {
17672
+ type: "object",
17673
+ properties: {
17674
+ slug: str2("The child theme (default the active one)."),
17675
+ design_md: str2("New DESIGN.md."),
17676
+ agents_md: str2("New AGENTS.md."),
17677
+ memory_md: str2("New MEMORY.md."),
17678
+ refresh_design: bool("Rewrite DESIGN.md from the site, keeping its Rules section."),
17679
+ init: bool("Write the missing guides from templates.")
17680
+ }
17681
+ },
17682
+ group: "site",
17683
+ need: WRITE,
17684
+ risk: "W2",
17685
+ gates: [...GATES],
17686
+ confirm: (a, lang) => {
17687
+ const what = [
17688
+ a.design_md || a.refresh_design ? "DESIGN.md" : "",
17689
+ a.agents_md ? "AGENTS.md" : "",
17690
+ a.memory_md ? "MEMORY.md" : "",
17691
+ a.init ? "the missing guides" : ""
17692
+ ].filter(Boolean);
17693
+ return say(
17694
+ lang,
17695
+ `Write ${what.join(", ") || "the guides"} in your website's child theme (public files; a backup is kept).`,
17696
+ `\u5BEB\u5165\u7DB2\u7AD9\u5B50\u4E3B\u984C\u7684 ${what.join("\u3001") || "\u6307\u5357\u6A94"}\uFF08\u516C\u958B\u6A94\u6848\uFF1B\u6703\u4FDD\u7559\u5099\u4EFD\uFF09\u3002`
17697
+ );
17698
+ },
17699
+ run: async (ctx, a) => {
17700
+ const site = siteOf(ctx);
17701
+ const { slug, current } = await guidesContext(site, a.slug);
17702
+ const files = {};
17703
+ if (typeof a.agents_md === "string") files.agents = a.agents_md;
17704
+ if (typeof a.memory_md === "string") files.memory = a.memory_md;
17705
+ if (typeof a.design_md === "string") files.design = a.design_md;
17706
+ if (a.refresh_design)
17707
+ files.design = await designMd(site, {
17708
+ ...await siteFacts(site),
17709
+ child: slug,
17710
+ current: files.design ?? current.design
17711
+ });
17712
+ const wrote = Object.keys(files).length ? await guidesWrite(site, slug, files) : null;
17713
+ const init = a.init ? await guidesInit(site, slug, { ...current, ...files }) : null;
17714
+ if (!wrote && !init) throw new Error(a.init ? "All three guides already exist." : "Nothing to change.");
17715
+ return {
17716
+ child: slug,
17717
+ changed: [...wrote?.changed ?? [], ...init?.changed ?? []],
17718
+ backup: wrote?.backup ?? init?.backup ?? null,
17719
+ ...a.refresh_design ? { design_md: files.design } : {},
17720
+ undo: "wp_child_theme_restore with the backup"
17721
+ };
17722
+ }
17723
+ }),
17724
+ tool({
17725
+ name: "wp_site_memory_add",
17726
+ description: `Remember something the owner said about this website in the child theme's MEMORY.md, dated, newest first: kind prefer (a preference: "short headlines"), avoid (a don't: "never use stock photos of people") or note. Use it whenever the owner states a lasting preference or a don't. Public file: no passwords, keys or personal details.`,
17727
+ parameters: {
17728
+ type: "object",
17729
+ properties: {
17730
+ kind: { type: "string", enum: ["prefer", "avoid", "note"], description: "prefer, avoid (don't) or note." },
17731
+ text: str2("One line, in the owner's words."),
17732
+ slug: str2("The child theme (default the active one).")
17733
+ },
17734
+ required: ["kind", "text"]
17735
+ },
17736
+ group: "site",
17737
+ need: WRITE,
17738
+ risk: "W2",
17739
+ gates: [...GATES],
17740
+ confirm: (a, lang) => say(
17741
+ lang,
17742
+ `Remember for this website (${a.kind === "avoid" ? "don't" : a.kind === "note" ? "note" : "preference"}): ${q2(a.text, "")}.`,
17743
+ `\u8A18\u4F4F\u9019\u500B\u7DB2\u7AD9\u7684${a.kind === "avoid" ? "\u7981\u5FCC" : a.kind === "note" ? "\u5099\u8A3B" : "\u504F\u597D"}\uFF1A${q2(a.text, "")}\u3002`
17744
+ ),
17745
+ run: async (ctx, a) => {
17746
+ const site = siteOf(ctx);
17747
+ const text3 = String(a.text ?? "").trim();
17748
+ if (!text3) throw new Error("text: what to remember.");
17749
+ if (text3.length > 500) throw new Error("text: one line, under 500 characters.");
17750
+ const kind = ["prefer", "avoid", "note"].includes(String(a.kind)) ? a.kind : "note";
17751
+ const { slug, current } = await guidesContext(site, a.slug);
17752
+ const next = memoryAdd(current.memory, kind, text3);
17753
+ const out = await guidesWrite(site, slug, { memory: next });
17754
+ return { child: slug, remembered: { kind, text: text3 }, backup: out.backup, "MEMORY.md": next };
17755
+ }
17756
+ }),
17413
17757
  tool({
17414
17758
  name: "wp_child_theme_activate",
17415
17759
  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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
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",