@cmo-so/mcp 0.12.4 → 0.12.6
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-mcp.mjs +193 -9
- 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.
|
|
7202
|
+
version: "0.12.6",
|
|
7203
7203
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
7204
7204
|
type: "module",
|
|
7205
7205
|
license: "MIT",
|
|
@@ -14220,7 +14220,7 @@ var SECTIONS2 = [
|
|
|
14220
14220
|
[
|
|
14221
14221
|
S(s.heading) || S(s.intro) ? prose([...headed(s), para(S(s.intro))]) : "",
|
|
14222
14222
|
group(boxes, {
|
|
14223
|
-
cls: `maggie-stagger grid gap-5 ${boxes.length
|
|
14223
|
+
cls: `maggie-stagger grid gap-5 ${boxes.length === 3 ? "sm:grid-cols-3" : "sm:grid-cols-2"}`
|
|
14224
14224
|
})
|
|
14225
14225
|
],
|
|
14226
14226
|
"flex flex-col gap-8"
|
|
@@ -14443,6 +14443,7 @@ var SECTIONS2 = [
|
|
|
14443
14443
|
slots: {
|
|
14444
14444
|
title: str('e.g. "Book your HydraFacial".'),
|
|
14445
14445
|
text: str("One sentence."),
|
|
14446
|
+
button_label: str('Default "Book Now".'),
|
|
14446
14447
|
button_url: str("Default: the booking link.")
|
|
14447
14448
|
},
|
|
14448
14449
|
required: ["title"],
|
|
@@ -14453,7 +14454,7 @@ var SECTIONS2 = [
|
|
|
14453
14454
|
buttons(
|
|
14454
14455
|
[
|
|
14455
14456
|
button(
|
|
14456
|
-
"Book Now",
|
|
14457
|
+
S(s.button_label) || "Book Now",
|
|
14457
14458
|
S(s.button_url) || kit.bookUrl || "/contact-us/",
|
|
14458
14459
|
"w-full [&_a]:w-full [&_a]:bg-white [&_a]:text-ink hover:[&_a]:bg-panel"
|
|
14459
14460
|
)
|
|
@@ -15001,7 +15002,7 @@ async function menuSet(site, ops, id) {
|
|
|
15001
15002
|
const blocks = parse(nav.content.raw);
|
|
15002
15003
|
const before = toItems(blocks);
|
|
15003
15004
|
const after = applyMenuOps(before, ops);
|
|
15004
|
-
const rest = blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
|
|
15005
|
+
const rest = ops.items ? [] : blocks.filter((b) => b.name && !["core/navigation-link", "core/navigation-submenu"].includes(b.name));
|
|
15005
15006
|
const content = [fromItems(after), serialize(rest)].filter(Boolean).join("\n");
|
|
15006
15007
|
await call(site, {
|
|
15007
15008
|
method: "POST",
|
|
@@ -15495,7 +15496,11 @@ function composePage(sections, kit, aside = {}) {
|
|
|
15495
15496
|
const asideParts = [
|
|
15496
15497
|
kit.synced?.share ? group([syncedRef(kit.synced.share)], { cls: "px-2" }) : "",
|
|
15497
15498
|
serialize([
|
|
15498
|
-
buildSection(
|
|
15499
|
+
buildSection(
|
|
15500
|
+
"cta-card",
|
|
15501
|
+
{ title: aside.ctaTitle || "Book your visit", text: aside.ctaText ?? "", button_label: aside.ctaButton },
|
|
15502
|
+
kit
|
|
15503
|
+
)
|
|
15499
15504
|
])
|
|
15500
15505
|
];
|
|
15501
15506
|
const body = band(
|
|
@@ -16059,6 +16064,27 @@ async function changePage(ctx, a, fn) {
|
|
|
16059
16064
|
undo: "wp_page_undo goes back to the version before this change."
|
|
16060
16065
|
};
|
|
16061
16066
|
}
|
|
16067
|
+
var findAllBlocks = (list3) => list3.flatMap((b) => [b, ...findAllBlocks(b.inner)]);
|
|
16068
|
+
async function setHome(site, id, postsPage) {
|
|
16069
|
+
const before = await call(site, {
|
|
16070
|
+
path: "/wp/v2/settings",
|
|
16071
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" }
|
|
16072
|
+
});
|
|
16073
|
+
const body = { show_on_front: "page", page_on_front: id };
|
|
16074
|
+
if (postsPage !== void 0) body.page_for_posts = postsPage;
|
|
16075
|
+
if (postsPage === id) throw new Error("The homepage and the posts page can't be the same page.");
|
|
16076
|
+
const after = await call(site, {
|
|
16077
|
+
method: "POST",
|
|
16078
|
+
path: "/wp/v2/settings",
|
|
16079
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" },
|
|
16080
|
+
body
|
|
16081
|
+
});
|
|
16082
|
+
return {
|
|
16083
|
+
before,
|
|
16084
|
+
after,
|
|
16085
|
+
undo: before.show_on_front === "page" && before.page_on_front ? `wp_site_home_set page ${before.page_on_front}` : "wp_site_home_set latest_posts true"
|
|
16086
|
+
};
|
|
16087
|
+
}
|
|
16062
16088
|
function position(a) {
|
|
16063
16089
|
return {
|
|
16064
16090
|
after: a.after || void 0,
|
|
@@ -16282,7 +16308,11 @@ var pageTools = [
|
|
|
16282
16308
|
description: str2(""),
|
|
16283
16309
|
keywords: str2("")
|
|
16284
16310
|
}),
|
|
16285
|
-
cta: obj("Sidebar booking card: title, text
|
|
16311
|
+
cta: obj("Sidebar booking card: title, text, button label (default Book Now).", {
|
|
16312
|
+
title: str2(""),
|
|
16313
|
+
text: str2(""),
|
|
16314
|
+
button: str2("")
|
|
16315
|
+
}),
|
|
16286
16316
|
related: bool(
|
|
16287
16317
|
"Add a related-services band from sibling pages (default true when the page has a reading column)."
|
|
16288
16318
|
),
|
|
@@ -16327,7 +16357,8 @@ var pageTools = [
|
|
|
16327
16357
|
const cta = a.cta ?? {};
|
|
16328
16358
|
const blocks = composePage(sections, kit, {
|
|
16329
16359
|
ctaTitle: cta.title || `Book ${String(a.title)}`,
|
|
16330
|
-
ctaText: cta.text
|
|
16360
|
+
ctaText: cta.text,
|
|
16361
|
+
ctaButton: cta.button
|
|
16331
16362
|
});
|
|
16332
16363
|
const outline = brief2(blocks, kit.syncedTitles);
|
|
16333
16364
|
if (a.preview) return { preview: true, title: a.title, sections: outline };
|
|
@@ -16426,6 +16457,103 @@ var pageTools = [
|
|
|
16426
16457
|
};
|
|
16427
16458
|
}
|
|
16428
16459
|
}),
|
|
16460
|
+
tool({
|
|
16461
|
+
name: "wp_page_publish",
|
|
16462
|
+
description: "Publish a page (a draft or private page) so visitors and search engines see it. Checks it first: one H1, an SEO title and description (warns when missing). homepage: true also makes it the site's homepage. Afterwards the live page is opened to confirm it works. To take it down again, wp_page_update status draft.",
|
|
16463
|
+
parameters: {
|
|
16464
|
+
type: "object",
|
|
16465
|
+
properties: {
|
|
16466
|
+
page: PAGE,
|
|
16467
|
+
homepage: bool("Also make it the site's homepage."),
|
|
16468
|
+
expect_modified: EXPECT
|
|
16469
|
+
},
|
|
16470
|
+
required: ["page"]
|
|
16471
|
+
},
|
|
16472
|
+
group: "site",
|
|
16473
|
+
need: WRITE,
|
|
16474
|
+
risk: "W3",
|
|
16475
|
+
gates: [...GATES],
|
|
16476
|
+
confirm: (a, lang) => say(
|
|
16477
|
+
lang,
|
|
16478
|
+
`Publish page ${q2(a.page, "")} on your website${a.homepage ? " and make it the homepage" : ""}.`,
|
|
16479
|
+
`\u767C\u4F48\u7DB2\u7AD9\u4E0A\u7684\u9801\u9762 ${q2(a.page, "")}${a.homepage ? "\uFF0C\u4E26\u8A2D\u70BA\u9996\u9801" : ""}\u3002`
|
|
16480
|
+
),
|
|
16481
|
+
run: async (ctx, a) => {
|
|
16482
|
+
const site = siteOf(ctx);
|
|
16483
|
+
const p = await findPage(site, a.page, true);
|
|
16484
|
+
checkFresh(p, a.expect_modified);
|
|
16485
|
+
const m = p.meta ?? {};
|
|
16486
|
+
const blocks = parse(p.content?.raw ?? "");
|
|
16487
|
+
const h1 = findAllBlocks(blocks).filter((b) => b.name === "core/heading" && Number(b.attrs.level) === 1).length;
|
|
16488
|
+
const warnings = [
|
|
16489
|
+
h1 === 1 ? "" : h1 === 0 ? "The page has no H1 in its content (a template may add one)." : `The page has ${h1} H1 headings.`,
|
|
16490
|
+
m.maggie_seo_title ? "" : "No SEO title: the page title is used.",
|
|
16491
|
+
m.maggie_seo_description ? "" : "No meta description: set one with wp_seo_set."
|
|
16492
|
+
].filter(Boolean);
|
|
16493
|
+
const wasStatus = p.status;
|
|
16494
|
+
const saved = p.status === "publish" ? p : await savePage(site, p.id, null, { status: "publish" });
|
|
16495
|
+
let home = null;
|
|
16496
|
+
if (a.homepage) home = await setHome(site, p.id);
|
|
16497
|
+
const live = await check(site, saved.link, "publish");
|
|
16498
|
+
return {
|
|
16499
|
+
published: true,
|
|
16500
|
+
id: p.id,
|
|
16501
|
+
path: path(saved.link),
|
|
16502
|
+
before: wasStatus,
|
|
16503
|
+
homepage: home,
|
|
16504
|
+
check: live,
|
|
16505
|
+
warnings
|
|
16506
|
+
};
|
|
16507
|
+
}
|
|
16508
|
+
}),
|
|
16509
|
+
tool({
|
|
16510
|
+
name: "wp_site_home_set",
|
|
16511
|
+
description: "Choose the site's homepage (a published page) and, optionally, the page that lists the blog posts. Returns the previous setting so it can be put back. latest_posts: true shows the newest posts as the homepage instead.",
|
|
16512
|
+
parameters: {
|
|
16513
|
+
type: "object",
|
|
16514
|
+
properties: {
|
|
16515
|
+
page: str2("The page to show as the homepage (path, slug or id)."),
|
|
16516
|
+
posts_page: str2('The page that lists the blog posts (optional; "" to clear).'),
|
|
16517
|
+
latest_posts: bool("Show the newest posts as the homepage instead of a page.")
|
|
16518
|
+
}
|
|
16519
|
+
},
|
|
16520
|
+
group: "site",
|
|
16521
|
+
need: WRITE,
|
|
16522
|
+
risk: "W3",
|
|
16523
|
+
gates: [...GATES],
|
|
16524
|
+
confirm: (a, lang) => a.latest_posts ? say(lang, "Show your newest posts as your website's homepage.", "\u628A\u7DB2\u7AD9\u9996\u9801\u6539\u70BA\u986F\u793A\u6700\u65B0\u6587\u7AE0\u3002") : say(
|
|
16525
|
+
lang,
|
|
16526
|
+
`Make ${q2(a.page, "that page")} your website's homepage.`,
|
|
16527
|
+
`\u628A ${q2(a.page, "\u8A72\u9801\u9762")} \u8A2D\u70BA\u7DB2\u7AD9\u9996\u9801\u3002`
|
|
16528
|
+
),
|
|
16529
|
+
run: async (ctx, a) => {
|
|
16530
|
+
const site = siteOf(ctx);
|
|
16531
|
+
if (a.latest_posts) {
|
|
16532
|
+
const before = await call(site, {
|
|
16533
|
+
path: "/wp/v2/settings",
|
|
16534
|
+
query: { _fields: "show_on_front,page_on_front,page_for_posts" }
|
|
16535
|
+
});
|
|
16536
|
+
await call(site, {
|
|
16537
|
+
method: "POST",
|
|
16538
|
+
path: "/wp/v2/settings",
|
|
16539
|
+
query: { _fields: "show_on_front" },
|
|
16540
|
+
body: { show_on_front: "posts" }
|
|
16541
|
+
});
|
|
16542
|
+
return { before, after: { show_on_front: "posts" } };
|
|
16543
|
+
}
|
|
16544
|
+
if (!a.page) throw new Error("Which page should be the homepage?");
|
|
16545
|
+
const p = await findPage(site, a.page);
|
|
16546
|
+
if (p.status !== "publish")
|
|
16547
|
+
throw new Error(
|
|
16548
|
+
`${path(p.link)} isn't published yet: publish it first (wp_page_publish homepage: true does both).`
|
|
16549
|
+
);
|
|
16550
|
+
let postsPage;
|
|
16551
|
+
if (typeof a.posts_page === "string")
|
|
16552
|
+
postsPage = a.posts_page === "" ? 0 : (await findPage(site, a.posts_page)).id;
|
|
16553
|
+
const r = await setHome(site, p.id, postsPage);
|
|
16554
|
+
return { ...r, check: await check(site, `${site.url ?? ""}/`, "publish") };
|
|
16555
|
+
}
|
|
16556
|
+
}),
|
|
16429
16557
|
tool({
|
|
16430
16558
|
name: "wp_page_delete",
|
|
16431
16559
|
description: "Move a page to the WordPress trash (restorable for 30 days) and redirect its address (301) to another page, by default its parent or the homepage.",
|
|
@@ -16991,6 +17119,17 @@ var childTools = [
|
|
|
16991
17119
|
slug: str2("The child theme (default the active one)."),
|
|
16992
17120
|
colors: obj('e.g. { "olive": "#4F5B48" }.'),
|
|
16993
17121
|
fonts: obj('Font families by slug: { "body": "Jost, sans-serif" } (files must already be in the child).'),
|
|
17122
|
+
font_files: arr(
|
|
17123
|
+
"Install fonts in the child from https links to font files (woff2/woff/ttf), e.g. Google Fonts' fonts.gstatic.com files: each becomes a fontFace of its family (slug display / body / any).",
|
|
17124
|
+
obj("", {
|
|
17125
|
+
slug: str2("Family slug in theme.json: display, body, \u2026"),
|
|
17126
|
+
family: str2('Family name, e.g. "Roboto Slab".'),
|
|
17127
|
+
fallback: str2('Fallback stack, e.g. "Georgia, serif".'),
|
|
17128
|
+
url: str2("https link to the font file."),
|
|
17129
|
+
weight: str2('e.g. "400", "800" or "100 900".'),
|
|
17130
|
+
style: str2('"normal" or "italic".')
|
|
17131
|
+
})
|
|
17132
|
+
),
|
|
16994
17133
|
styles: obj('theme.json styles to merge in (e.g. { "elements": { "button": { \u2026 } } }).'),
|
|
16995
17134
|
css: str2("Replace the child's CSS (plain CSS)."),
|
|
16996
17135
|
css_append: str2("Add CSS at the end."),
|
|
@@ -17017,9 +17156,54 @@ var childTools = [
|
|
|
17017
17156
|
const cur = info.files ?? {};
|
|
17018
17157
|
const files = {};
|
|
17019
17158
|
const notes = [];
|
|
17159
|
+
if (Array.isArray(a.font_files) && a.font_files.length) {
|
|
17160
|
+
const tj0 = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17161
|
+
if (!tj0.settings || Array.isArray(tj0.settings)) tj0.settings = {};
|
|
17162
|
+
const typo = tj0.settings.typography ?? {};
|
|
17163
|
+
const fams = typo.fontFamilies ?? [];
|
|
17164
|
+
for (const f of a.font_files) {
|
|
17165
|
+
if (!/^[a-z0-9-]{2,30}$/.test(f.slug ?? ""))
|
|
17166
|
+
throw new Error(`Font slug "${f.slug}": lowercase letters, digits and dashes.`);
|
|
17167
|
+
if (!/^https:\/\//.test(f.url ?? "")) throw new Error(`${f.family}: the font's link must be https.`);
|
|
17168
|
+
const r = await fetch(f.url);
|
|
17169
|
+
if (!r.ok) throw new Error(`${f.family}: the font's link answered HTTP ${r.status}.`);
|
|
17170
|
+
const bytes = new Uint8Array(await r.arrayBuffer());
|
|
17171
|
+
const head = String.fromCharCode(...bytes.subarray(0, 4));
|
|
17172
|
+
const ext = head === "wOF2" ? "woff2" : head === "wOFF" ? "woff" : head === "OTTO" ? "otf" : head === "\0\0\0" || head === "true" ? "ttf" : "";
|
|
17173
|
+
if (!ext) throw new Error(`${f.family}: that link isn't a font file.`);
|
|
17174
|
+
if (bytes.length > 4 * 1024 * 1024) throw new Error(`${f.family}: the font file is larger than 4 MB.`);
|
|
17175
|
+
const weight = f.weight || "400";
|
|
17176
|
+
const style = f.style || "normal";
|
|
17177
|
+
const name = `${f.family.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${weight.replace(/\s+/g, "-")}${style === "italic" ? "-italic" : ""}.${ext}`;
|
|
17178
|
+
let bin = "";
|
|
17179
|
+
for (let i = 0; i < bytes.length; i += 32768) bin += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
17180
|
+
files[`assets/fonts/${name}`] = btoa(bin);
|
|
17181
|
+
let fam = fams.find((x) => x.slug === f.slug);
|
|
17182
|
+
const stack = `"${f.family}", ${f.fallback || "sans-serif"}`;
|
|
17183
|
+
if (!fam || !fam.fontFamily.includes(`"${f.family}"`)) {
|
|
17184
|
+
const keep = fam ? fams.indexOf(fam) : -1;
|
|
17185
|
+
fam = { slug: f.slug, name: f.family, fontFamily: stack, fontFace: [] };
|
|
17186
|
+
if (keep >= 0) fams[keep] = fam;
|
|
17187
|
+
else fams.push(fam);
|
|
17188
|
+
}
|
|
17189
|
+
fam.fontFace = [
|
|
17190
|
+
...(fam.fontFace ?? []).filter((x) => !(x.fontWeight === weight && x.fontStyle === style)),
|
|
17191
|
+
{
|
|
17192
|
+
fontFamily: f.family,
|
|
17193
|
+
fontWeight: weight,
|
|
17194
|
+
fontStyle: style,
|
|
17195
|
+
fontDisplay: "swap",
|
|
17196
|
+
src: [`file:./assets/fonts/${name}`]
|
|
17197
|
+
}
|
|
17198
|
+
];
|
|
17199
|
+
}
|
|
17200
|
+
tj0.settings.typography = { ...typo, fontFamilies: fams };
|
|
17201
|
+
files["theme.json"] = JSON.stringify(tj0, null, " ");
|
|
17202
|
+
}
|
|
17020
17203
|
if (a.colors || a.fonts || a.styles) {
|
|
17021
|
-
const tj = JSON.parse(cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17022
|
-
tj.settings
|
|
17204
|
+
const tj = JSON.parse(files["theme.json"] ?? cur["theme.json"] ?? '{"version":3,"settings":{}}');
|
|
17205
|
+
if (!tj.settings || Array.isArray(tj.settings)) tj.settings = {};
|
|
17206
|
+
if (Array.isArray(tj.styles)) tj.styles = {};
|
|
17023
17207
|
if (a.colors) {
|
|
17024
17208
|
let palette = tj.settings.color?.palette ?? [];
|
|
17025
17209
|
if (!palette.length) palette = ((await styleGet(site))._palette ?? []).map((c) => ({ ...c }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
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",
|