@cmo-so/cli 0.12.0 → 0.12.2
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 +228 -3
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7662,6 +7662,8 @@ function domainApi(api2, domain) {
|
|
|
7662
7662
|
wpBuilder: () => api2.request(`${base}/wp/builder`),
|
|
7663
7663
|
connectWpBuilder: (input) => post(`${base}/wp/builder`, input),
|
|
7664
7664
|
disconnectWpBuilder: () => api2.request(`${base}/wp/builder`, { method: "DELETE" }),
|
|
7665
|
+
/** Put one picture in the site's media library: a public https link, or a local file as base64 (≤ 10 MB). */
|
|
7666
|
+
wpMedia: (input) => post(`${base}/wp/media`, input),
|
|
7665
7667
|
/** One WordPress REST request, signed by TOPY with the saved login (allowlisted routes only). */
|
|
7666
7668
|
wpRequest: (input) => post(`${base}/wp/request`, input),
|
|
7667
7669
|
/** A new website API key (aicmo_…) for the user's own code, shown once; for the CLI, which writes it to .env. */
|
|
@@ -14727,6 +14729,27 @@ function walkTouch(b) {
|
|
|
14727
14729
|
}
|
|
14728
14730
|
|
|
14729
14731
|
// ../../packages/tools/src/wp/transport.ts
|
|
14732
|
+
var IMAGE_EXT = {
|
|
14733
|
+
"image/jpeg": "jpg",
|
|
14734
|
+
"image/png": "png",
|
|
14735
|
+
"image/webp": "webp",
|
|
14736
|
+
"image/gif": "gif",
|
|
14737
|
+
"image/avif": "avif"
|
|
14738
|
+
};
|
|
14739
|
+
function sniffImage(b) {
|
|
14740
|
+
const at = (i, ...v) => v.every((x, k) => b[i + k] === x);
|
|
14741
|
+
const ascii = (i, t) => [...t].every((ch, k) => b[i + k] === ch.charCodeAt(0));
|
|
14742
|
+
if (at(0, 255, 216, 255)) return "image/jpeg";
|
|
14743
|
+
if (at(0, 137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
|
|
14744
|
+
if (ascii(0, "RIFF") && ascii(8, "WEBP")) return "image/webp";
|
|
14745
|
+
if (ascii(0, "GIF87a") || ascii(0, "GIF89a")) return "image/gif";
|
|
14746
|
+
if (ascii(4, "ftypavif") || ascii(4, "ftypavis")) return "image/avif";
|
|
14747
|
+
return null;
|
|
14748
|
+
}
|
|
14749
|
+
function mediaName(name, mime) {
|
|
14750
|
+
const stem = (name ?? "").trim().toLowerCase().replace(/\.[a-z0-9]{2,5}$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "picture";
|
|
14751
|
+
return `${stem}.${IMAGE_EXT[mime]}`;
|
|
14752
|
+
}
|
|
14730
14753
|
var WpError = class extends Error {
|
|
14731
14754
|
constructor(message2, status, code) {
|
|
14732
14755
|
super(message2);
|
|
@@ -14755,8 +14778,10 @@ function directSite(siteUrl, username, password) {
|
|
|
14755
14778
|
fetchPage: publicFetch,
|
|
14756
14779
|
async request(req) {
|
|
14757
14780
|
const method = req.method ?? "GET";
|
|
14758
|
-
const
|
|
14781
|
+
const query = qs(req.query);
|
|
14782
|
+
const r = await fetch(`${base}/?rest_route=${encodeURIComponent(req.path)}${query ? `&${query.slice(1)}` : ""}`, {
|
|
14759
14783
|
method,
|
|
14784
|
+
redirect: "manual",
|
|
14760
14785
|
headers: {
|
|
14761
14786
|
Authorization: auth,
|
|
14762
14787
|
Accept: "application/json",
|
|
@@ -14772,6 +14797,49 @@ function directSite(siteUrl, username, password) {
|
|
|
14772
14797
|
}
|
|
14773
14798
|
const n2 = (h) => r.headers.get(h) != null ? Number(r.headers.get(h)) : void 0;
|
|
14774
14799
|
return { status: r.status, data, total: n2("x-wp-total"), totalPages: n2("x-wp-totalpages") };
|
|
14800
|
+
},
|
|
14801
|
+
async uploadMedia(input) {
|
|
14802
|
+
let bytes = input.data;
|
|
14803
|
+
if (!bytes && input.url) {
|
|
14804
|
+
if (!/^https:\/\//i.test(input.url)) throw new WpError("The picture's link must be https.", 400);
|
|
14805
|
+
const g = await fetch(input.url, { headers: { Accept: "image/*" } });
|
|
14806
|
+
if (!g.ok) throw new WpError(`The picture's link answered HTTP ${g.status}.`, g.status);
|
|
14807
|
+
bytes = new Uint8Array(await g.arrayBuffer());
|
|
14808
|
+
}
|
|
14809
|
+
if (!bytes) throw new WpError("Give the picture's link or the file.", 400);
|
|
14810
|
+
if (bytes.length > 15 * 1024 * 1024) throw new WpError("The picture is larger than 15 MB.", 413);
|
|
14811
|
+
const mime = sniffImage(bytes);
|
|
14812
|
+
if (!mime) throw new WpError("That isn't a JPEG, PNG, WebP, GIF or AVIF picture.", 415);
|
|
14813
|
+
const name = mediaName(input.filename ?? input.url?.split("/").pop()?.split("?")[0], mime);
|
|
14814
|
+
const r = await fetch(`${base}/?rest_route=${encodeURIComponent("/wp/v2/media")}`, {
|
|
14815
|
+
method: "POST",
|
|
14816
|
+
redirect: "manual",
|
|
14817
|
+
headers: {
|
|
14818
|
+
Authorization: auth,
|
|
14819
|
+
"Content-Type": mime,
|
|
14820
|
+
"Content-Disposition": `attachment; filename="${name}"`,
|
|
14821
|
+
Accept: "application/json"
|
|
14822
|
+
},
|
|
14823
|
+
body: bytes
|
|
14824
|
+
});
|
|
14825
|
+
const media = await r.json().catch(() => null);
|
|
14826
|
+
if (!media?.id)
|
|
14827
|
+
throw new WpError(`WordPress didn't take the picture: ${media?.message ?? `HTTP ${r.status}`}.`, r.status);
|
|
14828
|
+
const meta4 = {};
|
|
14829
|
+
if (input.alt) meta4.alt_text = input.alt;
|
|
14830
|
+
if (input.title) meta4.title = input.title;
|
|
14831
|
+
if (input.caption) meta4.caption = input.caption;
|
|
14832
|
+
if (Object.keys(meta4).length)
|
|
14833
|
+
await this.request({ method: "POST", path: `/wp/v2/media/${media.id}`, body: meta4, query: { _fields: "id" } });
|
|
14834
|
+
return {
|
|
14835
|
+
id: media.id,
|
|
14836
|
+
source_url: media.source_url ?? "",
|
|
14837
|
+
mime_type: mime,
|
|
14838
|
+
bytes: bytes.length,
|
|
14839
|
+
width: media.media_details?.width ?? null,
|
|
14840
|
+
height: media.media_details?.height ?? null,
|
|
14841
|
+
alt: input.alt ?? ""
|
|
14842
|
+
};
|
|
14775
14843
|
}
|
|
14776
14844
|
};
|
|
14777
14845
|
}
|
|
@@ -14780,6 +14848,23 @@ function apiSite(api2, domain) {
|
|
|
14780
14848
|
return {
|
|
14781
14849
|
url: `https://${domain}`,
|
|
14782
14850
|
fetchPage: publicFetch,
|
|
14851
|
+
async uploadMedia(input) {
|
|
14852
|
+
let data_base64;
|
|
14853
|
+
if (input.data) {
|
|
14854
|
+
let bin = "";
|
|
14855
|
+
for (let i = 0; i < input.data.length; i += 32768)
|
|
14856
|
+
bin += String.fromCharCode(...input.data.subarray(i, i + 32768));
|
|
14857
|
+
data_base64 = btoa(bin);
|
|
14858
|
+
}
|
|
14859
|
+
return d.wpMedia({
|
|
14860
|
+
url: input.url,
|
|
14861
|
+
data_base64,
|
|
14862
|
+
filename: input.filename,
|
|
14863
|
+
alt: input.alt,
|
|
14864
|
+
title: input.title,
|
|
14865
|
+
caption: input.caption
|
|
14866
|
+
});
|
|
14867
|
+
},
|
|
14783
14868
|
async request(req) {
|
|
14784
14869
|
let r;
|
|
14785
14870
|
try {
|
|
@@ -14812,6 +14897,12 @@ async function call(site, req) {
|
|
|
14812
14897
|
const r = await site.request(req);
|
|
14813
14898
|
if (r.status >= 200 && r.status < 300) return r.data;
|
|
14814
14899
|
const body = r.data;
|
|
14900
|
+
if (r.status >= 300 && r.status < 400)
|
|
14901
|
+
throw new WpError(
|
|
14902
|
+
`WordPress redirected the request (HTTP ${r.status}): use the site's final address (https, with or without www).`,
|
|
14903
|
+
r.status,
|
|
14904
|
+
"redirect"
|
|
14905
|
+
);
|
|
14815
14906
|
const msg = typeof body === "object" && body?.message ? body.message.replace(/<[^>]+>/g, "") : `HTTP ${r.status}`;
|
|
14816
14907
|
const code = typeof body === "object" ? body?.code : void 0;
|
|
14817
14908
|
if (r.status === 409 && code === "wp_not_connected")
|
|
@@ -16377,6 +16468,8 @@ var pageTools = [
|
|
|
16377
16468
|
meta: meta4
|
|
16378
16469
|
}
|
|
16379
16470
|
});
|
|
16471
|
+
if (!created || typeof created.id !== "number")
|
|
16472
|
+
throw new Error("WordPress didn't create the page (no id came back); nothing was saved.");
|
|
16380
16473
|
let menu = null;
|
|
16381
16474
|
if (typeof a.menu_parent === "string") {
|
|
16382
16475
|
menu = await menuSet(site, {
|
|
@@ -16547,6 +16640,8 @@ var pageTools = [
|
|
|
16547
16640
|
}
|
|
16548
16641
|
}
|
|
16549
16642
|
});
|
|
16643
|
+
if (!created || typeof created.id !== "number")
|
|
16644
|
+
throw new Error("WordPress didn't create the page (no id came back); nothing was saved.");
|
|
16550
16645
|
return {
|
|
16551
16646
|
created: true,
|
|
16552
16647
|
id: created.id,
|
|
@@ -16791,6 +16886,103 @@ var sectionTools = [
|
|
|
16791
16886
|
}
|
|
16792
16887
|
})
|
|
16793
16888
|
];
|
|
16889
|
+
var CHAT_FILE = {
|
|
16890
|
+
type: "object",
|
|
16891
|
+
description: "The picture from this chat (ChatGPT passes it here).",
|
|
16892
|
+
properties: {
|
|
16893
|
+
download_url: { type: "string" },
|
|
16894
|
+
file_id: { type: "string" },
|
|
16895
|
+
mime_type: { type: "string" },
|
|
16896
|
+
file_name: { type: "string" }
|
|
16897
|
+
},
|
|
16898
|
+
required: ["download_url", "file_id"]
|
|
16899
|
+
};
|
|
16900
|
+
var mediaTools = [
|
|
16901
|
+
tool({
|
|
16902
|
+
name: "wp_media_upload",
|
|
16903
|
+
description: "Put a picture in the WordPress site's media library: a picture from this chat (file) or a public https link to an image (url). JPEG, PNG, WebP, GIF or AVIF, up to 15 MB; checked as a real image. Give alt text that says what the picture shows. Returns its id and source_url; then use the url in a section (wp_section_update edits.images, or slots image) or as a page's featured_media. Doesn't change any page by itself.",
|
|
16904
|
+
parameters: {
|
|
16905
|
+
type: "object",
|
|
16906
|
+
properties: {
|
|
16907
|
+
file: CHAT_FILE,
|
|
16908
|
+
url: str2("Public https link to the image file."),
|
|
16909
|
+
filename: str2("File name to save it as (default from the link), e.g. maggie-hero.webp."),
|
|
16910
|
+
alt: str2("Alt text: what the picture shows, in one short sentence."),
|
|
16911
|
+
title: str2("Media title (optional)."),
|
|
16912
|
+
caption: str2("Caption (optional).")
|
|
16913
|
+
}
|
|
16914
|
+
},
|
|
16915
|
+
group: "site",
|
|
16916
|
+
need: WRITE,
|
|
16917
|
+
risk: "W2",
|
|
16918
|
+
gates: [...GATES],
|
|
16919
|
+
fileParams: ["file"],
|
|
16920
|
+
confirm: (a, lang) => {
|
|
16921
|
+
const name = a.file?.file_name ?? (typeof a.url === "string" ? a.url.split("/").pop() : "");
|
|
16922
|
+
return say(
|
|
16923
|
+
lang,
|
|
16924
|
+
`Upload the picture ${q2(name, "")} to your website's media library.`,
|
|
16925
|
+
`\u628A\u5716\u7247 ${q2(name, "")} \u4E0A\u50B3\u5230\u7DB2\u7AD9\u7684\u5A92\u9AD4\u5EAB\u3002`
|
|
16926
|
+
);
|
|
16927
|
+
},
|
|
16928
|
+
run: async (ctx, a) => {
|
|
16929
|
+
const site = siteOf(ctx);
|
|
16930
|
+
if (!site.uploadMedia) throw new Error("This connection can't upload pictures.");
|
|
16931
|
+
const chat = a.file;
|
|
16932
|
+
const url = String(chat?.download_url ?? a.url ?? "").trim();
|
|
16933
|
+
if (!url) throw new Error("Give the picture: a file from this chat, or a public https link (url).");
|
|
16934
|
+
const out = await site.uploadMedia({
|
|
16935
|
+
url,
|
|
16936
|
+
filename: a.filename || chat?.file_name || void 0,
|
|
16937
|
+
alt: a.alt || void 0,
|
|
16938
|
+
title: a.title || void 0,
|
|
16939
|
+
caption: a.caption || void 0
|
|
16940
|
+
});
|
|
16941
|
+
return {
|
|
16942
|
+
uploaded: true,
|
|
16943
|
+
...out,
|
|
16944
|
+
next: "Use source_url in wp_section_update (edits.images from \u2192 to, or slots image), or set it as the page's featured_media with wp_page_update."
|
|
16945
|
+
};
|
|
16946
|
+
}
|
|
16947
|
+
}),
|
|
16948
|
+
tool({
|
|
16949
|
+
name: "wp_media_list",
|
|
16950
|
+
description: "Pictures already in the WordPress media library (newest first), with id, link, alt text and size; search by words in the title or file name.",
|
|
16951
|
+
parameters: {
|
|
16952
|
+
type: "object",
|
|
16953
|
+
properties: {
|
|
16954
|
+
search: str2("Words in the title or file name."),
|
|
16955
|
+
limit: int2("How many (default 30, at most 100).")
|
|
16956
|
+
}
|
|
16957
|
+
},
|
|
16958
|
+
group: "site",
|
|
16959
|
+
need: READ,
|
|
16960
|
+
risk: "R0",
|
|
16961
|
+
gates: ["WP"],
|
|
16962
|
+
run: async (ctx, a) => {
|
|
16963
|
+
const rows = await call(siteOf(ctx), {
|
|
16964
|
+
path: "/wp/v2/media",
|
|
16965
|
+
query: {
|
|
16966
|
+
search: a.search || void 0,
|
|
16967
|
+
per_page: Math.min(100, Math.max(1, Number(a.limit) || 30)),
|
|
16968
|
+
media_type: "image",
|
|
16969
|
+
_fields: "id,source_url,alt_text,mime_type,date,title,media_details"
|
|
16970
|
+
}
|
|
16971
|
+
});
|
|
16972
|
+
return {
|
|
16973
|
+
count: rows.length,
|
|
16974
|
+
items: rows.map((m) => ({
|
|
16975
|
+
id: m.id,
|
|
16976
|
+
url: m.source_url,
|
|
16977
|
+
alt: m.alt_text,
|
|
16978
|
+
type: m.mime_type,
|
|
16979
|
+
size: m.media_details?.width ? `${m.media_details.width}\xD7${m.media_details.height}` : null,
|
|
16980
|
+
date: m.date
|
|
16981
|
+
}))
|
|
16982
|
+
};
|
|
16983
|
+
}
|
|
16984
|
+
})
|
|
16985
|
+
];
|
|
16794
16986
|
var seoFields = {
|
|
16795
16987
|
title: str2("SEO title (\u224860 characters; the site name is added by the theme)."),
|
|
16796
16988
|
description: str2("Meta description (\u2248150-160 characters)."),
|
|
@@ -17046,7 +17238,14 @@ var elementTools = [
|
|
|
17046
17238
|
}
|
|
17047
17239
|
})
|
|
17048
17240
|
];
|
|
17049
|
-
var wordpressTools = [
|
|
17241
|
+
var wordpressTools = [
|
|
17242
|
+
...siteTools,
|
|
17243
|
+
...pageTools,
|
|
17244
|
+
...sectionTools,
|
|
17245
|
+
...mediaTools,
|
|
17246
|
+
...seoTools,
|
|
17247
|
+
...elementTools
|
|
17248
|
+
];
|
|
17050
17249
|
|
|
17051
17250
|
// ../../packages/tools/src/index.ts
|
|
17052
17251
|
var tools = [
|
|
@@ -20456,7 +20655,7 @@ var program = new Command();
|
|
|
20456
20655
|
// package.json
|
|
20457
20656
|
var package_default = {
|
|
20458
20657
|
name: "@cmo-so/cli",
|
|
20459
|
-
version: "0.12.
|
|
20658
|
+
version: "0.12.2",
|
|
20460
20659
|
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.",
|
|
20461
20660
|
type: "module",
|
|
20462
20661
|
license: "MIT",
|
|
@@ -31570,6 +31769,32 @@ ${table(r.issues, ["severity", "code", "path", "detail"])}`;
|
|
|
31570
31769
|
fonts: o.font ? Object.fromEntries(pairs(o.font)) : void 0
|
|
31571
31770
|
})
|
|
31572
31771
|
);
|
|
31772
|
+
const media = wp.command("media").description("pictures in the media library");
|
|
31773
|
+
media.command("ls", { isDefault: true }).option("--search <words>").option("-n, --limit <n>", "how many", "30").action(
|
|
31774
|
+
(o, c) => run(g(c), "wp_media_list", { search: o.search, limit: Number(o.limit) })
|
|
31775
|
+
);
|
|
31776
|
+
media.command("upload <fileOrUrl>").description("upload a picture (a local file or a public https link): JPEG, PNG, WebP, GIF or AVIF").option("--alt <text>", "what the picture shows").option("--filename <name>", "save it as").option("--title <title>").action(async (src, o, c) => {
|
|
31777
|
+
const opts = g(c);
|
|
31778
|
+
if (/^https?:\/\//i.test(src))
|
|
31779
|
+
return run(opts, "wp_media_upload", { url: src, alt: o.alt, filename: o.filename, title: o.title });
|
|
31780
|
+
const data = new Uint8Array(
|
|
31781
|
+
await readFile2(src).catch(() => {
|
|
31782
|
+
throw new CliError(`Can't read ${src}.`);
|
|
31783
|
+
})
|
|
31784
|
+
);
|
|
31785
|
+
const site = await siteFor(opts);
|
|
31786
|
+
if (!site.uploadMedia) throw new CliError("This connection can't upload pictures.");
|
|
31787
|
+
const preview = `Upload ${src.split("/").pop()} (${Math.round(data.length / 1024)} KB) to the media library of ${site.url}.`;
|
|
31788
|
+
if (opts.dryRun) {
|
|
31789
|
+
say2(`Would: ${preview}`);
|
|
31790
|
+
return;
|
|
31791
|
+
}
|
|
31792
|
+
if (!opts.yes && !await ask(preview)) throw new CliError("Nothing changed.", 1);
|
|
31793
|
+
print(
|
|
31794
|
+
await site.uploadMedia({ data, filename: o.filename ?? src.split("/").pop(), alt: o.alt, title: o.title }),
|
|
31795
|
+
opts.json
|
|
31796
|
+
);
|
|
31797
|
+
});
|
|
31573
31798
|
wp.command("pattern <pattern>").description("change a synced pattern every page shares (contact, share, quote or id)").option("--replace <find=replace...>").option("--link <from=to...>").action(
|
|
31574
31799
|
(p, o, c) => run(g(c), "wp_pattern_update", {
|
|
31575
31800
|
pattern: p,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|