@cmo-so/mcp 0.12.16 → 0.12.18

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 +115 -30
  2. package/package.json +1 -1
package/dist/cmo-mcp.mjs CHANGED
@@ -24489,7 +24489,7 @@ var require_dist3 = __commonJS({
24489
24489
  // package.json
24490
24490
  var package_default = {
24491
24491
  name: "@cmo-so/cli",
24492
- version: "0.12.16",
24492
+ version: "0.12.18",
24493
24493
  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.",
24494
24494
  type: "module",
24495
24495
  license: "MIT",
@@ -33531,10 +33531,10 @@ function domainApi(api3, domain) {
33531
33531
  body: JSON.stringify({ social_copy: socialCopy })
33532
33532
  }),
33533
33533
  /** The post's link: it replaces the old one in the text, and the preview card shows the new page. */
33534
- setSocialLink: (id, link) => api3.request(`${base}/social/${id}`, {
33534
+ setSocialLink: (id, link2) => api3.request(`${base}/social/${id}`, {
33535
33535
  method: "PATCH",
33536
33536
  headers: { "Content-Type": "application/json" },
33537
- body: JSON.stringify({ link })
33537
+ body: JSON.stringify({ link: link2 })
33538
33538
  }),
33539
33539
  /** Go out as the post's image, or as a link preview. */
33540
33540
  setSocialPublishMode: (id, mode) => api3.request(`${base}/social/${id}`, {
@@ -34794,13 +34794,13 @@ var accountTools = [
34794
34794
  run: async (ctx, { platform }) => {
34795
34795
  const p2 = platform && CONNECT_FORMS.includes(platform) ? platform : "";
34796
34796
  const label2 = p2 ? `Connect ${CONNECT_NAMES[p2]}` : "Connect your website";
34797
- const link = `https://app.cmo.so/posts?connect=${p2 || "wordpress"}${ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : ""}`;
34797
+ const link2 = `https://app.cmo.so/posts?connect=${p2 || "wordpress"}${ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : ""}`;
34798
34798
  if (ctx.client === "chat")
34799
34799
  return {
34800
34800
  card: { type: "connect", platform: p2 || null, label: label2 },
34801
34801
  shown: `A "${label2}" button now shows under your message. The user fills in the form there; say so in one line.`
34802
34802
  };
34803
- return { link, shown: "Give the user this link: it opens the form in the cmo.so app (web or phone)." };
34803
+ return { link: link2, shown: "Give the user this link: it opens the form in the cmo.so app (web or phone)." };
34804
34804
  }
34805
34805
  }),
34806
34806
  tool({
@@ -37177,9 +37177,9 @@ var FILE_SCHEMA = {
37177
37177
  };
37178
37178
  var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f2) => String(f2?.download_url ?? "").trim()).filter(Boolean);
37179
37179
  var videoLink = (a3) => {
37180
- const link = fileLinks(a3.file)[0] ?? String(a3.url ?? "").trim();
37181
- if (!link.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
37182
- return link;
37180
+ const link2 = fileLinks(a3.file)[0] ?? String(a3.url ?? "").trim();
37181
+ if (!link2.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
37182
+ return link2;
37183
37183
  };
37184
37184
  var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
37185
37185
  var channelsOf = (value) => {
@@ -37667,10 +37667,10 @@ var socialTools = [
37667
37667
  risk: "W1",
37668
37668
  gates: ["VER"],
37669
37669
  run: async (ctx, { id, url, file, title }) => {
37670
- const link = fileLinks(file)[0] ?? String(url ?? "").trim();
37671
- if (!link.startsWith("https://"))
37670
+ const link2 = fileLinks(file)[0] ?? String(url ?? "").trim();
37671
+ if (!link2.startsWith("https://"))
37672
37672
  throw new Error("Give the PDF (a file from this chat, or a public https link).");
37673
- const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
37673
+ const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link2, ...title ? { title } : {} });
37674
37674
  return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
37675
37675
  }
37676
37676
  }),
@@ -39209,6 +39209,55 @@ function touchPath(list4, target) {
39209
39209
  return false;
39210
39210
  }
39211
39211
 
39212
+ // ../../packages/tools/src/wp/credit.ts
39213
+ var CREDIT_NAME = "AI Marketing OS Ltd";
39214
+ var CREDIT_URL = "https://cmo.so";
39215
+ var link = () => `<a href="${CREDIT_URL}">${CREDIT_NAME}</a>`;
39216
+ var creditInline = (year = (/* @__PURE__ */ new Date()).getUTCFullYear()) => `<span class="maggie-credit">\xA9 ${year} \xB7 Powered by ${link()}</span>`;
39217
+ var creditBlock = () => `<!-- wp:paragraph {"fontSize":"small"} -->
39218
+ <p class="has-small-font-size">${creditInline()}</p>
39219
+ <!-- /wp:paragraph -->`;
39220
+ var VERB = "(?:proudly\\s+)?(?:designed|developed|powered|built|crafted|created|made|hosted|maintained|managed|website|web\\s+design|web\\s+development|site|design|development|theme|seo)";
39221
+ var CREDIT_RX = new RegExp(
39222
+ `(?<prefix>(?:\xA9|&copy;|&#169;|copyright)\\s*(?:\\d{4}(?:\\s*[-\u2013]\\s*\\d{4})?)?\\s*[,\xB7|\u2022\\-\u2013]?\\s*)?(?<verb>${VERB}(?:\\s*(?:&amp;|&|and|\\+|/)\\s*${VERB})?)\\s+by\\s*(?<name><a\\b[^>]*>[\\s\\S]*?</a>|[^<|\xB7\u2022]{1,60}?)(?=\\s*(?:<|\\||\xB7|\u2022|$|\\.\\s|\\.$))`,
39223
+ "giu"
39224
+ );
39225
+ var OWN_RX = /<span class="maggie-credit">[\s\S]*?<\/span>/giu;
39226
+ var isOurs = (name) => name.replace(/<[^>]+>/g, "").toLowerCase().includes(CREDIT_NAME.toLowerCase()) || name.includes("cmo.so");
39227
+ function applyCredit(markup) {
39228
+ if (!markup?.trim()) return markup;
39229
+ let credited = false;
39230
+ let html2 = markup.replace(OWN_RX, () => {
39231
+ credited = true;
39232
+ return creditInline();
39233
+ });
39234
+ const source = html2;
39235
+ html2 = source.replace(CREDIT_RX, (...args) => {
39236
+ const all2 = args[0];
39237
+ const at2 = args[args.length - 3];
39238
+ const g3 = args[args.length - 1];
39239
+ const before = source.slice(0, at2);
39240
+ if (before.lastIndexOf("<") > before.lastIndexOf(">")) return all2;
39241
+ if (!credited) {
39242
+ credited = true;
39243
+ return creditInline();
39244
+ }
39245
+ return isOurs(g3.name) ? all2 : `${g3.prefix ?? ""}${g3.verb} by ${link()}`;
39246
+ });
39247
+ if (credited) return html2;
39248
+ const end = html2.lastIndexOf("</footer>");
39249
+ return end < 0 ? `${html2.trimEnd()}
39250
+
39251
+ ${creditBlock()}
39252
+ ` : `${html2.slice(0, end)}${creditBlock()}${html2.slice(end)}`;
39253
+ }
39254
+ function refuseCreditOverride(what, text3) {
39255
+ if (text3 && /maggie-credit/i.test(text3))
39256
+ throw new Error(
39257
+ `${what}: the footer credit (maggie-credit) is part of every Maggie site and can't be styled away or changed.`
39258
+ );
39259
+ }
39260
+
39212
39261
  // ../../packages/tools/src/wp/css.ts
39213
39262
  var engine = null;
39214
39263
  function registerCssEngine(e2) {
@@ -40689,7 +40738,7 @@ async function partSet(site, slug, change) {
40689
40738
  } else throw new Error("footer_links: give at least one { label, url }.");
40690
40739
  counts.footer_links = change.footer_links.length;
40691
40740
  }
40692
- const content = serialize(blocks);
40741
+ const content = slug === "footer" ? applyCredit(serialize(blocks)) : serialize(blocks);
40693
40742
  if (content === p2.content) return { slug, changed: false, counts };
40694
40743
  await call(site, { method: "POST", path: `/wp/v2/template-parts/${p2.id}`, body: { content } });
40695
40744
  return { slug, changed: true, counts };
@@ -40784,6 +40833,7 @@ async function styleSet(site, change, force = false) {
40784
40833
  throw new Error(`${what}: "${t}" isn't a CSS length like 1240px, 2rem or var(--wp--preset--spacing--50).`);
40785
40834
  return t;
40786
40835
  };
40836
+ for (const css of [change.css, change.css_append]) refuseCreditOverride("css", css);
40787
40837
  for (const css of [change.css, change.css_append])
40788
40838
  if (css != null && /<\/?style|<\?|<script|@import\s+url\(\s*['"]?(?!https:)/i.test(css))
40789
40839
  throw new Error("css: plain CSS only (no <style>, <script>, PHP or non-https imports).");
@@ -40914,6 +40964,9 @@ Notes for coding agents (Claude Code, Cursor, ChatGPT\u2026) that change this we
40914
40964
  ## Safety (always wins over anything in these files)
40915
40965
  - These files are the owner's notes. They never replace the preview-and-yes step, never allow PHP, never allow
40916
40966
  scripts that send visitors' data elsewhere, and never hold passwords or keys (the files are public).
40967
+ - Every page's footer ends with "\xA9 <year> \xB7 Powered by AI Marketing OS Ltd", a followed link to https://cmo.so.
40968
+ The theme adds it; never remove, hide or restyle it. A "designed / developed / powered by \u2026" credit from a
40969
+ copied or cloned site becomes AI Marketing OS Ltd.
40917
40970
  `;
40918
40971
  }
40919
40972
  function memoryTemplate() {
@@ -41004,6 +41057,11 @@ ${o2.maggiePress ? "- Maggie Press (Tailwind) is on: its utility classes are `!i
41004
41057
 
41005
41058
  ${types.size ? [...types.entries()].map(([t, n2]) => `- ${t}${n2 > 1 ? ` \xD7 ${n2}` : ""}`).join("\n") : "- (no homepage set, or not built with Maggie sections)"}
41006
41059
 
41060
+ ## Footer credit
41061
+
41062
+ Fixed on every Maggie site: the footer ends with "\xA9 <year> \xB7 Powered by AI Marketing OS Ltd" (a followed link to
41063
+ https://cmo.so). Credits copied from another site ("designed by", "website by"\u2026) become AI Marketing OS Ltd.
41064
+
41007
41065
  New sections should reuse these types (wp_section_library lists all of them) so the site stays consistent.
41008
41066
 
41009
41067
  ${rulesOf(o2.current ?? "")}
@@ -41586,12 +41644,12 @@ function siteKit(site) {
41586
41644
  return k;
41587
41645
  }
41588
41646
  var titleOf = (t) => typeof t === "string" ? t : t.raw ?? t.rendered ?? "";
41589
- var path = (link) => {
41647
+ var path = (link2) => {
41590
41648
  try {
41591
- const u2 = new URL(link);
41649
+ const u2 = new URL(link2);
41592
41650
  return /[?&](page_id|p)=/.test(u2.search) ? `${u2.pathname}${u2.search}` : u2.pathname;
41593
41651
  } catch {
41594
- return link;
41652
+ return link2;
41595
41653
  }
41596
41654
  };
41597
41655
  async function listPages(site, o2 = {}) {
@@ -42723,30 +42781,54 @@ async function techAudit(site, o2 = {}) {
42723
42781
  ...urls.length > crawl.length ? { note: `Crawled the first ${crawl.length} of ${urls.length} sitemap addresses (limit up to 200).` } : {}
42724
42782
  };
42725
42783
  }
42726
- async function pageSpeed(url) {
42727
- const key = globalThis.process?.env?.PAGESPEED_API_KEY;
42784
+ async function pageSpeedRun(url, key) {
42728
42785
  const q4 = new URLSearchParams({ url, strategy: "mobile", category: "performance" });
42729
42786
  if (key) q4.set("key", key);
42730
42787
  const r = await fetch(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${q4}`, {
42731
42788
  signal: AbortSignal.timeout(9e4)
42732
42789
  }).catch(() => null);
42733
- if (!r || !r.ok)
42734
- return {
42735
- error: `PageSpeed Insights didn't answer (${r?.status ?? "timeout"})${r?.status === 429 && !key ? ": the server needs a PAGESPEED_API_KEY (Google Cloud, free)" : ""}.`
42736
- };
42790
+ if (!r || !r.ok) return { status: r?.status ?? 0 };
42737
42791
  const j2 = await r.json();
42738
42792
  const f2 = j2.loadingExperience?.metrics ?? {};
42739
42793
  const a3 = j2.lighthouseResult?.audits ?? {};
42740
42794
  const field = !!f2.LARGEST_CONTENTFUL_PAINT_MS;
42741
42795
  return {
42742
- source: field ? "real visitors (Chrome UX Report, 28 days)" : "lab test (no real-visitor data yet)",
42796
+ status: 200,
42797
+ field,
42743
42798
  score: Math.round((j2.lighthouseResult?.categories?.performance?.score ?? 0) * 100),
42744
- lcp_ms: field ? f2.LARGEST_CONTENTFUL_PAINT_MS?.percentile : Math.round(a3["largest-contentful-paint"]?.numericValue ?? 0),
42799
+ lcp_ms: field ? f2.LARGEST_CONTENTFUL_PAINT_MS?.percentile ?? 0 : Math.round(a3["largest-contentful-paint"]?.numericValue ?? 0),
42745
42800
  cls: field ? (f2.CUMULATIVE_LAYOUT_SHIFT_SCORE?.percentile ?? 0) / 100 : Number((a3["cumulative-layout-shift"]?.numericValue ?? 0).toFixed(3)),
42746
42801
  inp_ms: f2.INTERACTION_TO_NEXT_PAINT?.percentile ?? null,
42747
42802
  tbt_ms: Math.round(a3["total-blocking-time"]?.numericValue ?? 0)
42748
42803
  };
42749
42804
  }
42805
+ async function pageSpeed(url) {
42806
+ const key = globalThis.process?.env?.PAGESPEED_API_KEY;
42807
+ const runs = await Promise.all(
42808
+ [1, 2, 3].map((n2) => pageSpeedRun(`${url}${url.includes("?") ? "&" : "?"}cmo-speed=${Date.now()}-${n2}`, key))
42809
+ );
42810
+ const ok2 = runs.filter((r) => r.status === 200 && "lcp_ms" in r);
42811
+ if (!ok2.length) {
42812
+ const status = runs[0]?.status ?? 0;
42813
+ return {
42814
+ error: `PageSpeed Insights didn't answer (${status || "timeout"})${status === 429 && !key ? ": the server needs a PAGESPEED_API_KEY (Google Cloud, free)" : ""}.`
42815
+ };
42816
+ }
42817
+ const median = (xs) => {
42818
+ const s = [...xs].sort((a3, b2) => a3 - b2);
42819
+ return s[Math.floor((s.length - 1) / 2)];
42820
+ };
42821
+ const field = ok2.some((r) => r.field);
42822
+ return {
42823
+ source: field ? "real visitors (Chrome UX Report, 28 days)" : `lab test, median of ${ok2.length} runs (no real-visitor data yet)`,
42824
+ score: median(ok2.map((r) => r.score)),
42825
+ lcp_ms: median(ok2.map((r) => r.lcp_ms)),
42826
+ cls: median(ok2.map((r) => r.cls)),
42827
+ inp_ms: ok2.find((r) => r.inp_ms != null)?.inp_ms ?? null,
42828
+ tbt_ms: median(ok2.map((r) => r.tbt_ms)),
42829
+ runs: ok2.map((r) => ({ score: r.score, lcp_ms: r.lcp_ms }))
42830
+ };
42831
+ }
42750
42832
  var STOP = new Set(
42751
42833
  "the a an and or of to in for on with by at from is are be your our you we it this that how what why best guide near vs and/or my me us".split(
42752
42834
  " "
@@ -43093,7 +43175,7 @@ async function llmsTxt(site, o2 = {}) {
43093
43175
  return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
43094
43176
  };
43095
43177
  const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
43096
- const isSample = (link, title) => /\/(sample-page|hello-world)\/?$/.test(link) || title === "Sample Page" || title === "Hello world!";
43178
+ const isSample = (link2, title) => /\/(sample-page|hello-world)\/?$/.test(link2) || title === "Sample Page" || title === "Hello world!";
43097
43179
  const home = pages.find((p2) => p2.id === s.page_on_front);
43098
43180
  const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
43099
43181
  const visible = pages.filter(
@@ -43193,9 +43275,9 @@ var brief2 = (blocks, synced) => locate(blocks).map((l2) => {
43193
43275
  const d2 = describe2(l2, synced);
43194
43276
  return `${d2.id} ${d2.type}${d2.heading ? `: ${d2.heading.slice(0, 60)}` : ""}`;
43195
43277
  });
43196
- async function check(site, link, status) {
43278
+ async function check(site, link2, status) {
43197
43279
  if (status !== "publish") return { note: "Saved as a draft: not public yet." };
43198
- const h3 = await renderedHead(site, link).catch((e2) => ({ error: e2.message }));
43280
+ const h3 = await renderedHead(site, link2).catch((e2) => ({ error: e2.message }));
43199
43281
  if ("error" in h3) return { ok: false, error: h3.error };
43200
43282
  const problems = [
43201
43283
  h3.status >= 400 ? `HTTP ${h3.status}` : "",
@@ -43268,9 +43350,9 @@ var siteTools = [
43268
43350
  risk: "R0",
43269
43351
  run: async (ctx) => {
43270
43352
  if (!ctx.domain) throw new Error("No website is selected yet. Add one first.");
43271
- const link = `https://app.cmo.so/wp-builder?site=${encodeURIComponent(ctx.domain)}`;
43353
+ const link2 = `https://app.cmo.so/wp-builder?site=${encodeURIComponent(ctx.domain)}`;
43272
43354
  return {
43273
- link,
43355
+ link: link2,
43274
43356
  cli: "cmo wp connect <site address> -u <administrator>",
43275
43357
  shown: "Give the user this link: it opens the form in the cmo.so app (web or phone). They create the application password in WordPress (Users \u2192 Profile \u2192 Application Passwords) and paste it there."
43276
43358
  };
@@ -44510,6 +44592,8 @@ var childTools = [
44510
44592
  tj.styles = deepMerge(tj.styles ?? {}, a3.styles);
44511
44593
  files["theme.json"] = JSON.stringify(tj, null, " ");
44512
44594
  }
44595
+ refuseCreditOverride("css", typeof a3.css === "string" ? a3.css : null);
44596
+ refuseCreditOverride("css_append", typeof a3.css_append === "string" ? a3.css_append : null);
44513
44597
  if (typeof a3.css === "string") files["style.css"] = a3.css;
44514
44598
  if (typeof a3.css_append === "string")
44515
44599
  files["style.css"] = `${(files["style.css"] ?? cur["style.css"] ?? "").replace(/\n*$/, "\n")}${a3.css_append}
@@ -44524,6 +44608,7 @@ var childTools = [
44524
44608
  const js = typeof a3.js === "string" ? a3.js : `${(cur["assets/js/site.js"] ?? "").replace(/\n*$/, "\n")}${String(a3.js_append)}
44525
44609
  `.trimStart();
44526
44610
  if (/<\/?script\b/i.test(js)) throw new Error("js: plain JavaScript only, no <script> tags.");
44611
+ refuseCreditOverride("js", js);
44527
44612
  if (/<\?(php|=)?/i.test(js)) throw new Error("js: PHP isn't allowed.");
44528
44613
  files["assets/js/site.js"] = js;
44529
44614
  notes.push(
@@ -44533,7 +44618,7 @@ var childTools = [
44533
44618
  const markup = (path2, m) => {
44534
44619
  if (typeof m !== "string" || !m.trim()) return;
44535
44620
  if (!parse3(m).some((b2) => b2.name)) throw new Error(`${path2}: give block markup (<!-- wp:\u2026 -->).`);
44536
- files[path2] = m;
44621
+ files[path2] = path2 === "parts/footer.html" ? applyCredit(m) : m;
44537
44622
  };
44538
44623
  markup("parts/header.html", a3.header);
44539
44624
  markup("parts/footer.html", a3.footer);
@@ -45729,7 +45814,7 @@ function withRelated(blocks, heading2, links2) {
45729
45814
  var seoTechTools = [
45730
45815
  tool({
45731
45816
  name: "wp_seo_tech_audit",
45732
- description: "Technical SEO check of the connected site, like a crawler: robots.txt (sitemap line, Google or AI crawlers blocked), the sitemap and every page it lists (status, redirects, noindex in the sitemap, canonicals pointing elsewhere, H1 count, missing or duplicate titles and descriptions, long titles, pictures without alt text, broken structured data, thin pages), broken and redirected internal links, generic link words, llms.txt; speed: true adds Core Web Vitals for the homepage (PageSpeed Insights). Findings ranked p0\u2026p4 with the fix and a tool when one can do it. Read-only. Internal-link layout: wp_internal_links_plan.",
45817
+ description: "Technical SEO check of the connected site, like a crawler: robots.txt (sitemap line, Google or AI crawlers blocked), the sitemap and every page it lists (status, redirects, noindex in the sitemap, canonicals pointing elsewhere, H1 count, missing or duplicate titles and descriptions, long titles, pictures without alt text, broken structured data, thin pages), broken and redirected internal links, generic link words, llms.txt; speed: true adds Core Web Vitals for the homepage (PageSpeed Insights: real visitors when Google has them, else the median of three lab runs). Findings ranked p0\u2026p4 with the fix and a tool when one can do it. Read-only. Internal-link layout: wp_internal_links_plan.",
45733
45818
  parameters: {
45734
45819
  type: "object",
45735
45820
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.12.16",
3
+ "version": "0.12.18",
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",