@cmo-so/mcp 0.7.6 → 0.7.7

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 +186 -4
  2. 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.7.6",
7202
+ version: "0.7.7",
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",
@@ -8202,9 +8202,9 @@ function domainApi(api2, domain) {
8202
8202
  /** The last time posts delivered by API pull were found in the website's sitemap (gives them their address). */
8203
8203
  sitemapMatch: () => api2.request(`${base}/rewrites/sitemap-match`),
8204
8204
  /** Read the sitemap (found on its own when blank) and match API-pull posts to their live address. */
8205
- runSitemapMatch: (sitemapUrl) => post(
8205
+ runSitemapMatch: (sitemapUrl2) => post(
8206
8206
  `${base}/rewrites/sitemap-match`,
8207
- sitemapUrl ? { sitemap_url: sitemapUrl } : {}
8207
+ sitemapUrl2 ? { sitemap_url: sitemapUrl2 } : {}
8208
8208
  ),
8209
8209
  rewritePolicy: () => api2.request(`${base}/rewrite-policy`),
8210
8210
  setRewritePolicy: (p) => put(`${base}/rewrite-policy`, p),
@@ -8341,6 +8341,75 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
8341
8341
  };
8342
8342
  }
8343
8343
 
8344
+ // ../../packages/api-client/src/sitemaps.ts
8345
+ var SITEMAPS_MAX = 20;
8346
+ var SITEMAP_LABEL_MAX = 80;
8347
+ var SITEMAP_OWNERS = ["own", "competitor", "other"];
8348
+ var SITEMAP_USES = {
8349
+ own: ["post_ideas", "channel_ideas", "internal_links", "post_matching"],
8350
+ competitor: ["post_ideas", "channel_ideas"],
8351
+ other: ["post_ideas", "channel_ideas"]
8352
+ };
8353
+ function sitemapUrl(input) {
8354
+ let v = input.trim();
8355
+ if (!v || /^[a-z][a-z0-9+.-]*:(?!\/\/|\d)/i.test(v)) return null;
8356
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) v = `https://${v}`;
8357
+ let u;
8358
+ try {
8359
+ u = new URL(v);
8360
+ } catch {
8361
+ return null;
8362
+ }
8363
+ if (u.protocol !== "http:" && u.protocol !== "https:" || u.username || u.password) return null;
8364
+ if (!u.hostname.includes(".") || /\s/.test(v)) return null;
8365
+ u.protocol = "https:";
8366
+ return u.toString();
8367
+ }
8368
+ function sitemapUses(owner, uses) {
8369
+ const allowed2 = SITEMAP_USES[owner];
8370
+ const kept = allowed2.filter((u) => uses?.includes(u));
8371
+ return kept.length ? kept : [...allowed2];
8372
+ }
8373
+ function onWebsite(url, domain) {
8374
+ let host;
8375
+ try {
8376
+ host = new URL(url).hostname.toLowerCase();
8377
+ } catch {
8378
+ return false;
8379
+ }
8380
+ const d = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "");
8381
+ return Boolean(d) && (host === d || host.endsWith(`.${d}`));
8382
+ }
8383
+ function cleanSitemap(entry, domain) {
8384
+ const url = sitemapUrl(entry.url ?? "");
8385
+ if (!url) return "The sitemap must be a web address starting with http:// or https://.";
8386
+ let owner = SITEMAP_OWNERS.includes(entry.owner) ? entry.owner : "other";
8387
+ if (owner === "own" && domain && !onWebsite(url, domain)) owner = "other";
8388
+ const label = (entry.label ?? "").trim().slice(0, SITEMAP_LABEL_MAX);
8389
+ const out = { ...entry, url, owner, label: label || null, uses: sitemapUses(owner, entry.uses) };
8390
+ if (!out.id) delete out.id;
8391
+ return out;
8392
+ }
8393
+ function checkSitemaps(list2) {
8394
+ if (list2.length > SITEMAPS_MAX) return `Up to ${SITEMAPS_MAX} sitemaps. Remove one first.`;
8395
+ const seen = /* @__PURE__ */ new Set();
8396
+ for (const s of list2) {
8397
+ const k = s.url.trim().toLowerCase();
8398
+ if (seen.has(k)) return `${s.url} is already in the list.`;
8399
+ seen.add(k);
8400
+ }
8401
+ return null;
8402
+ }
8403
+ function sitemapCheckLine(check) {
8404
+ if (!check || check.status === "unchecked") return "Checked when you save";
8405
+ if (check.status === "error") return check.error?.trim() || "Couldn't read this sitemap.";
8406
+ const n = check.url_count;
8407
+ const found = n == null ? "Read" : `Found ${n.toLocaleString("en-GB")} ${n === 1 ? "page" : "pages"}`;
8408
+ const at = check.checked_at ? new Date(check.checked_at) : null;
8409
+ const when = at && !Number.isNaN(at.getTime()) ? at.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : null;
8410
+ return when ? `${found} \xB7 checked ${when}` : found;
8411
+ }
8412
+
8344
8413
  // ../../packages/domain/src/copy/maggie.ts
8345
8414
  var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
8346
8415
  var named = (title, id) => {
@@ -8428,6 +8497,22 @@ var confirmCopy = {
8428
8497
  en: (a) => `Remove the competitor ${q(a.name)}.`,
8429
8498
  "zh-Hant": (a) => `\u522A\u9664\u7AF6\u722D\u5C0D\u624B ${q(a.name)}\u3002`
8430
8499
  },
8500
+ sitemap_upsert: {
8501
+ en: (a) => {
8502
+ const whose = { own: "your site's ", competitor: "a competitor's ", other: "another site's " }[a.owner];
8503
+ const label = a.label ? ` (${q(a.label)})` : "";
8504
+ return a.url ? `Save ${whose ?? "the "}sitemap ${a.url}${label}.` : `Change the sitemap${label}: ${keys(a).filter((k) => k !== "id").join(", ")}.`;
8505
+ },
8506
+ "zh-Hant": (a) => {
8507
+ const whose = { own: "\u81EA\u5DF1\u7DB2\u7AD9\u7684", competitor: "\u7AF6\u722D\u5C0D\u624B\u7684", other: "\u5176\u4ED6\u7DB2\u7AD9\u7684" }[a.owner];
8508
+ const label = a.label ? `\uFF08${q(a.label)}\uFF09` : "";
8509
+ return a.url ? `\u5132\u5B58${whose ?? ""} sitemap ${a.url}${label}\u3002` : `\u4FEE\u6539 sitemap${label}\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001")}\u3002`;
8510
+ }
8511
+ },
8512
+ sitemap_remove: {
8513
+ en: (a) => a.url ?? a.id ? `Remove the sitemap ${a.url ?? a.id}.` : "Remove a sitemap.",
8514
+ "zh-Hant": (a) => a.url ?? a.id ? `\u522A\u9664 sitemap ${a.url ?? a.id}\u3002` : "\u522A\u9664\u4E00\u500B sitemap\u3002"
8515
+ },
8431
8516
  brand_research_update: {
8432
8517
  en: (a) => `Update your market notes: ${keys(a).join(", ") || "nothing"}.`,
8433
8518
  "zh-Hant": (a) => `\u66F4\u65B0\u5E02\u5834\u8CC7\u6599\uFF1A${keys(a).join("\u3001") || "\u6C92\u6709"}\u3002`
@@ -9880,6 +9965,85 @@ var brandTools = [
9880
9965
  return { found: out.found, suggestions: out.items.map(({ id, name, website }) => ({ id, name, website })) };
9881
9966
  }
9882
9967
  }),
9968
+ tool({
9969
+ name: "sitemap_list",
9970
+ description: "The sitemaps saved in the business profile: the website's own, competitors' and other sites' (industry sites, partners), each with what it is used for (post_ideas = blog post ideas, channel_ideas = social post ideas, internal_links and post_matching = own sitemaps only) and its last check (pages found or the error). Max 20.",
9971
+ parameters: none,
9972
+ group: "brand",
9973
+ need: { resource: "overview", level: "read" },
9974
+ risk: "R0",
9975
+ run: async (ctx) => {
9976
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
9977
+ return { max: SITEMAPS_MAX, sitemaps: list2.map(sitemapResult) };
9978
+ }
9979
+ }),
9980
+ tool({
9981
+ name: "sitemap_upsert",
9982
+ description: "Add a sitemap to the business profile, or change one (found by id from sitemap_list, else by url). owner: own (the website's own; its host must be the website or a subdomain, otherwise it is saved as other), competitor, or other. uses: own may have post_ideas, channel_ideas, internal_links, post_matching; competitor/other only post_ideas, channel_ideas; leave out for all allowed. A new sitemap needs url and owner. Only sitemap addresses the user gives or confirms (never guess one). Saving checks the sitemap; an unreachable one is still saved with the error. Max 20.",
9983
+ parameters: {
9984
+ type: "object",
9985
+ properties: {
9986
+ id: { type: "string", description: "The sitemap to change (from sitemap_list)" },
9987
+ url: { type: "string", description: "The sitemap's http(s) address; with id, its new address" },
9988
+ owner: { type: "string", enum: ["own", "competitor", "other"] },
9989
+ label: { type: "string", description: "A short name, up to 80 characters" },
9990
+ uses: {
9991
+ type: "array",
9992
+ items: { type: "string", enum: ["post_ideas", "channel_ideas", "internal_links", "post_matching"] }
9993
+ }
9994
+ }
9995
+ },
9996
+ group: "brand",
9997
+ need: write2,
9998
+ risk: "W1",
9999
+ run: async (ctx, args) => {
10000
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
10001
+ const i = findSitemap(list2, args.id, args.id ? void 0 : args.url);
10002
+ if (args.id && i < 0) throw new Error(`No sitemap with id ${args.id}. Use sitemap_list.`);
10003
+ const old = i >= 0 ? list2[i] : void 0;
10004
+ if (!old && (!args.url || !args.owner)) throw new Error("A new sitemap needs url and owner.");
10005
+ const owner = args.owner ?? old?.owner ?? "other";
10006
+ const entry = cleanSitemap(
10007
+ {
10008
+ ...old,
10009
+ url: args.url ?? old?.url ?? "",
10010
+ owner,
10011
+ label: args.label ?? old?.label ?? null,
10012
+ // A new owner without uses gets that owner's defaults.
10013
+ uses: args.uses ?? (args.owner && args.owner !== old?.owner ? [] : old?.uses ?? [])
10014
+ },
10015
+ ctx.domain ?? void 0
10016
+ );
10017
+ if (typeof entry === "string") throw new Error(entry);
10018
+ if (old && sitemapUrl(old.url) !== entry.url) delete entry.check;
10019
+ const next = old ? list2.map((x, n) => n === i ? entry : x) : [...list2, entry];
10020
+ const problem = checkSitemaps(next);
10021
+ if (problem) throw new Error(problem);
10022
+ const saved = (await ws(ctx).updateBrand({ sitemaps: next })).sitemaps ?? next;
10023
+ const mine = saved[findSitemap(saved, void 0, entry.url)];
10024
+ return {
10025
+ saved: mine ? sitemapResult(mine) : sitemapResult(entry),
10026
+ ...(mine?.owner ?? entry.owner) !== owner ? { note: "Not on this website, so it was saved as another site's sitemap (owner other)." } : {},
10027
+ sitemaps: saved.length
10028
+ };
10029
+ }
10030
+ }),
10031
+ tool({
10032
+ name: "sitemap_remove",
10033
+ description: "Remove a sitemap from the business profile (by id from sitemap_list, or by url). Maggie stops reading it; ideas already found stay.",
10034
+ parameters: { type: "object", properties: { id: { type: "string" }, url: { type: "string" } } },
10035
+ group: "brand",
10036
+ need: write2,
10037
+ risk: "W1",
10038
+ run: async (ctx, { id, url }) => {
10039
+ if (!id && !url) throw new Error("Say which sitemap: id or url.");
10040
+ const list2 = (await ws(ctx).brand()).sitemaps ?? [];
10041
+ const i = findSitemap(list2, id, url);
10042
+ if (i < 0) throw new Error(`No sitemap ${id ?? url} was found. Use sitemap_list.`);
10043
+ const saved = (await ws(ctx).updateBrand({ sitemaps: list2.filter((_, n) => n !== i) })).sitemaps ?? [];
10044
+ return { removed: list2[i]?.url, sitemaps: saved.map((s) => s.url) };
10045
+ }
10046
+ }),
9883
10047
  tool({
9884
10048
  name: "brand_research_update",
9885
10049
  description: "Change the profile's market notes: market_research (text), additional_information (anything else Maggie should know) or the SWOT list (each {name: Strengths|Weaknesses|Opportunities|Threats, description}). A list replaces the whole list.",
@@ -10015,6 +10179,22 @@ function brandGaps(b) {
10015
10179
  check("industries", b.industries, "the industry, for examples and wording");
10016
10180
  return { complete: gaps.length === 0, gaps };
10017
10181
  }
10182
+ function findSitemap(list2, id, url) {
10183
+ if (id) return list2.findIndex((s) => s.id === id);
10184
+ const u = url ? (sitemapUrl(url) ?? url).toLowerCase() : "";
10185
+ return u ? list2.findIndex((s) => (sitemapUrl(s.url) ?? s.url).toLowerCase() === u) : -1;
10186
+ }
10187
+ function sitemapResult(s) {
10188
+ return {
10189
+ id: s.id ?? null,
10190
+ url: s.url,
10191
+ owner: s.owner,
10192
+ label: s.label ?? null,
10193
+ uses: s.uses,
10194
+ check: s.check?.status ?? "unchecked",
10195
+ status: sitemapCheckLine(s.check)
10196
+ };
10197
+ }
10018
10198
 
10019
10199
  // ../../packages/tools/src/feedback.ts
10020
10200
  var account = { resource: "account", level: "read" };
@@ -22422,7 +22602,9 @@ var OPEN_WORLD = /* @__PURE__ */ new Set([
22422
22602
  "seo_audit_run",
22423
22603
  "ai_visibility_prompt_check",
22424
22604
  "idea_add",
22425
- "competitor_suggestions_find"
22605
+ "competitor_suggestions_find",
22606
+ // Saving makes CMO.so fetch the sitemap from its website.
22607
+ "sitemap_upsert"
22426
22608
  ]);
22427
22609
  var OVERWRITES = /* @__PURE__ */ new Set([
22428
22610
  "ai_visibility_keywords_set",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
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",