@cmo-so/cli 0.12.12 → 0.12.14

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 +78 -22
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -17013,7 +17013,29 @@ async function techAudit(site, o = {}) {
17013
17013
  const add = (f) => findings.push(f);
17014
17014
  const robotsR = await get(`${base}/robots.txt`, "follow");
17015
17015
  const robots = robotsR?.status === 200 ? robotsR.text : "";
17016
- const { urls, sitemap } = await sitemapUrls(base, robots);
17016
+ const visible = await site.request({ path: "/wp/v2/settings", query: { _fields: "blog_public" } }).then((r) => r.status === 200 && r.data.blog_public != null ? !!Number(r.data.blog_public) : null).catch(() => null);
17017
+ const engagedOff = visible === false;
17018
+ let { urls, sitemap } = await sitemapUrls(base, robots);
17019
+ let discovered = "sitemap";
17020
+ if (!urls.length) {
17021
+ const rows = await Promise.all(
17022
+ ["/wp/v2/pages", "/wp/v2/posts"].map(
17023
+ (path3) => all(site, path3, { status: "publish", _fields: "link", per_page: 100 }, limit).catch(() => [])
17024
+ )
17025
+ );
17026
+ urls = [...new Set(rows.flat().map((r) => r.link))];
17027
+ discovered = "wordpress";
17028
+ }
17029
+ if (engagedOff)
17030
+ add({
17031
+ id: "search-engines-off",
17032
+ priority: "p1",
17033
+ area: "settings",
17034
+ title: "Search engines are switched off for this site",
17035
+ why: "WordPress tells every search engine not to index it (noindex on every page, no sitemap). Right for a test or unfinished site; check MEMORY.md before changing it.",
17036
+ fix: "When the site is ready: wp_settings_set search_engines true.",
17037
+ tool: { name: "wp_settings_set", args: { search_engines: true } }
17038
+ });
17017
17039
  if (!robotsR || robotsR.status !== 200)
17018
17040
  add({
17019
17041
  id: "robots-missing",
@@ -17033,7 +17055,7 @@ async function techAudit(site, o = {}) {
17033
17055
  fix: "Turn search engines on (wp_settings_set search_engines true) or fix the robots.txt file.",
17034
17056
  tool: { name: "wp_settings_set", args: { search_engines: true } }
17035
17057
  });
17036
- if (robots && !/^sitemap:/im.test(robots))
17058
+ if (robots && !engagedOff && !/^sitemap:/im.test(robots))
17037
17059
  add({
17038
17060
  id: "robots-sitemap",
17039
17061
  priority: "p3",
@@ -17052,7 +17074,7 @@ async function techAudit(site, o = {}) {
17052
17074
  why: "ChatGPT, Claude, Perplexity and Google's AI answers can't read or cite the site.",
17053
17075
  fix: "Allow them in robots.txt unless the owner chose to block AI."
17054
17076
  });
17055
- if (!sitemap)
17077
+ if (!sitemap && !engagedOff)
17056
17078
  add({
17057
17079
  id: "sitemap-missing",
17058
17080
  priority: "p1",
@@ -17074,6 +17096,7 @@ async function techAudit(site, o = {}) {
17074
17096
  tool: { name: "wp_llms_txt_set", args: { generate: true } }
17075
17097
  });
17076
17098
  const crawl = urls.length ? urls.slice(0, limit) : [`${base}/`];
17099
+ if (!crawl.some((u) => sitePath(u, base) === "/")) crawl.unshift(`${base}/`);
17077
17100
  const scans = await pool3(crawl, 6, async (u) => {
17078
17101
  const r = await get(u);
17079
17102
  if (!r) return null;
@@ -17093,7 +17116,7 @@ async function techAudit(site, o = {}) {
17093
17116
  fix: p.status >= 400 ? "Publish the page again, or remove it (wp_post_delete / wp_page_publish)." : "List the final address; the redirect is fine for visitors."
17094
17117
  });
17095
17118
  for (const p of ok2) {
17096
- if (/noindex/.test(p.robots))
17119
+ if (/noindex/.test(p.robots) && !engagedOff && discovered === "sitemap")
17097
17120
  add({
17098
17121
  id: `noindex-in-sitemap:${p.path}`,
17099
17122
  priority: "p1",
@@ -17270,7 +17293,9 @@ async function techAudit(site, o = {}) {
17270
17293
  summary: {
17271
17294
  ...Object.fromEntries(order2.map((p) => [p, findings.filter((f) => f.priority === p).length])),
17272
17295
  sitemap,
17273
- sitemap_urls: urls.length,
17296
+ pages_from: discovered,
17297
+ search_engines: visible,
17298
+ sitemap_urls: discovered === "sitemap" ? urls.length : 0,
17274
17299
  crawled: pages.length,
17275
17300
  live: ok2.length,
17276
17301
  llms_txt: hasLlms,
@@ -17298,7 +17323,10 @@ async function pageSpeed(url) {
17298
17323
  const r = await fetch(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?${q3}`, {
17299
17324
  signal: AbortSignal.timeout(9e4)
17300
17325
  }).catch(() => null);
17301
- if (!r || !r.ok) return { error: `PageSpeed Insights didn't answer (${r?.status ?? "timeout"}).` };
17326
+ if (!r || !r.ok)
17327
+ return {
17328
+ error: `PageSpeed Insights didn't answer (${r?.status ?? "timeout"})${r?.status === 429 && !key ? ": the server needs a PAGESPEED_API_KEY (Google Cloud, free)" : ""}.`
17329
+ };
17302
17330
  const j = await r.json();
17303
17331
  const f = j.loadingExperience?.metrics ?? {};
17304
17332
  const a = j.lighthouseResult?.audits ?? {};
@@ -17415,8 +17443,24 @@ async function linksPlan(site, o = {}) {
17415
17443
  const blogPage = docs.find((d) => d.id === settings2.page_for_posts && d.kind === "page");
17416
17444
  const actions = [];
17417
17445
  const findings = [];
17418
- const pageDocs = docs.filter((d) => d.kind === "page" && d !== home);
17419
- const postDocs = docs.filter((d) => d.kind === "post");
17446
+ const sample = docs.filter(
17447
+ (d) => d.kind === "page" && (/\/sample-page\/$/.test(d.path) || d.title === "Sample Page") || d.kind === "post" && (/\/hello-world\/$/.test(d.path) || d.title === "Hello world!")
17448
+ );
17449
+ for (const d of sample)
17450
+ findings.push({
17451
+ id: `sample:${d.path}`,
17452
+ priority: "p2",
17453
+ area: "outside",
17454
+ title: `WordPress's sample ${d.kind === "page" ? "page" : "article"} \u201C${d.title}\u201D is still published`,
17455
+ why: "It tells visitors and search engines the site isn't finished, and dilutes the real pages.",
17456
+ fix: "Move it to the trash.",
17457
+ tool: {
17458
+ name: d.kind === "page" ? "wp_page_delete" : "wp_post_delete",
17459
+ args: d.kind === "page" ? { page: d.id } : { post: d.id }
17460
+ }
17461
+ });
17462
+ const pageDocs = docs.filter((d) => d.kind === "page" && d !== home && !sample.includes(d));
17463
+ const postDocs = docs.filter((d) => d.kind === "post" && !sample.includes(d));
17420
17464
  const menuItems = flatMenu(menu?.items ?? []);
17421
17465
  const top = (menu?.items ?? []).length;
17422
17466
  for (const i of menuItems) {
@@ -17504,7 +17548,7 @@ async function linksPlan(site, o = {}) {
17504
17548
  for (const post of postDocs) {
17505
17549
  let best = null;
17506
17550
  let score = 0;
17507
- for (const pg of pageDocs) {
17551
+ for (const pg of home ? [...pageDocs, home] : pageDocs) {
17508
17552
  if (pg === blogPage) continue;
17509
17553
  const s = overlap(post.words, pg.words, idf);
17510
17554
  if (s > score) [best, score] = [pg, s];
@@ -17536,7 +17580,7 @@ async function linksPlan(site, o = {}) {
17536
17580
  }
17537
17581
  const lonelyPosts = [];
17538
17582
  for (const d of docs) {
17539
- if (d === home || d === blogPage) continue;
17583
+ if (d === home || d === blogPage || sample.includes(d)) continue;
17540
17584
  const ins = inbound.get(d.path);
17541
17585
  if (ins && ins.size || chrome.has(d.path)) continue;
17542
17586
  if (d.kind === "post") lonelyPosts.push(d);
@@ -17612,7 +17656,7 @@ async function llmsTxt(site, o = {}) {
17612
17656
  const base = (site.url ?? "").replace(/\/+$/, "");
17613
17657
  const s = await site.request({
17614
17658
  path: "/wp/v2/settings",
17615
- query: { _fields: "title,description,page_on_front,page_for_posts" }
17659
+ query: { _fields: "title,description,page_on_front,page_for_posts,blog_public" }
17616
17660
  }).then((r) => r.status === 200 ? r.data : {});
17617
17661
  const pages = await listPages(site, { status: "publish" });
17618
17662
  const posts2 = await all(
@@ -17621,10 +17665,20 @@ async function llmsTxt(site, o = {}) {
17621
17665
  { status: "publish", per_page: 100, _fields: "link,title,excerpt,meta", orderby: "date", order: "desc" },
17622
17666
  o.posts ?? 25
17623
17667
  );
17624
- const desc = (m, fallback = "") => String(m?.maggie_seo_description || fallback).replace(/\s+/g, " ").trim();
17668
+ const clip3 = (t, n2 = 200) => {
17669
+ const x = t.replace(/\s+/g, " ").trim();
17670
+ if (x.length <= n2) return x;
17671
+ const cut = x.slice(0, n2);
17672
+ return `${cut.slice(0, Math.max(cut.lastIndexOf(" "), n2 - 30)).replace(/[\s,;:–-]+$/, "")}\u2026`;
17673
+ };
17674
+ const desc = (m, fallback = "") => clip3(String(m?.maggie_seo_description || fallback));
17675
+ const isSample = (link, title) => /\/(sample-page|hello-world)\/?$/.test(link) || title === "Sample Page" || title === "Hello world!";
17625
17676
  const home = pages.find((p) => p.id === s.page_on_front);
17626
17677
  const noise = /privacy|terms|cookie|thank|cart|checkout|my-account|login/i;
17627
- const visible = pages.filter((p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link));
17678
+ const visible = pages.filter(
17679
+ (p) => p.id !== s.page_on_front && p.id !== s.page_for_posts && !noise.test(p.link) && !isSample(p.link, titleOf(p.title))
17680
+ );
17681
+ const realPosts = posts2.filter((p) => !isSample(p.link, strip(p.title.rendered ?? "")));
17628
17682
  const byParent = /* @__PURE__ */ new Map();
17629
17683
  for (const p of visible) byParent.set(p.parent, [...byParent.get(p.parent) ?? [], p]);
17630
17684
  const line = (title, url, d) => `- [${title.replace(/[[\]]/g, "")}](${url})${d ? `: ${d}` : ""}`;
@@ -17632,25 +17686,27 @@ async function llmsTxt(site, o = {}) {
17632
17686
  const summary = desc(home?.meta, s.description ?? "");
17633
17687
  if (summary) out.push(`> ${summary}`, "");
17634
17688
  const top = byParent.get(0) ?? [];
17635
- if (top.length) {
17689
+ if (top.length || home) {
17636
17690
  out.push("## Pages", "");
17691
+ if (home) out.push(line(titleOf(home.title) || "Home", home.link, summary));
17637
17692
  for (const p of top) {
17638
17693
  out.push(line(titleOf(p.title), p.link, desc(p.meta)));
17639
17694
  for (const c of byParent.get(p.id) ?? []) out.push(` ${line(titleOf(c.title), c.link, desc(c.meta))}`);
17640
17695
  }
17641
17696
  out.push("");
17642
17697
  }
17643
- if (posts2.length) {
17698
+ if (realPosts.length) {
17644
17699
  out.push("## Articles", "");
17645
- for (const p of posts2)
17646
- out.push(
17647
- line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? "").slice(0, 160)))
17648
- );
17700
+ for (const p of realPosts)
17701
+ out.push(line(strip(p.title.rendered ?? ""), p.link, desc(p.meta, strip(p.excerpt?.rendered ?? ""))));
17649
17702
  out.push("");
17650
17703
  }
17651
17704
  const legal = pages.filter((p) => /privacy|terms/i.test(p.link));
17652
- out.push("## Optional", "", line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article"));
17653
- for (const p of legal) out.push(line(titleOf(p.title), p.link, ""));
17705
+ const optional2 = [
17706
+ ...s.blog_public === 0 ? [] : [line("Sitemap", `${base}/wp-sitemap.xml`, "every page and article")],
17707
+ ...legal.map((p) => line(titleOf(p.title), p.link, ""))
17708
+ ];
17709
+ if (optional2.length) out.push("## Optional", "", ...optional2);
17654
17710
  return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trim()}
17655
17711
  `;
17656
17712
  }
@@ -23844,7 +23900,7 @@ var program = new Command();
23844
23900
  // package.json
23845
23901
  var package_default = {
23846
23902
  name: "@cmo-so/cli",
23847
- version: "0.12.12",
23903
+ version: "0.12.14",
23848
23904
  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.",
23849
23905
  type: "module",
23850
23906
  license: "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.12.12",
3
+ "version": "0.12.14",
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",