@cmo-so/cli 0.9.0 → 0.9.1
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 +86 -16
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -8778,11 +8778,11 @@ var confirmCopy = {
|
|
|
8778
8778
|
},
|
|
8779
8779
|
social_pictures_attach: {
|
|
8780
8780
|
en: (a) => {
|
|
8781
|
-
const n = Array.isArray(a.urls) ? a.urls.length : 0;
|
|
8781
|
+
const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
|
|
8782
8782
|
return `${a.append ? "Add" : "Put"} your ${n} picture${n === 1 ? "" : "s"} ${a.append ? "after the pictures on" : "on"} post #${a.id}${a.append ? "" : ", in place of its picture"} (free). On LinkedIn a set goes out as a swipeable PDF carousel.`;
|
|
8783
8783
|
},
|
|
8784
8784
|
"zh-Hant": (a) => {
|
|
8785
|
-
const n = Array.isArray(a.urls) ? a.urls.length : 0;
|
|
8785
|
+
const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
|
|
8786
8786
|
return `${a.append ? `\u5728\u8CBC\u6587 #${a.id} \u73FE\u6709\u5716\u7247\u5F8C\u52A0\u4E0A` : `\u628A\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u63DB\u6210`}\u4F60\u7684 ${n} \u5F35\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002\u5728 LinkedIn \u4E0A\uFF0C\u4E00\u7D44\u5716\u7247\u6703\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA\u3002`;
|
|
8787
8787
|
}
|
|
8788
8788
|
},
|
|
@@ -11553,6 +11553,17 @@ var channelsParam = {
|
|
|
11553
11553
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
11554
11554
|
description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
|
|
11555
11555
|
};
|
|
11556
|
+
var FILE_SCHEMA = {
|
|
11557
|
+
type: "object",
|
|
11558
|
+
properties: {
|
|
11559
|
+
download_url: { type: "string" },
|
|
11560
|
+
file_id: { type: "string" },
|
|
11561
|
+
mime_type: { type: "string" },
|
|
11562
|
+
file_name: { type: "string" }
|
|
11563
|
+
},
|
|
11564
|
+
required: ["download_url", "file_id"]
|
|
11565
|
+
};
|
|
11566
|
+
var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
|
|
11556
11567
|
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11557
11568
|
var channelsOf = (value) => {
|
|
11558
11569
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -11922,27 +11933,35 @@ var socialTools = [
|
|
|
11922
11933
|
}),
|
|
11923
11934
|
tool({
|
|
11924
11935
|
name: "social_pictures_attach",
|
|
11925
|
-
description: "Put the user's own pictures on a post, in order: public https links to image files (JPEG, PNG or WebP
|
|
11936
|
+
description: "Put the user's own pictures on a post, in order: pictures from this chat (files: ones the user added or that were made here) or public https links to image files (urls), JPEG, PNG or WebP, for example carousel slides. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. If you can pass neither, give the user social_upload_link. Preview first.",
|
|
11926
11937
|
parameters: {
|
|
11927
11938
|
type: "object",
|
|
11928
11939
|
properties: {
|
|
11929
11940
|
id: { type: "integer", description: "The post (from social_list), not gone out yet" },
|
|
11941
|
+
files: {
|
|
11942
|
+
type: "array",
|
|
11943
|
+
items: FILE_SCHEMA,
|
|
11944
|
+
description: "Pictures from this chat, in the order they should appear"
|
|
11945
|
+
},
|
|
11930
11946
|
urls: {
|
|
11931
11947
|
type: "array",
|
|
11932
11948
|
items: { type: "string" },
|
|
11933
|
-
description: "Public https links to the image files, in the
|
|
11949
|
+
description: "Public https links to the image files, in order (after the files, when both are given)"
|
|
11934
11950
|
},
|
|
11935
11951
|
append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
|
|
11936
11952
|
},
|
|
11937
|
-
required: ["id"
|
|
11953
|
+
required: ["id"]
|
|
11938
11954
|
},
|
|
11955
|
+
fileParams: ["files"],
|
|
11939
11956
|
group: "social",
|
|
11940
11957
|
need: write5,
|
|
11941
11958
|
risk: "W1",
|
|
11942
11959
|
gates: ["VER"],
|
|
11943
|
-
run: async (ctx, { id, urls, append }) => {
|
|
11944
|
-
const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(
|
|
11945
|
-
|
|
11960
|
+
run: async (ctx, { id, urls, files, append }) => {
|
|
11961
|
+
const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
|
|
11962
|
+
Boolean
|
|
11963
|
+
);
|
|
11964
|
+
if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
|
|
11946
11965
|
if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
|
|
11947
11966
|
if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
|
|
11948
11967
|
const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
|
|
@@ -11955,30 +11974,57 @@ var socialTools = [
|
|
|
11955
11974
|
}),
|
|
11956
11975
|
tool({
|
|
11957
11976
|
name: "social_carousel_pdf_attach",
|
|
11958
|
-
description: "A LinkedIn post goes out as this PDF: a swipeable document carousel (a public https link to
|
|
11977
|
+
description: "A LinkedIn post goes out as this PDF: a swipeable document carousel. The PDF from this chat (file: one the user added or that was made here) or a public https link to it (url); up to 300 pages and 20 MB, no password. Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
|
|
11959
11978
|
parameters: {
|
|
11960
11979
|
type: "object",
|
|
11961
11980
|
properties: {
|
|
11962
11981
|
id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
|
|
11982
|
+
file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
|
|
11963
11983
|
url: { type: "string", description: "Public https link to the PDF file" },
|
|
11964
11984
|
title: {
|
|
11965
11985
|
type: "string",
|
|
11966
11986
|
description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
|
|
11967
11987
|
}
|
|
11968
11988
|
},
|
|
11969
|
-
required: ["id"
|
|
11989
|
+
required: ["id"]
|
|
11970
11990
|
},
|
|
11991
|
+
fileParams: ["file"],
|
|
11971
11992
|
group: "social",
|
|
11972
11993
|
need: write5,
|
|
11973
11994
|
risk: "W1",
|
|
11974
11995
|
gates: ["VER", "LI"],
|
|
11975
|
-
run: async (ctx, { id, url, title }) => {
|
|
11976
|
-
const link = String(url ?? "").trim();
|
|
11977
|
-
if (!link.startsWith("https://"))
|
|
11996
|
+
run: async (ctx, { id, url, file, title }) => {
|
|
11997
|
+
const link = fileLinks(file)[0] ?? String(url ?? "").trim();
|
|
11998
|
+
if (!link.startsWith("https://"))
|
|
11999
|
+
throw new Error("Give the PDF (a file from this chat, or a public https link).");
|
|
11978
12000
|
const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
|
|
11979
12001
|
return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
|
|
11980
12002
|
}
|
|
11981
12003
|
}),
|
|
12004
|
+
tool({
|
|
12005
|
+
name: "social_upload_link",
|
|
12006
|
+
description: "A link that opens one post's picture in the CMO.so app, where the user uploads their own photos (up to 20, a carousel on LinkedIn) or, on a computer, a PDF carousel themselves. Use it when you can't pass the files (this chat can't send files to tools, or they are on the user's device). Give the link; nothing changes until they upload.",
|
|
12007
|
+
parameters: {
|
|
12008
|
+
type: "object",
|
|
12009
|
+
properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
|
|
12010
|
+
required: ["id"]
|
|
12011
|
+
},
|
|
12012
|
+
group: "social",
|
|
12013
|
+
need: read5,
|
|
12014
|
+
risk: "R0",
|
|
12015
|
+
run: async (ctx, { id }) => {
|
|
12016
|
+
const post = await ws(ctx).socialPost(Number(id));
|
|
12017
|
+
if (["published", "publishing", "cancelled"].includes(String(post.status)))
|
|
12018
|
+
throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
|
|
12019
|
+
const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
|
|
12020
|
+
return {
|
|
12021
|
+
id: post.id,
|
|
12022
|
+
channel: post.provider ?? null,
|
|
12023
|
+
url: `https://app.cmo.so/social?picture=${post.id}${site}`,
|
|
12024
|
+
how: post.provider === "linkedin" ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
|
|
12025
|
+
};
|
|
12026
|
+
}
|
|
12027
|
+
}),
|
|
11982
12028
|
tool({
|
|
11983
12029
|
name: "social_carousel_set",
|
|
11984
12030
|
description: "How a LinkedIn post with a set of pictures goes out: format pdf (a swipeable PDF carousel made from the pictures, the default) or images (several pictures); and the carousel's title. Preview first.",
|
|
@@ -16499,7 +16545,7 @@ var program = new Command();
|
|
|
16499
16545
|
// package.json
|
|
16500
16546
|
var package_default = {
|
|
16501
16547
|
name: "@cmo-so/cli",
|
|
16502
|
-
version: "0.9.
|
|
16548
|
+
version: "0.9.1",
|
|
16503
16549
|
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.",
|
|
16504
16550
|
type: "module",
|
|
16505
16551
|
license: "MIT",
|
|
@@ -26688,6 +26734,30 @@ function description(t) {
|
|
|
26688
26734
|
].filter(Boolean);
|
|
26689
26735
|
return `${t.description}${notes.length ? ` (${notes.join("; ")}.)` : ""} Changes data: call without confirm for a preview, then with confirm: true and the confirmation after the user agrees.`;
|
|
26690
26736
|
}
|
|
26737
|
+
function fileMeta(t) {
|
|
26738
|
+
return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
|
|
26739
|
+
}
|
|
26740
|
+
function withFreshFiles(t, signed, now) {
|
|
26741
|
+
if (!t.fileParams?.length) return signed;
|
|
26742
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
26743
|
+
const collect = (v) => {
|
|
26744
|
+
for (const f of Array.isArray(v) ? v : [v])
|
|
26745
|
+
if (f && typeof f === "object" && typeof f.file_id === "string")
|
|
26746
|
+
fresh.set(f.file_id, f);
|
|
26747
|
+
};
|
|
26748
|
+
for (const p of t.fileParams) collect(now[p]);
|
|
26749
|
+
const swap = (v) => {
|
|
26750
|
+
if (Array.isArray(v)) return v.map(swap);
|
|
26751
|
+
if (v && typeof v === "object" && typeof v.file_id === "string") {
|
|
26752
|
+
const f = fresh.get(v.file_id);
|
|
26753
|
+
return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
|
|
26754
|
+
}
|
|
26755
|
+
return v;
|
|
26756
|
+
};
|
|
26757
|
+
const out = { ...signed };
|
|
26758
|
+
for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
|
|
26759
|
+
return out;
|
|
26760
|
+
}
|
|
26691
26761
|
function toolArgs(args) {
|
|
26692
26762
|
const out = {};
|
|
26693
26763
|
for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
|
|
@@ -26817,7 +26887,7 @@ function createMcpServer(deps) {
|
|
|
26817
26887
|
description: description(t),
|
|
26818
26888
|
inputSchema: inputSchema(t),
|
|
26819
26889
|
annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
|
|
26820
|
-
...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
|
|
26890
|
+
...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
|
|
26821
26891
|
}))
|
|
26822
26892
|
};
|
|
26823
26893
|
});
|
|
@@ -26853,7 +26923,7 @@ function createMcpServer(deps) {
|
|
|
26853
26923
|
return fail(
|
|
26854
26924
|
access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
|
|
26855
26925
|
);
|
|
26856
|
-
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
26926
|
+
const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
|
|
26857
26927
|
const ctx = { api: api2, domain, client: "mcp" };
|
|
26858
26928
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
26859
26929
|
let standing = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|