@cmo-so/cli 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.
- package/dist/cmo.mjs +186 -4
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -8145,9 +8145,9 @@ function domainApi(api2, domain) {
|
|
|
8145
8145
|
/** The last time posts delivered by API pull were found in the website's sitemap (gives them their address). */
|
|
8146
8146
|
sitemapMatch: () => api2.request(`${base}/rewrites/sitemap-match`),
|
|
8147
8147
|
/** Read the sitemap (found on its own when blank) and match API-pull posts to their live address. */
|
|
8148
|
-
runSitemapMatch: (
|
|
8148
|
+
runSitemapMatch: (sitemapUrl2) => post(
|
|
8149
8149
|
`${base}/rewrites/sitemap-match`,
|
|
8150
|
-
|
|
8150
|
+
sitemapUrl2 ? { sitemap_url: sitemapUrl2 } : {}
|
|
8151
8151
|
),
|
|
8152
8152
|
rewritePolicy: () => api2.request(`${base}/rewrite-policy`),
|
|
8153
8153
|
setRewritePolicy: (p) => put(`${base}/rewrite-policy`, p),
|
|
@@ -8284,6 +8284,75 @@ function createSession({ oauth: oauth2, store, skewMs = 6e4, now = Date.now, onS
|
|
|
8284
8284
|
};
|
|
8285
8285
|
}
|
|
8286
8286
|
|
|
8287
|
+
// ../../packages/api-client/src/sitemaps.ts
|
|
8288
|
+
var SITEMAPS_MAX = 20;
|
|
8289
|
+
var SITEMAP_LABEL_MAX = 80;
|
|
8290
|
+
var SITEMAP_OWNERS = ["own", "competitor", "other"];
|
|
8291
|
+
var SITEMAP_USES = {
|
|
8292
|
+
own: ["post_ideas", "channel_ideas", "internal_links", "post_matching"],
|
|
8293
|
+
competitor: ["post_ideas", "channel_ideas"],
|
|
8294
|
+
other: ["post_ideas", "channel_ideas"]
|
|
8295
|
+
};
|
|
8296
|
+
function sitemapUrl(input) {
|
|
8297
|
+
let v = input.trim();
|
|
8298
|
+
if (!v || /^[a-z][a-z0-9+.-]*:(?!\/\/|\d)/i.test(v)) return null;
|
|
8299
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(v)) v = `https://${v}`;
|
|
8300
|
+
let u;
|
|
8301
|
+
try {
|
|
8302
|
+
u = new URL(v);
|
|
8303
|
+
} catch {
|
|
8304
|
+
return null;
|
|
8305
|
+
}
|
|
8306
|
+
if (u.protocol !== "http:" && u.protocol !== "https:" || u.username || u.password) return null;
|
|
8307
|
+
if (!u.hostname.includes(".") || /\s/.test(v)) return null;
|
|
8308
|
+
u.protocol = "https:";
|
|
8309
|
+
return u.toString();
|
|
8310
|
+
}
|
|
8311
|
+
function sitemapUses(owner, uses) {
|
|
8312
|
+
const allowed2 = SITEMAP_USES[owner];
|
|
8313
|
+
const kept = allowed2.filter((u) => uses?.includes(u));
|
|
8314
|
+
return kept.length ? kept : [...allowed2];
|
|
8315
|
+
}
|
|
8316
|
+
function onWebsite(url, domain) {
|
|
8317
|
+
let host;
|
|
8318
|
+
try {
|
|
8319
|
+
host = new URL(url).hostname.toLowerCase();
|
|
8320
|
+
} catch {
|
|
8321
|
+
return false;
|
|
8322
|
+
}
|
|
8323
|
+
const d = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "");
|
|
8324
|
+
return Boolean(d) && (host === d || host.endsWith(`.${d}`));
|
|
8325
|
+
}
|
|
8326
|
+
function cleanSitemap(entry, domain) {
|
|
8327
|
+
const url = sitemapUrl(entry.url ?? "");
|
|
8328
|
+
if (!url) return "The sitemap must be a web address starting with http:// or https://.";
|
|
8329
|
+
let owner = SITEMAP_OWNERS.includes(entry.owner) ? entry.owner : "other";
|
|
8330
|
+
if (owner === "own" && domain && !onWebsite(url, domain)) owner = "other";
|
|
8331
|
+
const label = (entry.label ?? "").trim().slice(0, SITEMAP_LABEL_MAX);
|
|
8332
|
+
const out = { ...entry, url, owner, label: label || null, uses: sitemapUses(owner, entry.uses) };
|
|
8333
|
+
if (!out.id) delete out.id;
|
|
8334
|
+
return out;
|
|
8335
|
+
}
|
|
8336
|
+
function checkSitemaps(list2) {
|
|
8337
|
+
if (list2.length > SITEMAPS_MAX) return `Up to ${SITEMAPS_MAX} sitemaps. Remove one first.`;
|
|
8338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8339
|
+
for (const s of list2) {
|
|
8340
|
+
const k = s.url.trim().toLowerCase();
|
|
8341
|
+
if (seen.has(k)) return `${s.url} is already in the list.`;
|
|
8342
|
+
seen.add(k);
|
|
8343
|
+
}
|
|
8344
|
+
return null;
|
|
8345
|
+
}
|
|
8346
|
+
function sitemapCheckLine(check) {
|
|
8347
|
+
if (!check || check.status === "unchecked") return "Checked when you save";
|
|
8348
|
+
if (check.status === "error") return check.error?.trim() || "Couldn't read this sitemap.";
|
|
8349
|
+
const n = check.url_count;
|
|
8350
|
+
const found = n == null ? "Read" : `Found ${n.toLocaleString("en-GB")} ${n === 1 ? "page" : "pages"}`;
|
|
8351
|
+
const at = check.checked_at ? new Date(check.checked_at) : null;
|
|
8352
|
+
const when = at && !Number.isNaN(at.getTime()) ? at.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }) : null;
|
|
8353
|
+
return when ? `${found} \xB7 checked ${when}` : found;
|
|
8354
|
+
}
|
|
8355
|
+
|
|
8287
8356
|
// ../../packages/domain/src/copy/maggie.ts
|
|
8288
8357
|
var q = (s) => `\u201C${String(s ?? "").slice(0, 120)}\u201D`;
|
|
8289
8358
|
var named = (title, id) => {
|
|
@@ -8371,6 +8440,22 @@ var confirmCopy = {
|
|
|
8371
8440
|
en: (a) => `Remove the competitor ${q(a.name)}.`,
|
|
8372
8441
|
"zh-Hant": (a) => `\u522A\u9664\u7AF6\u722D\u5C0D\u624B ${q(a.name)}\u3002`
|
|
8373
8442
|
},
|
|
8443
|
+
sitemap_upsert: {
|
|
8444
|
+
en: (a) => {
|
|
8445
|
+
const whose = { own: "your site's ", competitor: "a competitor's ", other: "another site's " }[a.owner];
|
|
8446
|
+
const label = a.label ? ` (${q(a.label)})` : "";
|
|
8447
|
+
return a.url ? `Save ${whose ?? "the "}sitemap ${a.url}${label}.` : `Change the sitemap${label}: ${keys(a).filter((k) => k !== "id").join(", ")}.`;
|
|
8448
|
+
},
|
|
8449
|
+
"zh-Hant": (a) => {
|
|
8450
|
+
const whose = { own: "\u81EA\u5DF1\u7DB2\u7AD9\u7684", competitor: "\u7AF6\u722D\u5C0D\u624B\u7684", other: "\u5176\u4ED6\u7DB2\u7AD9\u7684" }[a.owner];
|
|
8451
|
+
const label = a.label ? `\uFF08${q(a.label)}\uFF09` : "";
|
|
8452
|
+
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`;
|
|
8453
|
+
}
|
|
8454
|
+
},
|
|
8455
|
+
sitemap_remove: {
|
|
8456
|
+
en: (a) => a.url ?? a.id ? `Remove the sitemap ${a.url ?? a.id}.` : "Remove a sitemap.",
|
|
8457
|
+
"zh-Hant": (a) => a.url ?? a.id ? `\u522A\u9664 sitemap ${a.url ?? a.id}\u3002` : "\u522A\u9664\u4E00\u500B sitemap\u3002"
|
|
8458
|
+
},
|
|
8374
8459
|
brand_research_update: {
|
|
8375
8460
|
en: (a) => `Update your market notes: ${keys(a).join(", ") || "nothing"}.`,
|
|
8376
8461
|
"zh-Hant": (a) => `\u66F4\u65B0\u5E02\u5834\u8CC7\u6599\uFF1A${keys(a).join("\u3001") || "\u6C92\u6709"}\u3002`
|
|
@@ -9823,6 +9908,85 @@ var brandTools = [
|
|
|
9823
9908
|
return { found: out.found, suggestions: out.items.map(({ id, name, website }) => ({ id, name, website })) };
|
|
9824
9909
|
}
|
|
9825
9910
|
}),
|
|
9911
|
+
tool({
|
|
9912
|
+
name: "sitemap_list",
|
|
9913
|
+
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.",
|
|
9914
|
+
parameters: none,
|
|
9915
|
+
group: "brand",
|
|
9916
|
+
need: { resource: "overview", level: "read" },
|
|
9917
|
+
risk: "R0",
|
|
9918
|
+
run: async (ctx) => {
|
|
9919
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
9920
|
+
return { max: SITEMAPS_MAX, sitemaps: list2.map(sitemapResult) };
|
|
9921
|
+
}
|
|
9922
|
+
}),
|
|
9923
|
+
tool({
|
|
9924
|
+
name: "sitemap_upsert",
|
|
9925
|
+
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.",
|
|
9926
|
+
parameters: {
|
|
9927
|
+
type: "object",
|
|
9928
|
+
properties: {
|
|
9929
|
+
id: { type: "string", description: "The sitemap to change (from sitemap_list)" },
|
|
9930
|
+
url: { type: "string", description: "The sitemap's http(s) address; with id, its new address" },
|
|
9931
|
+
owner: { type: "string", enum: ["own", "competitor", "other"] },
|
|
9932
|
+
label: { type: "string", description: "A short name, up to 80 characters" },
|
|
9933
|
+
uses: {
|
|
9934
|
+
type: "array",
|
|
9935
|
+
items: { type: "string", enum: ["post_ideas", "channel_ideas", "internal_links", "post_matching"] }
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
},
|
|
9939
|
+
group: "brand",
|
|
9940
|
+
need: write2,
|
|
9941
|
+
risk: "W1",
|
|
9942
|
+
run: async (ctx, args) => {
|
|
9943
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
9944
|
+
const i = findSitemap(list2, args.id, args.id ? void 0 : args.url);
|
|
9945
|
+
if (args.id && i < 0) throw new Error(`No sitemap with id ${args.id}. Use sitemap_list.`);
|
|
9946
|
+
const old = i >= 0 ? list2[i] : void 0;
|
|
9947
|
+
if (!old && (!args.url || !args.owner)) throw new Error("A new sitemap needs url and owner.");
|
|
9948
|
+
const owner = args.owner ?? old?.owner ?? "other";
|
|
9949
|
+
const entry = cleanSitemap(
|
|
9950
|
+
{
|
|
9951
|
+
...old,
|
|
9952
|
+
url: args.url ?? old?.url ?? "",
|
|
9953
|
+
owner,
|
|
9954
|
+
label: args.label ?? old?.label ?? null,
|
|
9955
|
+
// A new owner without uses gets that owner's defaults.
|
|
9956
|
+
uses: args.uses ?? (args.owner && args.owner !== old?.owner ? [] : old?.uses ?? [])
|
|
9957
|
+
},
|
|
9958
|
+
ctx.domain ?? void 0
|
|
9959
|
+
);
|
|
9960
|
+
if (typeof entry === "string") throw new Error(entry);
|
|
9961
|
+
if (old && sitemapUrl(old.url) !== entry.url) delete entry.check;
|
|
9962
|
+
const next = old ? list2.map((x, n) => n === i ? entry : x) : [...list2, entry];
|
|
9963
|
+
const problem = checkSitemaps(next);
|
|
9964
|
+
if (problem) throw new Error(problem);
|
|
9965
|
+
const saved = (await ws(ctx).updateBrand({ sitemaps: next })).sitemaps ?? next;
|
|
9966
|
+
const mine = saved[findSitemap(saved, void 0, entry.url)];
|
|
9967
|
+
return {
|
|
9968
|
+
saved: mine ? sitemapResult(mine) : sitemapResult(entry),
|
|
9969
|
+
...(mine?.owner ?? entry.owner) !== owner ? { note: "Not on this website, so it was saved as another site's sitemap (owner other)." } : {},
|
|
9970
|
+
sitemaps: saved.length
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
}),
|
|
9974
|
+
tool({
|
|
9975
|
+
name: "sitemap_remove",
|
|
9976
|
+
description: "Remove a sitemap from the business profile (by id from sitemap_list, or by url). Maggie stops reading it; ideas already found stay.",
|
|
9977
|
+
parameters: { type: "object", properties: { id: { type: "string" }, url: { type: "string" } } },
|
|
9978
|
+
group: "brand",
|
|
9979
|
+
need: write2,
|
|
9980
|
+
risk: "W1",
|
|
9981
|
+
run: async (ctx, { id, url }) => {
|
|
9982
|
+
if (!id && !url) throw new Error("Say which sitemap: id or url.");
|
|
9983
|
+
const list2 = (await ws(ctx).brand()).sitemaps ?? [];
|
|
9984
|
+
const i = findSitemap(list2, id, url);
|
|
9985
|
+
if (i < 0) throw new Error(`No sitemap ${id ?? url} was found. Use sitemap_list.`);
|
|
9986
|
+
const saved = (await ws(ctx).updateBrand({ sitemaps: list2.filter((_, n) => n !== i) })).sitemaps ?? [];
|
|
9987
|
+
return { removed: list2[i]?.url, sitemaps: saved.map((s) => s.url) };
|
|
9988
|
+
}
|
|
9989
|
+
}),
|
|
9826
9990
|
tool({
|
|
9827
9991
|
name: "brand_research_update",
|
|
9828
9992
|
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.",
|
|
@@ -9958,6 +10122,22 @@ function brandGaps(b) {
|
|
|
9958
10122
|
check("industries", b.industries, "the industry, for examples and wording");
|
|
9959
10123
|
return { complete: gaps.length === 0, gaps };
|
|
9960
10124
|
}
|
|
10125
|
+
function findSitemap(list2, id, url) {
|
|
10126
|
+
if (id) return list2.findIndex((s) => s.id === id);
|
|
10127
|
+
const u = url ? (sitemapUrl(url) ?? url).toLowerCase() : "";
|
|
10128
|
+
return u ? list2.findIndex((s) => (sitemapUrl(s.url) ?? s.url).toLowerCase() === u) : -1;
|
|
10129
|
+
}
|
|
10130
|
+
function sitemapResult(s) {
|
|
10131
|
+
return {
|
|
10132
|
+
id: s.id ?? null,
|
|
10133
|
+
url: s.url,
|
|
10134
|
+
owner: s.owner,
|
|
10135
|
+
label: s.label ?? null,
|
|
10136
|
+
uses: s.uses,
|
|
10137
|
+
check: s.check?.status ?? "unchecked",
|
|
10138
|
+
status: sitemapCheckLine(s.check)
|
|
10139
|
+
};
|
|
10140
|
+
}
|
|
9961
10141
|
|
|
9962
10142
|
// ../../packages/tools/src/feedback.ts
|
|
9963
10143
|
var account = { resource: "account", level: "read" };
|
|
@@ -15711,7 +15891,7 @@ var program = new Command();
|
|
|
15711
15891
|
// package.json
|
|
15712
15892
|
var package_default = {
|
|
15713
15893
|
name: "@cmo-so/cli",
|
|
15714
|
-
version: "0.7.
|
|
15894
|
+
version: "0.7.7",
|
|
15715
15895
|
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.",
|
|
15716
15896
|
type: "module",
|
|
15717
15897
|
license: "MIT",
|
|
@@ -25943,7 +26123,9 @@ var OPEN_WORLD = /* @__PURE__ */ new Set([
|
|
|
25943
26123
|
"seo_audit_run",
|
|
25944
26124
|
"ai_visibility_prompt_check",
|
|
25945
26125
|
"idea_add",
|
|
25946
|
-
"competitor_suggestions_find"
|
|
26126
|
+
"competitor_suggestions_find",
|
|
26127
|
+
// Saving makes CMO.so fetch the sitemap from its website.
|
|
26128
|
+
"sitemap_upsert"
|
|
25947
26129
|
]);
|
|
25948
26130
|
var OVERWRITES = /* @__PURE__ */ new Set([
|
|
25949
26131
|
"ai_visibility_keywords_set",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
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",
|