@cmo-so/cli 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.mjs +370 -8
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -15457,6 +15457,152 @@ database and win over both themes. Export them into this child (Site Editor \u21
15457
15457
  return files;
15458
15458
  }
15459
15459
 
15460
+ // ../../packages/tools/src/wp/guides.ts
15461
+ var GUIDE_FILES = { design: "DESIGN.md", agents: "AGENTS.md", memory: "MEMORY.md" };
15462
+ var MAX_GUIDE = 64 * 1024;
15463
+ var RULES_HEADING = "## Rules";
15464
+ function checkGuide(name, text3) {
15465
+ if (typeof text3 !== "string") throw new Error(`${name}: give the file's text (Markdown).`);
15466
+ if (text3.length > MAX_GUIDE) throw new Error(`${name}: keep it under 64 KB.`);
15467
+ if (/<\?(php|=)?/i.test(text3)) throw new Error(`${name}: PHP isn't allowed.`);
15468
+ const secret2 = [
15469
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
15470
+ /\b(sk|pk|rk)_(live|test)_[A-Za-z0-9]{10,}/,
15471
+ /\bsk-[A-Za-z0-9_-]{20,}/,
15472
+ /\bgh[pousr]_[A-Za-z0-9]{20,}/,
15473
+ /\bAKIA[0-9A-Z]{16}\b/,
15474
+ /\bxox[abposr]-[A-Za-z0-9-]{10,}/,
15475
+ /\bAIza[0-9A-Za-z_-]{30,}/,
15476
+ /\b(password|passwd|api[_ -]?key|secret|token)\s*[:=]\s*\S{6,}/i
15477
+ ];
15478
+ const appPassword = [...text3.matchAll(/\b[A-Za-z0-9]{4}(?: [A-Za-z0-9]{4}){5}\b/g)].some(
15479
+ (m) => /\d/.test(m[0]) && /[A-Z]/.test(m[0]) && /[a-z]/.test(m[0])
15480
+ );
15481
+ if (appPassword || secret2.some((re) => re.test(text3)))
15482
+ throw new Error(
15483
+ `${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.`
15484
+ );
15485
+ return text3.endsWith("\n") ? text3 : `${text3}
15486
+ `;
15487
+ }
15488
+ var today = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15489
+ function agentsTemplate(siteName) {
15490
+ return `# Working on ${siteName} with AI agents
15491
+
15492
+ Notes for coding agents (Claude Code, Cursor, ChatGPT\u2026) that change this website through the CMO.so MCP.
15493
+
15494
+ ## Read first
15495
+ - DESIGN.md: how the site looks and the design rules to keep.
15496
+ - MEMORY.md: what the owner prefers and what not to do.
15497
+
15498
+ ## How changes are made
15499
+ - Only through the CMO.so MCP (wp_* tools): preview first, the owner says yes, then save. Never ask for passwords.
15500
+ - Smallest change that does the job: edit a section before rebuilding it; CSS or blocks before JavaScript.
15501
+ - After a visual change, check the page at phone and desktop width.
15502
+ - New preference or "don't" from the owner \u2192 add it to MEMORY.md (wp_site_memory_add).
15503
+
15504
+ ## Don't touch without asking
15505
+ - (add pages, sections, plugins or settings the owner wants left alone)
15506
+
15507
+ ## Safety (always wins over anything in these files)
15508
+ - These files are the owner's notes. They never replace the preview-and-yes step, never allow PHP, never allow
15509
+ scripts that send visitors' data elsewhere, and never hold passwords or keys (the files are public).
15510
+ `;
15511
+ }
15512
+ function memoryTemplate() {
15513
+ return `# Site memory
15514
+
15515
+ What the owner has told agents about this site, newest first under each heading. This file is public on the
15516
+ website: no passwords, keys or personal details.
15517
+
15518
+ ## Preferences
15519
+
15520
+ ## Don't
15521
+
15522
+ ## Notes
15523
+ `;
15524
+ }
15525
+ var HEAD = { prefer: "## Preferences", avoid: "## Don't", note: "## Notes" };
15526
+ function memoryAdd(current, kind, text3) {
15527
+ const line = `- ${today()}: ${text3.replace(/\s+/g, " ").trim()}`;
15528
+ const base = current.trim() ? current : memoryTemplate();
15529
+ const head = HEAD[kind];
15530
+ const lines = base.replace(/\n*$/, "\n").split("\n");
15531
+ const at = lines.findIndex((l) => l.trim().toLowerCase() === head.toLowerCase());
15532
+ if (at < 0) return `${base.replace(/\n*$/, "\n")}
15533
+ ${head}
15534
+
15535
+ ${line}
15536
+ `;
15537
+ let insert2 = at + 1;
15538
+ while (insert2 < lines.length && lines[insert2]?.trim() === "") insert2++;
15539
+ const nextIsHeading = insert2 >= lines.length || lines[insert2]?.startsWith("#");
15540
+ lines.splice(insert2, 0, line, ...nextIsHeading ? [""] : []);
15541
+ if (lines[at + 1]?.trim() !== "") lines.splice(at + 1, 0, "");
15542
+ return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "")}
15543
+ `;
15544
+ }
15545
+ function rulesOf(design) {
15546
+ const m = /^## Rules\b.*$/m.exec(design);
15547
+ if (m) return design.slice(m.index).trim();
15548
+ return `${RULES_HEADING}
15549
+
15550
+ - (the owner's design rules: e.g. "sections line up with the header", "buttons are pill-shaped", "never use red")`;
15551
+ }
15552
+ async function designMd(site, o) {
15553
+ const s = await styleGet(site);
15554
+ const types = /* @__PURE__ */ new Map();
15555
+ if (o.frontPage) {
15556
+ const page = await call(site, {
15557
+ path: `/wp/v2/pages/${o.frontPage}`,
15558
+ query: { context: "edit", _fields: "content" }
15559
+ }).catch(() => null);
15560
+ const blocks = parse(page?.content?.raw ?? "");
15561
+ for (const l of locate(blocks)) {
15562
+ const t = classify(l.block, l.region);
15563
+ types.set(t, (types.get(t) ?? 0) + 1);
15564
+ }
15565
+ }
15566
+ const colors = Object.entries(s.colors).map(([k, v]) => `| ${k} | \`${v}\` | \`--wp--preset--color--${k}\` |`).join("\n");
15567
+ const fonts = Object.entries(s.fonts).map(([k, v]) => `| ${k} | ${v} |`).join("\n");
15568
+ const pad = s.layout.padding;
15569
+ const lowContrast = s.contrast.filter((r) => !r.ok).map((r) => `${r.text} on ${r.background} (${r.ratio}:1)`);
15570
+ return `# ${o.siteName} design system
15571
+
15572
+ How this website looks, written by CMO.so from the live site on ${today()} (child theme \`${o.child}\`).
15573
+ Everything before the Rules section at the end is refreshed from the site (wp_site_guides_set refresh_design);
15574
+ the Rules are the owner's and are kept.
15575
+
15576
+ ## Colours
15577
+
15578
+ | Name | Value | CSS variable |
15579
+ |---|---|---|
15580
+ ${colors || "| (theme defaults) | | |"}
15581
+ ${lowContrast.length ? `
15582
+ Text that is hard to read (under 4.5:1), avoid these pairs: ${lowContrast.join(", ")}.
15583
+ ` : ""}
15584
+ ## Fonts
15585
+
15586
+ | Role | Font |
15587
+ |---|---|
15588
+ ${fonts || "| (theme defaults) | |"}
15589
+
15590
+ ## Layout
15591
+
15592
+ - Content width: ${s.layout.content ?? "theme default"}; wide width: ${s.layout.wide ?? "theme default"} (the header and footer use the wide width).
15593
+ - Page padding: left ${pad.left ?? "default"}, right ${pad.right ?? "default"}.
15594
+ - Extra CSS in Global Styles: ${s.css.trim() ? `${s.css.length} characters (wp_style_get shows it)` : "none"}.
15595
+ ${o.maggiePress ? "- Maggie Press (Tailwind) is on: its utility classes are `!important`, so CSS that overrides them needs `!important` too.\n" : ""}
15596
+ ## Sections on the homepage
15597
+
15598
+ ${types.size ? [...types.entries()].map(([t, n2]) => `- ${t}${n2 > 1 ? ` \xD7 ${n2}` : ""}`).join("\n") : "- (no homepage set, or not built with Maggie sections)"}
15599
+
15600
+ New sections should reuse these types (wp_section_library lists all of them) so the site stays consistent.
15601
+
15602
+ ${rulesOf(o.current ?? "")}
15603
+ `;
15604
+ }
15605
+
15460
15606
  // ../../packages/tools/src/wp/site.ts
15461
15607
  async function siteStatus(site) {
15462
15608
  const me = await call(site, {
@@ -16331,13 +16477,28 @@ var siteTools = [
16331
16477
  }),
16332
16478
  tool({
16333
16479
  name: "wp_site_status",
16334
- 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.",
16480
+ 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.",
16335
16481
  parameters: { type: "object", properties: {} },
16336
16482
  group: "site",
16337
16483
  need: READ,
16338
16484
  risk: "R0",
16339
16485
  gates: ["WP"],
16340
- run: (ctx) => siteStatus(siteOf(ctx))
16486
+ run: async (ctx) => {
16487
+ const site = siteOf(ctx);
16488
+ const st = await siteStatus(site);
16489
+ const guides = await guidesContext(site).then(({ slug, current }) => {
16490
+ const missing = Object.keys(GUIDE_FILES).filter((k) => !current[k].trim());
16491
+ return {
16492
+ child: slug,
16493
+ 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.",
16494
+ design: clip2(current.design),
16495
+ agents: clip2(current.agents),
16496
+ memory: clip2(current.memory),
16497
+ ...missing.length ? { missing: missing.map((k) => GUIDE_FILES[k]), next: "wp_site_guides_set init: true writes them." } : {}
16498
+ };
16499
+ }).catch((e) => ({ unavailable: e.message }));
16500
+ return { ...st, site_guides: guides };
16501
+ }
16341
16502
  }),
16342
16503
  tool({
16343
16504
  name: "wp_site_map",
@@ -17251,6 +17412,59 @@ async function childSlug(site, slug) {
17251
17412
  throw new Error("No Maggie child theme is active here: name one (slug), or create one with wp_child_theme_create.");
17252
17413
  return active.slug;
17253
17414
  }
17415
+ var GUIDES_MIN = [0, 13];
17416
+ var atLeast = (v, [maj, min]) => {
17417
+ const [a = 0, b = 0] = v.split(".").map(Number);
17418
+ return a > (maj ?? 0) || a === maj && b >= (min ?? 0);
17419
+ };
17420
+ async function guidesContext(site, slugArg) {
17421
+ const slug = await childSlug(site, slugArg);
17422
+ const info = await childInfo(site, slug);
17423
+ const parent = String(info.parent?.version ?? "0");
17424
+ if (!atLeast(parent, GUIDES_MIN))
17425
+ throw new Error(
17426
+ `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.`
17427
+ );
17428
+ const files = info.files ?? {};
17429
+ const current = Object.fromEntries(
17430
+ Object.entries(GUIDE_FILES).map(([k, f]) => [k, files[f] ?? ""])
17431
+ );
17432
+ return { slug, current };
17433
+ }
17434
+ async function siteFacts(site) {
17435
+ const s = await site.request({ path: "/maggie-core/v1/site" });
17436
+ const t = await site.request({ path: "/wp/v2/settings", query: { _fields: "title" } }).then((r) => r.status === 200 ? r.data.title : "");
17437
+ return {
17438
+ siteName: t || site.url?.replace(/^https?:\/\//, "") || "This website",
17439
+ frontPage: s.status === 200 ? s.data.front_page ?? null : null,
17440
+ maggiePress: s.status === 200 ? !!s.data.maggie_press : false
17441
+ };
17442
+ }
17443
+ async function guidesWrite(site, slug, files) {
17444
+ const out = {};
17445
+ for (const [k, text3] of Object.entries(files))
17446
+ out[GUIDE_FILES[k]] = checkGuide(GUIDE_FILES[k], text3);
17447
+ if (!Object.keys(out).length) throw new Error("Nothing to change.");
17448
+ return call(site, {
17449
+ method: "POST",
17450
+ path: "/maggie-core/v1/child-theme",
17451
+ body: { slug, files: out, delete: [] }
17452
+ });
17453
+ }
17454
+ async function guidesInit(site, slug, current) {
17455
+ const facts = await siteFacts(site);
17456
+ const files = {};
17457
+ if (!current.design.trim()) files.design = await designMd(site, { ...facts, child: slug });
17458
+ if (!current.agents.trim()) files.agents = agentsTemplate(facts.siteName);
17459
+ if (!current.memory.trim()) files.memory = memoryTemplate();
17460
+ if (!Object.keys(files).length) return null;
17461
+ return {
17462
+ ...await guidesWrite(site, slug, files),
17463
+ created: Object.keys(files).map((k) => GUIDE_FILES[k])
17464
+ };
17465
+ }
17466
+ var clip2 = (t, n2 = 6e3) => t.length > n2 ? `${t.slice(0, n2)}
17467
+ \u2026 (${t.length - n2} more characters: wp_site_guides_get)` : t;
17254
17468
  async function homeShape(site) {
17255
17469
  if (!site.fetchPage || !site.url) return null;
17256
17470
  const r = await site.fetchPage(`${site.url}/?maggie-check=${Date.now()}`).catch(() => null);
@@ -17344,9 +17558,11 @@ var childTools = [
17344
17558
  path: "/maggie-core/v1/child-theme",
17345
17559
  body: { slug: a.slug, name: a.name, from_site: a.from_site !== false }
17346
17560
  });
17561
+ 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 }));
17347
17562
  if (!a.activate)
17348
17563
  return {
17349
17564
  ...out,
17565
+ ...guides,
17350
17566
  next: "Switch to it with wp_child_theme_activate when ready (the tool checks the homepage looks the same)."
17351
17567
  };
17352
17568
  const before = await homeShape(site);
@@ -17355,12 +17571,12 @@ var childTools = [
17355
17571
  path: "/maggie-core/v1/child-theme/activate",
17356
17572
  body: { slug: out.slug }
17357
17573
  });
17358
- return { ...out, activated: act2, check: compareShapes(before, await homeShape(site)) };
17574
+ return { ...out, ...guides, activated: act2, check: compareShapes(before, await homeShape(site)) };
17359
17575
  }
17360
17576
  }),
17361
17577
  tool({
17362
17578
  name: "wp_child_theme_update",
17363
- 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.",
17579
+ 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.",
17364
17580
  parameters: {
17365
17581
  type: "object",
17366
17582
  properties: {
@@ -17381,6 +17597,10 @@ var childTools = [
17381
17597
  styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
17382
17598
  css: str2("Replace the child's CSS (plain CSS)."),
17383
17599
  css_append: str2("Add CSS at the end."),
17600
+ js: str2(
17601
+ `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.`
17602
+ ),
17603
+ js_append: str2("Add JavaScript at the end of the site's script."),
17384
17604
  header: str2("Header block markup (parts/header.html)."),
17385
17605
  footer: str2("Footer block markup (parts/footer.html)."),
17386
17606
  templates: obj('Templates by slug \u2192 block markup, e.g. { "page-landing": "\u2026" }.'),
@@ -17394,8 +17614,8 @@ var childTools = [
17394
17614
  gates: [...GATES],
17395
17615
  confirm: (a, lang) => say(
17396
17616
  lang,
17397
- `Change the child theme ${q2(a.slug, "(the active one)")} on your website.`,
17398
- `\u4FEE\u6539\u7DB2\u7AD9\u7684\u5B50\u4E3B\u984C ${q2(a.slug, "\uFF08\u76EE\u524D\u4F7F\u7528\u4E2D\u7684\uFF09")}\u3002`
17617
+ `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." : ""}`,
17618
+ `\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" : ""}`
17399
17619
  ),
17400
17620
  run: async (ctx, a) => {
17401
17621
  const site = siteOf(ctx);
@@ -17493,6 +17713,22 @@ var childTools = [
17493
17713
  if (typeof a.css_append === "string")
17494
17714
  files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a.css_append}
17495
17715
  `;
17716
+ if (typeof a.js === "string" || typeof a.js_append === "string") {
17717
+ const parentV = String(info.parent?.version ?? "0");
17718
+ const [maj, min] = parentV.split(".").map(Number);
17719
+ if ((maj ?? 0) === 0 && (min ?? 0) < 12)
17720
+ throw new Error(
17721
+ `The site's script needs Maggie Core 0.12 or later (the site has ${parentV}): update it with wp_theme_update first.`
17722
+ );
17723
+ const js = typeof a.js === "string" ? a.js : `${(cur["assets/js/site.js"] ?? "").replace(/\n*$/, "\n")}${String(a.js_append)}
17724
+ `.trimStart();
17725
+ if (/<\/?script\b/i.test(js)) throw new Error("js: plain JavaScript only, no <script> tags.");
17726
+ if (/<\?(php|=)?/i.test(js)) throw new Error("js: PHP isn't allowed.");
17727
+ files["assets/js/site.js"] = js;
17728
+ notes.push(
17729
+ js.trim() ? "assets/js/site.js runs deferred on every page; undo with wp_child_theme_restore." : "The site's own script is off."
17730
+ );
17731
+ }
17496
17732
  const markup = (path3, m) => {
17497
17733
  if (typeof m !== "string" || !m.trim()) return;
17498
17734
  if (!parse(m).some((b) => b.name)) throw new Error(`${path3}: give block markup (<!-- wp:\u2026 -->).`);
@@ -17527,6 +17763,114 @@ var childTools = [
17527
17763
  };
17528
17764
  }
17529
17765
  }),
17766
+ tool({
17767
+ name: "wp_site_guides_get",
17768
+ 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.",
17769
+ parameters: { type: "object", properties: { slug: str2("The child theme (default the active one).") } },
17770
+ group: "site",
17771
+ need: READ,
17772
+ risk: "R0",
17773
+ gates: ["WP"],
17774
+ run: async (ctx, a) => {
17775
+ const { slug, current } = await guidesContext(siteOf(ctx), a.slug);
17776
+ return {
17777
+ child: slug,
17778
+ "DESIGN.md": current.design || null,
17779
+ "AGENTS.md": current.agents || null,
17780
+ "MEMORY.md": current.memory || null,
17781
+ ...!current.design || !current.agents || !current.memory ? { next: "wp_site_guides_set init: true writes the missing ones." } : {}
17782
+ };
17783
+ }
17784
+ }),
17785
+ tool({
17786
+ name: "wp_site_guides_set",
17787
+ 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).`,
17788
+ parameters: {
17789
+ type: "object",
17790
+ properties: {
17791
+ slug: str2("The child theme (default the active one)."),
17792
+ design_md: str2("New DESIGN.md."),
17793
+ agents_md: str2("New AGENTS.md."),
17794
+ memory_md: str2("New MEMORY.md."),
17795
+ refresh_design: bool("Rewrite DESIGN.md from the site, keeping its Rules section."),
17796
+ init: bool("Write the missing guides from templates.")
17797
+ }
17798
+ },
17799
+ group: "site",
17800
+ need: WRITE,
17801
+ risk: "W2",
17802
+ gates: [...GATES],
17803
+ confirm: (a, lang) => {
17804
+ const what = [
17805
+ a.design_md || a.refresh_design ? "DESIGN.md" : "",
17806
+ a.agents_md ? "AGENTS.md" : "",
17807
+ a.memory_md ? "MEMORY.md" : "",
17808
+ a.init ? "the missing guides" : ""
17809
+ ].filter(Boolean);
17810
+ return say(
17811
+ lang,
17812
+ `Write ${what.join(", ") || "the guides"} in your website's child theme (public files; a backup is kept).`,
17813
+ `\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`
17814
+ );
17815
+ },
17816
+ run: async (ctx, a) => {
17817
+ const site = siteOf(ctx);
17818
+ const { slug, current } = await guidesContext(site, a.slug);
17819
+ const files = {};
17820
+ if (typeof a.agents_md === "string") files.agents = a.agents_md;
17821
+ if (typeof a.memory_md === "string") files.memory = a.memory_md;
17822
+ if (typeof a.design_md === "string") files.design = a.design_md;
17823
+ if (a.refresh_design)
17824
+ files.design = await designMd(site, {
17825
+ ...await siteFacts(site),
17826
+ child: slug,
17827
+ current: files.design ?? current.design
17828
+ });
17829
+ const wrote = Object.keys(files).length ? await guidesWrite(site, slug, files) : null;
17830
+ const init = a.init ? await guidesInit(site, slug, { ...current, ...files }) : null;
17831
+ if (!wrote && !init) throw new Error(a.init ? "All three guides already exist." : "Nothing to change.");
17832
+ return {
17833
+ child: slug,
17834
+ changed: [...wrote?.changed ?? [], ...init?.changed ?? []],
17835
+ backup: wrote?.backup ?? init?.backup ?? null,
17836
+ ...a.refresh_design ? { design_md: files.design } : {},
17837
+ undo: "wp_child_theme_restore with the backup"
17838
+ };
17839
+ }
17840
+ }),
17841
+ tool({
17842
+ name: "wp_site_memory_add",
17843
+ 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.`,
17844
+ parameters: {
17845
+ type: "object",
17846
+ properties: {
17847
+ kind: { type: "string", enum: ["prefer", "avoid", "note"], description: "prefer, avoid (don't) or note." },
17848
+ text: str2("One line, in the owner's words."),
17849
+ slug: str2("The child theme (default the active one).")
17850
+ },
17851
+ required: ["kind", "text"]
17852
+ },
17853
+ group: "site",
17854
+ need: WRITE,
17855
+ risk: "W2",
17856
+ gates: [...GATES],
17857
+ confirm: (a, lang) => say(
17858
+ lang,
17859
+ `Remember for this website (${a.kind === "avoid" ? "don't" : a.kind === "note" ? "note" : "preference"}): ${q2(a.text, "")}.`,
17860
+ `\u8A18\u4F4F\u9019\u500B\u7DB2\u7AD9\u7684${a.kind === "avoid" ? "\u7981\u5FCC" : a.kind === "note" ? "\u5099\u8A3B" : "\u504F\u597D"}\uFF1A${q2(a.text, "")}\u3002`
17861
+ ),
17862
+ run: async (ctx, a) => {
17863
+ const site = siteOf(ctx);
17864
+ const text3 = String(a.text ?? "").trim();
17865
+ if (!text3) throw new Error("text: what to remember.");
17866
+ if (text3.length > 500) throw new Error("text: one line, under 500 characters.");
17867
+ const kind = ["prefer", "avoid", "note"].includes(String(a.kind)) ? a.kind : "note";
17868
+ const { slug, current } = await guidesContext(site, a.slug);
17869
+ const next = memoryAdd(current.memory, kind, text3);
17870
+ const out = await guidesWrite(site, slug, { memory: next });
17871
+ return { child: slug, remembered: { kind, text: text3 }, backup: out.backup, "MEMORY.md": next };
17872
+ }
17873
+ }),
17530
17874
  tool({
17531
17875
  name: "wp_child_theme_activate",
17532
17876
  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.",
@@ -21908,7 +22252,7 @@ var program = new Command();
21908
22252
  // package.json
21909
22253
  var package_default = {
21910
22254
  name: "@cmo-so/cli",
21911
- version: "0.12.9",
22255
+ version: "0.12.11",
21912
22256
  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.",
21913
22257
  type: "module",
21914
22258
  license: "MIT",
@@ -33156,12 +33500,15 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
33156
33500
  ).option(
33157
33501
  "--from <file>",
33158
33502
  "JSON: { colors, fonts, styles, css, css_append, header, footer, templates, patterns, remove_patterns }"
33159
- ).option("--color <slug=hex...>").option("--css-file <file>", "replace the CSS with this file").option("--header-file <file>", "header block markup").option("--footer-file <file>", "footer block markup").action(
33503
+ ).option("--color <slug=hex...>").option("--css-file <file>", "replace the CSS with this file").option("--js-file <file>", "replace the site's script (assets/js/site.js, runs on every page) with this file").option("--js-append <file>", "add this JavaScript file at the end of the site's script").option("--js-off", "turn the site's script off").option("--header-file <file>", "header block markup").option("--footer-file <file>", "footer block markup").action(
33160
33504
  async (slug, o, c) => {
33161
33505
  const args = o.from ? await json(o.from) : {};
33162
33506
  if (slug) args.slug = slug;
33163
33507
  if (o.color) args.colors = Object.fromEntries(pairs(o.color));
33164
33508
  if (o.cssFile) args.css = await readFile2(o.cssFile, "utf8");
33509
+ if (o.jsFile) args.js = await readFile2(o.jsFile, "utf8");
33510
+ if (o.jsAppend) args.js_append = await readFile2(o.jsAppend, "utf8");
33511
+ if (o.jsOff) args.js = "";
33165
33512
  if (o.headerFile) args.header = await readFile2(o.headerFile, "utf8");
33166
33513
  if (o.footerFile) args.footer = await readFile2(o.footerFile, "utf8");
33167
33514
  await run(g(c), "wp_child_theme_update", args);
@@ -33169,6 +33516,21 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
33169
33516
  );
33170
33517
  child.command("activate <slug>").description("switch the site to a child (or maggie-core to go back); checks the homepage before and after").action((slug, _o, c) => run(g(c), "wp_child_theme_activate", { slug }));
33171
33518
  child.command("restore <slug> <backup>").description("put a backup of the child's files back").action((slug, backup, _o, c) => run(g(c), "wp_child_theme_restore", { slug, backup }));
33519
+ const guides = wp.command("guides").description("the child theme's guides for agents: DESIGN.md, AGENTS.md, MEMORY.md");
33520
+ guides.command("get [slug]", { isDefault: true }).action((slug, _o, c) => run(g(c), "wp_site_guides_get", { slug }));
33521
+ guides.command("set [slug]").option("--design-file <file>", "replace DESIGN.md").option("--agents-file <file>", "replace AGENTS.md").option("--memory-file <file>", "replace MEMORY.md").option("--refresh-design", "rewrite DESIGN.md from the site, keeping its Rules").option("--init", "write the missing guides from templates").action(
33522
+ async (slug, o, c) => run(g(c), "wp_site_guides_set", {
33523
+ slug,
33524
+ design_md: o.designFile ? await readFile2(o.designFile, "utf8") : void 0,
33525
+ agents_md: o.agentsFile ? await readFile2(o.agentsFile, "utf8") : void 0,
33526
+ memory_md: o.memoryFile ? await readFile2(o.memoryFile, "utf8") : void 0,
33527
+ refresh_design: o.refreshDesign,
33528
+ init: o.init
33529
+ })
33530
+ );
33531
+ wp.command("remember <kind> <text...>").description("add to MEMORY.md: kind prefer, avoid (a don't) or note").action(
33532
+ (kind, text3, _o, c) => run(g(c), "wp_site_memory_add", { kind, text: text3.join(" ") })
33533
+ );
33172
33534
  wp.command("child-theme <slug>").description(
33173
33535
  "export the site's look as a child theme folder on this computer (to keep in git); cmo wp child works on the site itself"
33174
33536
  ).requiredOption("--name <name>", 'theme name, e.g. "Moon Lake Spa"').option("--out <dir>", "where to write it (default ./<slug>)").option("--from-site", "take the brand, header and footer from the connected site").option("--color <slug=hex...>", "brand colours").option("--font <slug=family...>", "fonts (replaces Maggie Core's font list: ship their files too)").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "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.",
5
5
  "type": "module",
6
6
  "license": "MIT",