@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.
- package/dist/cmo.mjs +370 -8
- 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) =>
|
|
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).
|
|
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.
|
|
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.
|
|
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",
|