@cmo-so/cli 0.9.3 → 0.10.0
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 +433 -95
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7921,8 +7921,9 @@ function domainApi(api2, domain) {
|
|
|
7921
7921
|
* https links to pictures. Send a few photos per call (16 MB request limit), `append` after the first.
|
|
7922
7922
|
* On LinkedIn a set goes out as a swipeable PDF carousel unless setSocialCarousel says images. */
|
|
7923
7923
|
setSocialPictures: (id, pictures) => post(`${base}/social/${id}/pictures`, pictures),
|
|
7924
|
-
/** A
|
|
7925
|
-
*
|
|
7924
|
+
/** A PDF carousel (base64 under ~11 MB, or a public https link; up to 300 pages): its first 20 pages become
|
|
7925
|
+
* the post's pictures; a LinkedIn post (or the idea's LinkedIn post) goes out as the PDF itself. LinkedIn,
|
|
7926
|
+
* Instagram, Facebook and TikTok posts. */
|
|
7926
7927
|
attachSocialCarouselPdf: (id, pdf) => post(`${base}/social/${id}/carousel/pdf`, pdf),
|
|
7927
7928
|
/** How a LinkedIn post with a set goes out: "pdf" (swipeable carousel, the default) or "images"; its title. */
|
|
7928
7929
|
setSocialCarousel: (id, changes) => api2.request(`${base}/social/${id}/carousel`, {
|
|
@@ -8072,6 +8073,26 @@ function domainApi(api2, domain) {
|
|
|
8072
8073
|
/** A post on one video channel (YouTube by default, or TikTok) from a ready video, waiting in Review; Maggie
|
|
8073
8074
|
* writes the details (YouTube) or the caption (TikTok) unless write is false. */
|
|
8074
8075
|
postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
|
|
8076
|
+
/** A TikTok photo post (a swipeable set; TikTok adds music) from this post's pictures, waiting in Review;
|
|
8077
|
+
* Maggie writes the caption unless `caption` is given or write is false. Beta, like TikTok. */
|
|
8078
|
+
tiktokPhotoPost: (id, opts = {}) => post(`${base}/social/${id}/tiktok-photo`, opts),
|
|
8079
|
+
/** Ad videos (Beta): this website's, with the voices and lengths to choose from. */
|
|
8080
|
+
adVideos: () => api2.request(
|
|
8081
|
+
`${base}/social/ad-videos`
|
|
8082
|
+
),
|
|
8083
|
+
adVideo: (adId) => api2.request(`${base}/social/ad-videos/${adId}`),
|
|
8084
|
+
/** A storyboard and its cost (one AI generation; nothing billed by the second yet). */
|
|
8085
|
+
planAdVideo: (opts) => post(`${base}/social/ad-videos`, opts),
|
|
8086
|
+
/** Change the storyboard: only what a change touches is made again (`redo`), with the new cost. */
|
|
8087
|
+
changeAdVideo: (adId, changes) => api2.request(`${base}/social/ad-videos/${adId}`, {
|
|
8088
|
+
method: "PATCH",
|
|
8089
|
+
headers: { "Content-Type": "application/json" },
|
|
8090
|
+
body: JSON.stringify(changes)
|
|
8091
|
+
}),
|
|
8092
|
+
/** Make it (billed): send back the estimate the person agreed to. */
|
|
8093
|
+
makeAdVideo: (adId, estimateUsd) => post(`${base}/social/ad-videos/${adId}/make`, { confirm: true, estimate_usd: estimateUsd }),
|
|
8094
|
+
/** Stop one being made (what's made is kept), or remove one. */
|
|
8095
|
+
stopAdVideo: (adId) => api2.request(`${base}/social/ad-videos/${adId}`, { method: "DELETE" }),
|
|
8075
8096
|
/** Make a LinkedIn, Facebook or Instagram post a video post: it goes out as this video (its picture is dropped). */
|
|
8076
8097
|
attachVideo: (id, videoId) => post(`${base}/social/${id}/video`, { video_id: videoId }),
|
|
8077
8098
|
/** Take the video off a post: it's a text post again. */
|
|
@@ -8479,6 +8500,12 @@ var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
|
8479
8500
|
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
8480
8501
|
(c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
|
|
8481
8502
|
);
|
|
8503
|
+
var videoFrom = (a, lang = "en") => {
|
|
8504
|
+
const name = a.file?.file_name;
|
|
8505
|
+
if (a.file) return name ? q(String(name)) : lang === "en" ? "your video" : "\u4F60\u7684\u5F71\u7247";
|
|
8506
|
+
if (a.video_id) return lang === "en" ? `video #${a.video_id}` : `\u5F71\u7247 #${a.video_id}`;
|
|
8507
|
+
return String(a.url ?? "");
|
|
8508
|
+
};
|
|
8482
8509
|
var confirmCopy = {
|
|
8483
8510
|
// --------------------------------------------------------- account / setup
|
|
8484
8511
|
website_add: {
|
|
@@ -8753,8 +8780,8 @@ var confirmCopy = {
|
|
|
8753
8780
|
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8754
8781
|
},
|
|
8755
8782
|
social_video_attach: {
|
|
8756
|
-
en: (a) => `Make post #${a.id} a video post with ${a
|
|
8757
|
-
"zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u6539\u6210\u5F71\u7247\u8CBC\u6587\uFF0C\u7528 ${a
|
|
8783
|
+
en: (a) => `Make post #${a.id} a video post with ${videoFrom(a)} (its picture is removed).`,
|
|
8784
|
+
"zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u6539\u6210\u5F71\u7247\u8CBC\u6587\uFF0C\u7528 ${videoFrom(a, "zh-Hant")}\uFF08\u5716\u7247\u6703\u79FB\u9664\uFF09\u3002`
|
|
8758
8785
|
},
|
|
8759
8786
|
social_video_detach: {
|
|
8760
8787
|
en: (a) => `Take the video off post #${a.id} (it's a text post again).`,
|
|
@@ -8767,11 +8794,11 @@ var confirmCopy = {
|
|
|
8767
8794
|
social_video_add: {
|
|
8768
8795
|
en: (a) => {
|
|
8769
8796
|
const names = videoChannels(a).join(" and ");
|
|
8770
|
-
return `Make a ${names} post from ${a
|
|
8797
|
+
return `Make a ${names} post from ${videoFrom(a)}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
|
|
8771
8798
|
},
|
|
8772
8799
|
"zh-Hant": (a) => {
|
|
8773
8800
|
const names = videoChannels(a).join(" \u548C ");
|
|
8774
|
-
return `\u7528 ${a
|
|
8801
|
+
return `\u7528 ${videoFrom(a, "zh-Hant")} \u505A ${names} \u5F71\u7247\u8CBC\u6587${a.write === false ? "" : "\uFF08Maggie \u4F86\u5BEB\uFF1A\u6BCF\u500B\u6E20\u9053\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09"}\uFF0C\u7B49\u4F60\u78BA\u8A8D\u5F8C\u624D\u767C\u3002`;
|
|
8775
8802
|
}
|
|
8776
8803
|
},
|
|
8777
8804
|
social_tiktok_set: {
|
|
@@ -8787,8 +8814,8 @@ var confirmCopy = {
|
|
|
8787
8814
|
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8788
8815
|
},
|
|
8789
8816
|
social_image_new: {
|
|
8790
|
-
en: (a) => `Make a new picture for post #${a.id}${a.instruction ? `: ${String(a.instruction).slice(0, 300)}` : ""} (1 AI generation, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.`,
|
|
8791
|
-
"zh-Hant": (a) => `\u70BA\u8CBC\u6587 #${a.id} \u751F\u6210\u65B0\u5716\u7247${a.instruction ? `\uFF1A${String(a.instruction).slice(0, 300)}` : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002`
|
|
8817
|
+
en: (a) => Number(a.count) >= 5 ? `Make a text carousel of ${a.count} slides for post #${a.id} (words on your brand's colours; free, no AI picture, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.` : `Make a new picture for post #${a.id}${a.instruction ? `: ${String(a.instruction).slice(0, 300)}` : ""} (1 AI generation, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.`,
|
|
8818
|
+
"zh-Hant": (a) => Number(a.count) >= 5 ? `\u70BA\u8CBC\u6587 #${a.id} \u505A ${a.count} \u9801\u6587\u5B57\u8F2A\u64AD\uFF08\u54C1\u724C\u8272\u4E0A\u7684\u6587\u5B57\uFF1B\u514D\u8CBB\uFF0C\u4E0D\u7528 AI \u751F\u5716\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002` : `\u70BA\u8CBC\u6587 #${a.id} \u751F\u6210\u65B0\u5716\u7247${a.instruction ? `\uFF1A${String(a.instruction).slice(0, 300)}` : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002`
|
|
8792
8819
|
},
|
|
8793
8820
|
social_image_stop: {
|
|
8794
8821
|
en: () => "Stop the picture being made; the post keeps the one it had.",
|
|
@@ -8805,8 +8832,28 @@ var confirmCopy = {
|
|
|
8805
8832
|
}
|
|
8806
8833
|
},
|
|
8807
8834
|
social_carousel_pdf_attach: {
|
|
8808
|
-
en: (a) => `
|
|
8809
|
-
"zh-Hant": (a) =>
|
|
8835
|
+
en: (a) => `Your PDF becomes post #${a.id}'s carousel${a.title ? `, titled ${q(a.title)}` : ""}: its first pages become the post's pictures, and on LinkedIn it goes out as the PDF itself (free).`,
|
|
8836
|
+
"zh-Hant": (a) => `\u4F60\u7684 PDF \u6210\u70BA\u8CBC\u6587 #${a.id} \u7684\u8F2A\u64AD${a.title ? `\uFF0C\u6A19\u984C ${q(a.title)}` : ""}\uFF1A\u524D\u5E7E\u9801\u6703\u6210\u70BA\u8CBC\u6587\u7684\u5716\u7247\uFF0C\u5728 LinkedIn \u4E0A\u76F4\u63A5\u4EE5 PDF \u767C\u51FA\uFF08\u514D\u8CBB\uFF09\u3002`
|
|
8837
|
+
},
|
|
8838
|
+
social_tiktok_photo: {
|
|
8839
|
+
en: (a) => `Make a TikTok photo post from post #${a.id}'s pictures${a.caption ? " with your caption" : " (Maggie writes the caption: 1 AI generation)"}; it waits for your OK.`,
|
|
8840
|
+
"zh-Hant": (a) => `\u7528\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u505A\u4E00\u5247 TikTok \u5716\u7247\u8CBC\u6587${a.caption ? "\uFF0C\u7528\u4F60\u7684\u6587\u6848" : "\uFF08Maggie \u5BEB\u6587\u6848\uFF1A\u7528 1 \u6B21 AI \u751F\u6210\uFF09"}\uFF0C\u7B49\u4F60\u78BA\u8A8D\u5F8C\u624D\u767C\u3002`
|
|
8841
|
+
},
|
|
8842
|
+
ad_video_plan: {
|
|
8843
|
+
en: (a) => `Plan a ${a.seconds ?? 20}-second ${a.aspect === "16:9" ? "landscape" : "vertical"} ad video${a.item_id ? ` from post #${a.item_id}` : ""}${a.reference_url ? ", following the YouTube ad you gave" : ""} (1 AI generation; nothing is billed by the second yet). You'll see the storyboard and the cost first.`,
|
|
8844
|
+
"zh-Hant": (a) => `\u898F\u5283\u4E00\u652F ${a.seconds ?? 20} \u79D2${a.aspect === "16:9" ? "\u6A6B\u5411" : "\u76F4\u5411"}\u5EE3\u544A\u5F71\u7247${a.item_id ? `\uFF08\u6839\u64DA\u8CBC\u6587 #${a.item_id}\uFF09` : ""}${a.reference_url ? "\uFF0C\u53C3\u8003\u4F60\u7D66\u7684 YouTube \u5EE3\u544A\u7BC0\u594F" : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u9084\u4E0D\u6703\u6309\u79D2\u8A08\u8CBB\uFF09\u3002\u6703\u5148\u7D66\u4F60\u770B\u5206\u93E1\u548C\u8CBB\u7528\u3002`
|
|
8845
|
+
},
|
|
8846
|
+
ad_video_change: {
|
|
8847
|
+
en: (a) => `Change ad video #${a.id}'s storyboard: ${keys(a).filter((k) => k !== "id").join(", ") || "nothing"}.`,
|
|
8848
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u5EE3\u544A\u5F71\u7247 #${a.id} \u7684\u5206\u93E1\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001") || "\u7121"}\u3002`
|
|
8849
|
+
},
|
|
8850
|
+
ad_video_make: {
|
|
8851
|
+
en: (a) => `Make ad video #${a.id} now. This costs about $${Number(a.estimate_usd ?? 0).toFixed(2)} (pictures, moving clips, voice and music), in the background.`,
|
|
8852
|
+
"zh-Hant": (a) => `\u73FE\u5728\u88FD\u4F5C\u5EE3\u544A\u5F71\u7247 #${a.id}\uFF0C\u8CBB\u7528\u7D04 $${Number(a.estimate_usd ?? 0).toFixed(2)}\uFF08\u5716\u7247\u3001\u52D5\u614B\u7247\u6BB5\u3001\u914D\u97F3\u548C\u914D\u6A02\uFF09\uFF0C\u5728\u80CC\u666F\u9032\u884C\u3002`
|
|
8853
|
+
},
|
|
8854
|
+
ad_video_stop: {
|
|
8855
|
+
en: (a) => `Stop ad video #${a.id} if it's being made (what's made is kept), or remove it from the list.`,
|
|
8856
|
+
"zh-Hant": (a) => `\u505C\u6B62\u88FD\u4F5C\u5EE3\u544A\u5F71\u7247 #${a.id}\uFF08\u5DF2\u505A\u597D\u7684\u90E8\u5206\u6703\u4FDD\u7559\uFF09\uFF0C\u6216\u628A\u5B83\u5F9E\u6E05\u55AE\u79FB\u9664\u3002`
|
|
8810
8857
|
},
|
|
8811
8858
|
social_carousel_set: {
|
|
8812
8859
|
en: (a) => `LinkedIn post #${a.id} goes out ${a.format === "images" ? "as several pictures" : a.format === "pdf" ? "as a swipeable PDF carousel" : "as it is set"}${a.title ? `, titled ${q(a.title)}` : ""}.`,
|
|
@@ -11566,6 +11613,36 @@ var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
|
11566
11613
|
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
|
|
11567
11614
|
var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
|
|
11568
11615
|
var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
11616
|
+
var SLIDES = [5, 6, 7, 8, 9, 10];
|
|
11617
|
+
var adVideo = (v) => ({
|
|
11618
|
+
id: v.id,
|
|
11619
|
+
status: v.status,
|
|
11620
|
+
step: v.step,
|
|
11621
|
+
aspect: v.aspect,
|
|
11622
|
+
plan: v.plan ? {
|
|
11623
|
+
title: v.plan.title,
|
|
11624
|
+
style: v.plan.style,
|
|
11625
|
+
voice: v.plan.voice,
|
|
11626
|
+
music: v.plan.music,
|
|
11627
|
+
seconds: v.plan.seconds,
|
|
11628
|
+
end_card: v.plan.cta,
|
|
11629
|
+
scenes: v.plan.scenes.map((s) => ({
|
|
11630
|
+
id: s.id,
|
|
11631
|
+
seconds: s.seconds,
|
|
11632
|
+
picture: s.still,
|
|
11633
|
+
motion: s.motion,
|
|
11634
|
+
line: s.line,
|
|
11635
|
+
caption: s.caption,
|
|
11636
|
+
picture_url: s.still_url ?? null
|
|
11637
|
+
}))
|
|
11638
|
+
} : null,
|
|
11639
|
+
estimate: v.estimate,
|
|
11640
|
+
cost_usd: v.cost_usd,
|
|
11641
|
+
video_id: v.video_id,
|
|
11642
|
+
video: v.video ? { status: v.video.status, preview_url: v.video.preview_url, poster_url: v.video.poster_url } : null,
|
|
11643
|
+
error: v.error,
|
|
11644
|
+
next: v.status === "planned" ? `Show the storyboard and the cost (about $${(v.estimate?.usd ?? 0).toFixed(2)}); change it with ad_video_change, make it with ad_video_make when the user agrees to that cost.` : v.status === "running" ? "Being made; check again with ad_video_get in a minute or so." : v.status === "ready" ? "Ready: post it with social_video_add (video_id and channels) once video.status is ready." : v.status === "failed" ? "Failed: say why; ad_video_make again carries on from what was made." : ""
|
|
11645
|
+
});
|
|
11569
11646
|
var channelsParam = {
|
|
11570
11647
|
type: "array",
|
|
11571
11648
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
@@ -11582,6 +11659,11 @@ var FILE_SCHEMA = {
|
|
|
11582
11659
|
required: ["download_url", "file_id"]
|
|
11583
11660
|
};
|
|
11584
11661
|
var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
|
|
11662
|
+
var videoLink = (a) => {
|
|
11663
|
+
const link = fileLinks(a.file)[0] ?? String(a.url ?? "").trim();
|
|
11664
|
+
if (!link.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
|
|
11665
|
+
return link;
|
|
11666
|
+
};
|
|
11585
11667
|
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11586
11668
|
var channelsOf = (value) => {
|
|
11587
11669
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -11780,7 +11862,7 @@ var socialTools = [
|
|
|
11780
11862
|
}),
|
|
11781
11863
|
tool({
|
|
11782
11864
|
name: "social_image_new",
|
|
11783
|
-
description: "Make a new picture for a social post that hasn't gone out, or change its current one (mode: edit, as `instruction` says). Uses 1 AI generation. It is made in the background, on the user's own ChatGPT when their Maggie Bridge has images on, else by CMO.so; it takes up to a couple of minutes and appears on the post by itself (check with social_image_status). One picture is made at a time per website: while another is being made this one waits in line (status queued). It fails with image_busy only when this post already has one being made or in line; pass replace: true only when the user agrees to stop the one being made and make this one now. A post written from an article gets its own picture (from its text and the article's title) and then goes out as that picture. Every new picture carries a short title and line set on it; count: 4 makes a set of 4 pictures that tell one story (still 1 AI generation, drawn as one storyboard and cut); LinkedIn and Facebook post a set together as a collage. text_effect picks the style of those words (an id from social_text_effects); leave it out to let Maggie pick. style picks the picture style (an id from social_look: photo_headline, editorial, duotone, illustrated, bold_type, infographic); leave it out and Maggie takes turns with the brand's 3. bold_type and infographic make no AI picture (big words or a list in the brand's look) and cost nothing.",
|
|
11865
|
+
description: "Make a new picture for a social post that hasn't gone out, or change its current one (mode: edit, as `instruction` says). Uses 1 AI generation. It is made in the background, on the user's own ChatGPT when their Maggie Bridge has images on, else by CMO.so; it takes up to a couple of minutes and appears on the post by itself (check with social_image_status). One picture is made at a time per website: while another is being made this one waits in line (status queued). It fails with image_busy only when this post already has one being made or in line; pass replace: true only when the user agrees to stop the one being made and make this one now. A post written from an article gets its own picture (from its text and the article's title) and then goes out as that picture. Every new picture carries a short title and line set on it; count: 4 makes a set of 4 pictures that tell one story (still 1 AI generation, drawn as one storyboard and cut); LinkedIn and Facebook post a set together as a collage. text_effect picks the style of those words (an id from social_text_effects); leave it out to let Maggie pick. style picks the picture style (an id from social_look: photo_headline, editorial, duotone, illustrated, bold_type, infographic); leave it out and Maggie takes turns with the brand's 3. bold_type and infographic make no AI picture (big words or a list in the brand's look) and cost nothing. count 5-10 makes a text carousel: a cover with the hook, one point a slide and a last slide with the next step, words on the brand's colours (style bold_type or infographic), no AI picture and free; LinkedIn posts it as a swipeable PDF carousel, Instagram and Facebook as a carousel (the first 10), TikTok as a photo post (social_tiktok_photo).",
|
|
11784
11866
|
parameters: {
|
|
11785
11867
|
type: "object",
|
|
11786
11868
|
properties: {
|
|
@@ -11797,7 +11879,7 @@ var socialTools = [
|
|
|
11797
11879
|
},
|
|
11798
11880
|
count: {
|
|
11799
11881
|
type: "integer",
|
|
11800
|
-
description: "1 (default)
|
|
11882
|
+
description: "1 (default); 4: a set of 4 pictures telling one story, a line of words on each; 5-10: a text carousel of that many slides (free)"
|
|
11801
11883
|
},
|
|
11802
11884
|
text_effect: {
|
|
11803
11885
|
type: "string",
|
|
@@ -11819,7 +11901,7 @@ var socialTools = [
|
|
|
11819
11901
|
replace: replace === true,
|
|
11820
11902
|
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
11821
11903
|
...mode === "edit" ? { mode: "edit" } : {},
|
|
11822
|
-
...Number(count) === 4
|
|
11904
|
+
...mode !== "edit" && (Number(count) === 4 || SLIDES.includes(Number(count))) ? { count: Number(count) } : {},
|
|
11823
11905
|
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
11824
11906
|
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
11825
11907
|
});
|
|
@@ -12045,11 +12127,14 @@ var socialTools = [
|
|
|
12045
12127
|
}),
|
|
12046
12128
|
tool({
|
|
12047
12129
|
name: "social_carousel_pdf_attach",
|
|
12048
|
-
description: "A
|
|
12130
|
+
description: "A PDF carousel for a LinkedIn, Instagram, Facebook or TikTok post: 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 become the post's pictures: Instagram and Facebook post them as a carousel (the first 10), TikTok as a photo post. A LinkedIn post (this one, or the same idea's LinkedIn post) goes out as the PDF itself, a swipeable document carousel. Free. Change the pictures later and the LinkedIn carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
|
|
12049
12131
|
parameters: {
|
|
12050
12132
|
type: "object",
|
|
12051
12133
|
properties: {
|
|
12052
|
-
id: {
|
|
12134
|
+
id: {
|
|
12135
|
+
type: "integer",
|
|
12136
|
+
description: "A LinkedIn, Instagram, Facebook or TikTok post (from social_list), not gone out yet"
|
|
12137
|
+
},
|
|
12053
12138
|
file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
|
|
12054
12139
|
url: { type: "string", description: "Public https link to the PDF file" },
|
|
12055
12140
|
title: {
|
|
@@ -12063,7 +12148,7 @@ var socialTools = [
|
|
|
12063
12148
|
group: "social",
|
|
12064
12149
|
need: write5,
|
|
12065
12150
|
risk: "W1",
|
|
12066
|
-
gates: ["VER"
|
|
12151
|
+
gates: ["VER"],
|
|
12067
12152
|
run: async (ctx, { id, url, file, title }) => {
|
|
12068
12153
|
const link = fileLinks(file)[0] ?? String(url ?? "").trim();
|
|
12069
12154
|
if (!link.startsWith("https://"))
|
|
@@ -12092,7 +12177,7 @@ var socialTools = [
|
|
|
12092
12177
|
id: post.id,
|
|
12093
12178
|
channel: post.provider ?? null,
|
|
12094
12179
|
url: `https://app.cmo.so/social?picture=${post.id}${site}`,
|
|
12095
|
-
how:
|
|
12180
|
+
how: ["linkedin", "instagram", "facebook", "tiktok"].includes(String(post.provider)) ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
|
|
12096
12181
|
};
|
|
12097
12182
|
}
|
|
12098
12183
|
}),
|
|
@@ -12790,22 +12875,24 @@ var socialTools = [
|
|
|
12790
12875
|
}),
|
|
12791
12876
|
tool({
|
|
12792
12877
|
name: "social_video_attach",
|
|
12793
|
-
description: "Make a LinkedIn, Facebook or Instagram post that hasn't gone out a video post: it goes out as this video (a DIRECT link to a video file
|
|
12878
|
+
description: "Make a LinkedIn, Facebook or Instagram post that hasn't gone out a video post: it goes out as this video (5-30 seconds) with its text, and its picture is removed (a video post has no picture). The video from this chat (file: one the user added) or a DIRECT https link to a video file (url; never a platform page). If you can pass neither, the user adds the video in the CMO.so app (Social). Not for Google Business (posts can't carry video). Preview first.",
|
|
12794
12879
|
parameters: {
|
|
12795
12880
|
type: "object",
|
|
12796
12881
|
properties: {
|
|
12797
12882
|
id: { type: "integer", description: "The post (from social_list)" },
|
|
12883
|
+
file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
|
|
12798
12884
|
url: { type: "string", description: "Direct https link to the video file" }
|
|
12799
12885
|
},
|
|
12800
|
-
required: ["id"
|
|
12886
|
+
required: ["id"]
|
|
12801
12887
|
},
|
|
12888
|
+
fileParams: ["file"],
|
|
12802
12889
|
group: "social",
|
|
12803
12890
|
need: write5,
|
|
12804
12891
|
risk: "W1",
|
|
12805
12892
|
gates: ["VER"],
|
|
12806
12893
|
run: async (ctx, a) => {
|
|
12807
12894
|
const d = ws(ctx);
|
|
12808
|
-
let v = await d.videoFromLink(
|
|
12895
|
+
let v = await d.videoFromLink(videoLink(a));
|
|
12809
12896
|
const until = Date.now() + 15e4;
|
|
12810
12897
|
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
12811
12898
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -12881,11 +12968,13 @@ var socialTools = [
|
|
|
12881
12968
|
}),
|
|
12882
12969
|
tool({
|
|
12883
12970
|
name: "social_video_add",
|
|
12884
|
-
description: "Make video posts (channels: linkedin, facebook, instagram, youtube, tiktok; default youtube; one post each, never google_business: Google posts can't carry video) from a video: a DIRECT link to a video file (an .mp4 on their website, a public Dropbox link), never a YouTube / TikTok / Instagram page (we never download those
|
|
12971
|
+
description: "Make video posts (channels: linkedin, facebook, instagram, youtube, tiktok; default youtube; one post each, never google_business: Google posts can't carry video) from a video: a video already in CMO.so (video_id, e.g. a ready ad video's), the video from this chat (file: one the user added) or a DIRECT https link to a video file (url: an .mp4 on their website, a public Dropbox link), never a YouTube / TikTok / Instagram page (we never download those). If you can pass neither, tell the user to add the video in the CMO.so app (Social). The video must be 5-30 seconds: give start/end (seconds) to use a part of a longer one. vertical: make a wide video vertical (a Short) cropped at left, middle or right. Maggie writes YouTube's title, description and tags, TikTok's caption, or the LinkedIn / Facebook / Instagram text, from the video and the note (one AI generation per channel) unless write is false. Each post waits in Review: video posts are always approved by the user. YouTube asks whether it's made for kids; TikTok asks who can view it and the other TikTok settings (social_tiktok_set, as the user chooses). is_aigc: TikTok's AI-generated label, only when the user says the video was made with AI. Preview first.",
|
|
12885
12972
|
parameters: {
|
|
12886
12973
|
type: "object",
|
|
12887
12974
|
properties: {
|
|
12975
|
+
file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
|
|
12888
12976
|
url: { type: "string", description: "Direct https link to the video file" },
|
|
12977
|
+
video_id: { type: "integer", description: "A video already in CMO.so (an ad video's video_id)" },
|
|
12889
12978
|
channels: {
|
|
12890
12979
|
type: "array",
|
|
12891
12980
|
items: { type: "string", enum: [...VIDEO_KEYS] },
|
|
@@ -12897,9 +12986,9 @@ var socialTools = [
|
|
|
12897
12986
|
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
12898
12987
|
end: { type: "number", description: "End of the part to use, seconds" },
|
|
12899
12988
|
vertical: { type: "string", enum: ["left", "middle", "right"] }
|
|
12900
|
-
}
|
|
12901
|
-
required: ["url"]
|
|
12989
|
+
}
|
|
12902
12990
|
},
|
|
12991
|
+
fileParams: ["file"],
|
|
12903
12992
|
group: "social",
|
|
12904
12993
|
need: write5,
|
|
12905
12994
|
risk: "W2",
|
|
@@ -12911,7 +13000,7 @@ var socialTools = [
|
|
|
12911
13000
|
...a.end != null ? { end: Number(a.end) } : {},
|
|
12912
13001
|
...a.vertical ? { aspect: "9:16", crop_x: a.vertical === "left" ? 0 : a.vertical === "right" ? 1 : 0.5 } : {}
|
|
12913
13002
|
};
|
|
12914
|
-
let v = await d.
|
|
13003
|
+
let v = a.video_id ? await d.video(Number(a.video_id)) : await d.videoFromLink(videoLink(a), Object.keys(edit).length ? edit : void 0);
|
|
12915
13004
|
const until = Date.now() + 15e4;
|
|
12916
13005
|
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
12917
13006
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -12944,6 +13033,183 @@ var socialTools = [
|
|
|
12944
13033
|
};
|
|
12945
13034
|
}
|
|
12946
13035
|
}),
|
|
13036
|
+
tool({
|
|
13037
|
+
name: "social_tiktok_photo",
|
|
13038
|
+
description: "A TikTok photo post (a swipeable set of pictures; TikTok adds music) from a post's pictures: a set, a text carousel (social_image_new count 5-10) or the pages of a PDF. A new TikTok post that waits in Review like every TikTok post. Maggie writes the caption from the post (one AI generation) unless caption is given. Then TikTok's settings: read tiktok_creator, ask who can view it and the rest, social_tiktok_set exactly as the user says. TikTok is in beta. Preview first.",
|
|
13039
|
+
parameters: {
|
|
13040
|
+
type: "object",
|
|
13041
|
+
properties: {
|
|
13042
|
+
id: { type: "integer", description: "A post with pictures (from social_list), not a video post" },
|
|
13043
|
+
note: { type: "string", description: "What to say in the caption, in the user's words" },
|
|
13044
|
+
caption: { type: "string", description: "The caption itself, when the user wrote it (no AI)" }
|
|
13045
|
+
},
|
|
13046
|
+
required: ["id"]
|
|
13047
|
+
},
|
|
13048
|
+
group: "social",
|
|
13049
|
+
need: write5,
|
|
13050
|
+
risk: "W2",
|
|
13051
|
+
gates: ["VER", "LLM"],
|
|
13052
|
+
run: async (ctx, { id, note, caption }) => {
|
|
13053
|
+
const out = await ws(ctx).tiktokPhotoPost(Number(id), {
|
|
13054
|
+
...note ? { note: String(note).slice(0, 1e3) } : {},
|
|
13055
|
+
...typeof caption === "string" && caption.trim() ? { caption: caption.trim(), write: false } : {}
|
|
13056
|
+
});
|
|
13057
|
+
return {
|
|
13058
|
+
post: item(out),
|
|
13059
|
+
next: "Show the caption; read tiktok_creator and ask who can view it, whether to allow comments and whether it promotes a brand, then social_tiktok_set exactly as they say; tell them the Music Usage Confirmation line. The user approves it."
|
|
13060
|
+
};
|
|
13061
|
+
}
|
|
13062
|
+
}),
|
|
13063
|
+
tool({
|
|
13064
|
+
name: "ad_video_plan",
|
|
13065
|
+
description: "Plan a short ad video (beta) and say what making it costs. Maggie writes a storyboard: 2-5 scenes, each with the first picture's brief (no words in pictures), what moves and one camera move, the narrator's line and the words on screen; an end card with the next step and the logo; a voice and music. One AI generation; nothing is billed by the second yet. brief: what the ad should do, in the user's words, or item_id: a post whose text is the brief. aspect 9:16 (Reels, TikTok, Shorts; default) or 16:9 (LinkedIn, YouTube); seconds 15, 20 (default) or 30; reference_url: a public YouTube ad whose rhythm to follow (Gemini watches it; never downloaded). Show the storyboard and the cost (estimate.items, estimate.usd) before ad_video_make. Preview first.",
|
|
13066
|
+
parameters: {
|
|
13067
|
+
type: "object",
|
|
13068
|
+
properties: {
|
|
13069
|
+
brief: { type: "string", description: "What the ad should do, who it's for, the offer (user's words)" },
|
|
13070
|
+
item_id: { type: "integer", description: "A post (from social_list) to make the ad from" },
|
|
13071
|
+
aspect: { type: "string", enum: ["9:16", "16:9"] },
|
|
13072
|
+
seconds: { type: "integer", description: "15, 20 or 30" },
|
|
13073
|
+
scenes: { type: "integer", description: "2-5 (default 3, or 4 for 30 seconds)" },
|
|
13074
|
+
reference_url: { type: "string", description: "A public YouTube ad to follow the rhythm of" }
|
|
13075
|
+
}
|
|
13076
|
+
},
|
|
13077
|
+
group: "social",
|
|
13078
|
+
need: write5,
|
|
13079
|
+
risk: "W2",
|
|
13080
|
+
gates: ["VER", "LLM"],
|
|
13081
|
+
run: async (ctx, a) => {
|
|
13082
|
+
const out = await ws(ctx).planAdVideo({
|
|
13083
|
+
...a.brief ? { brief: String(a.brief).slice(0, 3e3) } : {},
|
|
13084
|
+
...a.item_id ? { item_id: Number(a.item_id) } : {},
|
|
13085
|
+
...a.aspect ? { aspect: a.aspect } : {},
|
|
13086
|
+
...a.seconds ? { seconds: Number(a.seconds) } : {},
|
|
13087
|
+
...a.scenes ? { scenes: Number(a.scenes) } : {},
|
|
13088
|
+
...a.reference_url ? { reference_url: String(a.reference_url).trim() } : {}
|
|
13089
|
+
});
|
|
13090
|
+
return adVideo(out);
|
|
13091
|
+
}
|
|
13092
|
+
}),
|
|
13093
|
+
tool({
|
|
13094
|
+
name: "ad_video_list",
|
|
13095
|
+
description: "This website's ad videos (beta): each with its status (planned, running and its step, ready, failed), the storyboard's title, its cost so far and, when ready, the video (video_id) to post.",
|
|
13096
|
+
parameters: none,
|
|
13097
|
+
group: "social",
|
|
13098
|
+
need: read5,
|
|
13099
|
+
risk: "R0",
|
|
13100
|
+
run: async (ctx) => {
|
|
13101
|
+
const out = await ws(ctx).adVideos();
|
|
13102
|
+
return {
|
|
13103
|
+
ad_videos: out.ad_videos.map((v) => ({
|
|
13104
|
+
id: v.id,
|
|
13105
|
+
status: v.status,
|
|
13106
|
+
step: v.step,
|
|
13107
|
+
title: v.plan?.title ?? null,
|
|
13108
|
+
aspect: v.aspect,
|
|
13109
|
+
cost_usd: v.cost_usd,
|
|
13110
|
+
video_id: v.video_id,
|
|
13111
|
+
video_status: v.video?.status ?? null,
|
|
13112
|
+
error: v.error
|
|
13113
|
+
})),
|
|
13114
|
+
voices: out.voices
|
|
13115
|
+
};
|
|
13116
|
+
}
|
|
13117
|
+
}),
|
|
13118
|
+
tool({
|
|
13119
|
+
name: "ad_video_get",
|
|
13120
|
+
description: "One ad video (beta): its storyboard (scenes with their pictures once made), what making the missing pieces costs (estimate), its status and step; when ready, video_id and the video's status (it is prepared for the channels in the background: post it once video.status is ready). While it's running, check again every minute or so.",
|
|
13121
|
+
parameters: {
|
|
13122
|
+
type: "object",
|
|
13123
|
+
properties: { id: { type: "integer", description: "The ad video (from ad_video_list)" } },
|
|
13124
|
+
required: ["id"]
|
|
13125
|
+
},
|
|
13126
|
+
group: "social",
|
|
13127
|
+
need: read5,
|
|
13128
|
+
risk: "R0",
|
|
13129
|
+
run: async (ctx, { id }) => adVideo(await ws(ctx).adVideo(Number(id)))
|
|
13130
|
+
}),
|
|
13131
|
+
tool({
|
|
13132
|
+
name: "ad_video_change",
|
|
13133
|
+
description: "Change an ad video's storyboard (beta) as the user asks: a scene's picture brief (still), its movement (motion), the narrator's line, the words on screen (caption) or its seconds (3-8); redo_scenes to make scenes' pictures and clips again; the end card (cta_headline, cta_sub); the voice (a name from ad_video_list voices, or none); the music (words, or none). Only what a change touches is made again (redo): a picture brief or movement means a new paid clip; lines, words, end card, voice or music are cheap. Says the new cost; then ad_video_make. Not while it's being made. Preview first.",
|
|
13134
|
+
parameters: {
|
|
13135
|
+
type: "object",
|
|
13136
|
+
properties: {
|
|
13137
|
+
id: { type: "integer", description: "The ad video" },
|
|
13138
|
+
scenes: {
|
|
13139
|
+
type: "array",
|
|
13140
|
+
items: {
|
|
13141
|
+
type: "object",
|
|
13142
|
+
properties: {
|
|
13143
|
+
id: { type: "string", description: "Scene id, e.g. s2" },
|
|
13144
|
+
still: { type: "string" },
|
|
13145
|
+
motion: { type: "string" },
|
|
13146
|
+
line: { type: "string" },
|
|
13147
|
+
caption: { type: "string" },
|
|
13148
|
+
seconds: { type: "number" }
|
|
13149
|
+
},
|
|
13150
|
+
required: ["id"]
|
|
13151
|
+
}
|
|
13152
|
+
},
|
|
13153
|
+
redo_scenes: { type: "array", items: { type: "string" } },
|
|
13154
|
+
cta_headline: { type: "string" },
|
|
13155
|
+
cta_sub: { type: "string" },
|
|
13156
|
+
voice: { type: "string", description: "A voice name, or none" },
|
|
13157
|
+
music: { type: "string", description: "The music in words, or none" },
|
|
13158
|
+
title: { type: "string" }
|
|
13159
|
+
},
|
|
13160
|
+
required: ["id"]
|
|
13161
|
+
},
|
|
13162
|
+
group: "social",
|
|
13163
|
+
need: write5,
|
|
13164
|
+
risk: "W1",
|
|
13165
|
+
gates: ["VER"],
|
|
13166
|
+
run: async (ctx, a) => {
|
|
13167
|
+
const cta = {
|
|
13168
|
+
...a.cta_headline != null ? { headline: a.cta_headline } : {},
|
|
13169
|
+
...a.cta_sub != null ? { sub: a.cta_sub } : {}
|
|
13170
|
+
};
|
|
13171
|
+
const out = await ws(ctx).changeAdVideo(Number(a.id), {
|
|
13172
|
+
...a.scenes?.length ? { scenes: a.scenes } : {},
|
|
13173
|
+
...a.redo_scenes?.length ? { redo_scenes: a.redo_scenes } : {},
|
|
13174
|
+
...Object.keys(cta).length ? { cta } : {},
|
|
13175
|
+
...a.voice != null ? { voice: /^none$/i.test(a.voice) ? false : a.voice } : {},
|
|
13176
|
+
...a.music != null ? { music: /^none$/i.test(a.music) ? "" : a.music } : {},
|
|
13177
|
+
...a.title ? { title: a.title } : {}
|
|
13178
|
+
});
|
|
13179
|
+
return { ...adVideo(out), redo: out.redo ?? [] };
|
|
13180
|
+
}
|
|
13181
|
+
}),
|
|
13182
|
+
tool({
|
|
13183
|
+
name: "ad_video_make",
|
|
13184
|
+
description: "Make the ad video (beta): its pictures (on the user's own ChatGPT when their Maggie Bridge has images on, else Gemini), the moving clips (Gemini, billed per second), the voice, the music, the words and the end card, cut into one video, in the background (several minutes; follow it with ad_video_get). It COSTS MONEY: estimate_usd must be the estimate.usd from ad_video_plan / ad_video_get / ad_video_change that the user agreed to; if the cost changed it is refused with the new one. Each website has a daily limit. Making it again after a change or a failure only makes what's missing. When ready, post it with social_video_add and its video_id. Preview first.",
|
|
13185
|
+
parameters: {
|
|
13186
|
+
type: "object",
|
|
13187
|
+
properties: {
|
|
13188
|
+
id: { type: "integer", description: "The ad video" },
|
|
13189
|
+
estimate_usd: { type: "number", description: "The cost the user agreed to (estimate.usd)" }
|
|
13190
|
+
},
|
|
13191
|
+
required: ["id", "estimate_usd"]
|
|
13192
|
+
},
|
|
13193
|
+
group: "social",
|
|
13194
|
+
need: write5,
|
|
13195
|
+
risk: "W3",
|
|
13196
|
+
gates: ["VER"],
|
|
13197
|
+
run: async (ctx, { id, estimate_usd }) => adVideo(await ws(ctx).makeAdVideo(Number(id), Number(estimate_usd)))
|
|
13198
|
+
}),
|
|
13199
|
+
tool({
|
|
13200
|
+
name: "ad_video_stop",
|
|
13201
|
+
description: "Stop an ad video being made (after the piece it's on; what was made is kept and making it again carries on), or remove one that isn't being made from the list (posts made from it keep their video). Preview first.",
|
|
13202
|
+
parameters: {
|
|
13203
|
+
type: "object",
|
|
13204
|
+
properties: { id: { type: "integer", description: "The ad video" } },
|
|
13205
|
+
required: ["id"]
|
|
13206
|
+
},
|
|
13207
|
+
group: "social",
|
|
13208
|
+
need: write5,
|
|
13209
|
+
risk: "W1",
|
|
13210
|
+
gates: ["VER"],
|
|
13211
|
+
run: async (ctx, { id }) => ws(ctx).stopAdVideo(Number(id))
|
|
13212
|
+
}),
|
|
12947
13213
|
tool({
|
|
12948
13214
|
name: "social_youtube_set",
|
|
12949
13215
|
description: "Change a YouTube post that hasn't gone out: title (at most 100 characters, aim 40-60), tags, visibility (public, unlisted, private), made_for_kids (YouTube asks for every video: ask the user, never guess) or synthetic (realistic AI-made or altered content). The description is the post's text (social_update_copy). Preview first.",
|
|
@@ -16616,7 +16882,7 @@ var program = new Command();
|
|
|
16616
16882
|
// package.json
|
|
16617
16883
|
var package_default = {
|
|
16618
16884
|
name: "@cmo-so/cli",
|
|
16619
|
-
version: "0.
|
|
16885
|
+
version: "0.10.0",
|
|
16620
16886
|
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.",
|
|
16621
16887
|
type: "module",
|
|
16622
16888
|
license: "MIT",
|
|
@@ -16826,75 +17092,6 @@ import { randomBytes } from "node:crypto";
|
|
|
16826
17092
|
// ../../packages/mcp/src/index.ts
|
|
16827
17093
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16828
17094
|
|
|
16829
|
-
// ../../packages/mcp/src/messages.ts
|
|
16830
|
-
var UPPER = /* @__PURE__ */ new Set(["seo", "gsc", "gbp", "cta", "cmo", "dns", "ai", "api", "pdf", "url", "id"]);
|
|
16831
|
-
var n = (count, word, plural) => count === 1 ? `1 ${word}` : `${count ? count : "no"} ${plural ?? `${word}s`}`;
|
|
16832
|
-
function titleOf(name) {
|
|
16833
|
-
const words = name.split("_").map((w) => UPPER.has(w) ? w.toUpperCase() : w);
|
|
16834
|
-
const head = words[0] ?? "";
|
|
16835
|
-
return [head.charAt(0).toUpperCase() + head.slice(1), ...words.slice(1)].join(" ");
|
|
16836
|
-
}
|
|
16837
|
-
var LIST_KEYS = ["items", "results", "posts", "ideas", "workspaces", "rows"];
|
|
16838
|
-
function items(value) {
|
|
16839
|
-
if (Array.isArray(value)) return value;
|
|
16840
|
-
if (value && typeof value === "object") {
|
|
16841
|
-
for (const k of LIST_KEYS) {
|
|
16842
|
-
const v = value[k];
|
|
16843
|
-
if (Array.isArray(v)) return v;
|
|
16844
|
-
}
|
|
16845
|
-
}
|
|
16846
|
-
return null;
|
|
16847
|
-
}
|
|
16848
|
-
function label(x) {
|
|
16849
|
-
if (x && typeof x === "object") {
|
|
16850
|
-
const o = x;
|
|
16851
|
-
for (const k of ["title", "name", "domain", "label", "what", "text", "id"]) {
|
|
16852
|
-
if (o[k] !== void 0 && o[k] !== null && o[k] !== "") return String(o[k]);
|
|
16853
|
-
}
|
|
16854
|
-
return Object.entries(o).filter(([, v]) => typeof v !== "object").slice(0, 3).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
16855
|
-
}
|
|
16856
|
-
return String(x);
|
|
16857
|
-
}
|
|
16858
|
-
function bodyLines(value) {
|
|
16859
|
-
const list2 = items(value);
|
|
16860
|
-
if (list2) {
|
|
16861
|
-
const out = list2.slice(0, 8).map((x) => `- ${label(x)}`.slice(0, 200));
|
|
16862
|
-
return list2.length > 8 ? [...out, `\u2026and ${list2.length - 8} more`] : out;
|
|
16863
|
-
}
|
|
16864
|
-
if (value && typeof value === "object") {
|
|
16865
|
-
return Object.entries(value).filter(([k, v]) => v !== null && v !== void 0 && v !== "" && typeof v !== "object" && k !== "next").slice(0, 8).map(([k, v]) => `${k.replace(/_/g, " ")}: ${v}`.slice(0, 200));
|
|
16866
|
-
}
|
|
16867
|
-
return value === null || value === void 0 || value === "" ? [] : [String(value).slice(0, 600)];
|
|
16868
|
-
}
|
|
16869
|
-
function said(name, value) {
|
|
16870
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
16871
|
-
for (const k of ["say", "headline", "summary", "message"]) {
|
|
16872
|
-
const v = value[k];
|
|
16873
|
-
if (typeof v === "string" && v.trim()) return (v.trim().split("\n")[0] ?? "").slice(0, 240);
|
|
16874
|
-
}
|
|
16875
|
-
}
|
|
16876
|
-
const list2 = items(value);
|
|
16877
|
-
if (list2) {
|
|
16878
|
-
let noun = name.endsWith("_list") ? name.slice(0, -5).split("_").pop() ?? "item" : "item";
|
|
16879
|
-
if (noun.endsWith("s")) noun = noun.slice(0, -1);
|
|
16880
|
-
return list2.length ? `I found ${n(list2.length, noun)}.` : "There's nothing there yet.";
|
|
16881
|
-
}
|
|
16882
|
-
if (typeof value === "string") return (value.trim().split("\n")[0] ?? "").slice(0, 240) || "Done.";
|
|
16883
|
-
return "Here it is.";
|
|
16884
|
-
}
|
|
16885
|
-
var lowerFirst = (s) => /^[A-Za-z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
16886
|
-
function message(name, value, opts = {}) {
|
|
16887
|
-
const title = titleOf(name);
|
|
16888
|
-
if (opts.error) return { say: `That didn't work: ${opts.error}`.slice(0, 300), title, lines: [] };
|
|
16889
|
-
if (opts.preview) return { say: "This needs your yes first; nothing has changed yet.", title, lines: [opts.preview] };
|
|
16890
|
-
if (opts.done) return { say: `Done: ${lowerFirst(opts.done)}`, title, lines: bodyLines(value) };
|
|
16891
|
-
return { say: said(name, value), title, lines: bodyLines(value) };
|
|
16892
|
-
}
|
|
16893
|
-
function messageText(m) {
|
|
16894
|
-
const body = [m.title ? `**${m.title}**` : "", ...m.lines].filter(Boolean).join("\n");
|
|
16895
|
-
return [m.say, body].filter(Boolean).join("\n\n").trim();
|
|
16896
|
-
}
|
|
16897
|
-
|
|
16898
17095
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
|
|
16899
17096
|
var util_exports = {};
|
|
16900
17097
|
__export(util_exports, {
|
|
@@ -26552,6 +26749,75 @@ var Server = class extends Protocol {
|
|
|
26552
26749
|
}
|
|
26553
26750
|
};
|
|
26554
26751
|
|
|
26752
|
+
// ../../packages/mcp/src/messages.ts
|
|
26753
|
+
var UPPER = /* @__PURE__ */ new Set(["seo", "gsc", "gbp", "cta", "cmo", "dns", "ai", "api", "pdf", "url", "id"]);
|
|
26754
|
+
var n = (count, word, plural) => count === 1 ? `1 ${word}` : `${count ? count : "no"} ${plural ?? `${word}s`}`;
|
|
26755
|
+
function titleOf(name) {
|
|
26756
|
+
const words = name.split("_").map((w) => UPPER.has(w) ? w.toUpperCase() : w);
|
|
26757
|
+
const head = words[0] ?? "";
|
|
26758
|
+
return [head.charAt(0).toUpperCase() + head.slice(1), ...words.slice(1)].join(" ");
|
|
26759
|
+
}
|
|
26760
|
+
var LIST_KEYS = ["items", "results", "posts", "ideas", "workspaces", "rows"];
|
|
26761
|
+
function items(value) {
|
|
26762
|
+
if (Array.isArray(value)) return value;
|
|
26763
|
+
if (value && typeof value === "object") {
|
|
26764
|
+
for (const k of LIST_KEYS) {
|
|
26765
|
+
const v = value[k];
|
|
26766
|
+
if (Array.isArray(v)) return v;
|
|
26767
|
+
}
|
|
26768
|
+
}
|
|
26769
|
+
return null;
|
|
26770
|
+
}
|
|
26771
|
+
function label(x) {
|
|
26772
|
+
if (x && typeof x === "object") {
|
|
26773
|
+
const o = x;
|
|
26774
|
+
for (const k of ["title", "name", "domain", "label", "what", "text", "id"]) {
|
|
26775
|
+
if (o[k] !== void 0 && o[k] !== null && o[k] !== "") return String(o[k]);
|
|
26776
|
+
}
|
|
26777
|
+
return Object.entries(o).filter(([, v]) => typeof v !== "object").slice(0, 3).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
26778
|
+
}
|
|
26779
|
+
return String(x);
|
|
26780
|
+
}
|
|
26781
|
+
function bodyLines(value) {
|
|
26782
|
+
const list2 = items(value);
|
|
26783
|
+
if (list2) {
|
|
26784
|
+
const out = list2.slice(0, 8).map((x) => `- ${label(x)}`.slice(0, 200));
|
|
26785
|
+
return list2.length > 8 ? [...out, `\u2026and ${list2.length - 8} more`] : out;
|
|
26786
|
+
}
|
|
26787
|
+
if (value && typeof value === "object") {
|
|
26788
|
+
return Object.entries(value).filter(([k, v]) => v !== null && v !== void 0 && v !== "" && typeof v !== "object" && k !== "next").slice(0, 8).map(([k, v]) => `${k.replace(/_/g, " ")}: ${v}`.slice(0, 200));
|
|
26789
|
+
}
|
|
26790
|
+
return value === null || value === void 0 || value === "" ? [] : [String(value).slice(0, 600)];
|
|
26791
|
+
}
|
|
26792
|
+
function said(name, value) {
|
|
26793
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
26794
|
+
for (const k of ["say", "headline", "summary", "message"]) {
|
|
26795
|
+
const v = value[k];
|
|
26796
|
+
if (typeof v === "string" && v.trim()) return (v.trim().split("\n")[0] ?? "").slice(0, 240);
|
|
26797
|
+
}
|
|
26798
|
+
}
|
|
26799
|
+
const list2 = items(value);
|
|
26800
|
+
if (list2) {
|
|
26801
|
+
let noun = name.endsWith("_list") ? name.slice(0, -5).split("_").pop() ?? "item" : "item";
|
|
26802
|
+
if (noun.endsWith("s")) noun = noun.slice(0, -1);
|
|
26803
|
+
return list2.length ? `I found ${n(list2.length, noun)}.` : "There's nothing there yet.";
|
|
26804
|
+
}
|
|
26805
|
+
if (typeof value === "string") return (value.trim().split("\n")[0] ?? "").slice(0, 240) || "Done.";
|
|
26806
|
+
return "Here it is.";
|
|
26807
|
+
}
|
|
26808
|
+
var lowerFirst = (s) => /^[A-Za-z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
26809
|
+
function message(name, value, opts = {}) {
|
|
26810
|
+
const title = titleOf(name);
|
|
26811
|
+
if (opts.error) return { say: `That didn't work: ${opts.error}`.slice(0, 300), title, lines: [] };
|
|
26812
|
+
if (opts.preview) return { say: "This needs your yes first; nothing has changed yet.", title, lines: [opts.preview] };
|
|
26813
|
+
if (opts.done) return { say: `Done: ${lowerFirst(opts.done)}`, title, lines: bodyLines(value) };
|
|
26814
|
+
return { say: said(name, value), title, lines: bodyLines(value) };
|
|
26815
|
+
}
|
|
26816
|
+
function messageText(m) {
|
|
26817
|
+
const body = [m.title ? `**${m.title}**` : "", ...m.lines].filter(Boolean).join("\n");
|
|
26818
|
+
return [m.say, body].filter(Boolean).join("\n\n").trim();
|
|
26819
|
+
}
|
|
26820
|
+
|
|
26555
26821
|
// ../../packages/mcp/src/prompts.ts
|
|
26556
26822
|
var workspaceArg = {
|
|
26557
26823
|
name: "workspace",
|
|
@@ -27708,7 +27974,18 @@ social.command("pictures <id> <urls...>").description("your own pictures on a po
|
|
|
27708
27974
|
async (opts, id, urls) => tool2(opts, "social_pictures_attach", { id: ids2([id])[0], urls, append: opts.append === true })
|
|
27709
27975
|
)
|
|
27710
27976
|
);
|
|
27711
|
-
social.command("
|
|
27977
|
+
social.command("slides <id> [count]").description("a text carousel of 5-10 slides for a post (a cover, one point a slide, the next step; free)").option("--style <style>", "bold_type (big words, the default) or infographic (lists and numbers)").action(
|
|
27978
|
+
act(
|
|
27979
|
+
async (opts, id, count) => tool2(opts, "social_image_new", {
|
|
27980
|
+
id: ids2([id])[0],
|
|
27981
|
+
count: Number(count ?? 7),
|
|
27982
|
+
...opts.style ? { style: opts.style } : {}
|
|
27983
|
+
})
|
|
27984
|
+
)
|
|
27985
|
+
);
|
|
27986
|
+
social.command("carousel <id>").description(
|
|
27987
|
+
"a post's carousel: --pdf for LinkedIn, Instagram, Facebook or TikTok; --format / --title for LinkedIn's"
|
|
27988
|
+
).option("--format <format>", "pdf (a swipeable carousel, the default) or images").option("--title <text>", "shown on the carousel").option("--pdf <url>", "a public https link to your own PDF: its pages become the pictures (LinkedIn: the PDF)").action(
|
|
27712
27989
|
act(async (opts, id) => {
|
|
27713
27990
|
const post = ids2([id])[0];
|
|
27714
27991
|
if (opts.pdf)
|
|
@@ -27800,6 +28077,58 @@ video.command("add <url>").description("posts from a direct link to a video file
|
|
|
27800
28077
|
})
|
|
27801
28078
|
)
|
|
27802
28079
|
);
|
|
28080
|
+
video.command("post <videoId>").description("posts from a video already in CMO.so (e.g. a ready ad video's video_id), one per channel").option("--channel <names>", "linkedin, facebook, instagram, youtube, tiktok (comma-separated)", "youtube").option("--note <text>", "what the video is about").action(
|
|
28081
|
+
act(
|
|
28082
|
+
async (opts, videoId) => tool2(opts, "social_video_add", {
|
|
28083
|
+
video_id: ids2([videoId])[0],
|
|
28084
|
+
channels: (channelsOpt(opts.channel) ?? ["youtube"]).filter((c) => c !== "google_business"),
|
|
28085
|
+
...opts.note ? { note: opts.note } : {}
|
|
28086
|
+
})
|
|
28087
|
+
)
|
|
28088
|
+
);
|
|
28089
|
+
var ad = video.command("ad").description("ad videos (beta): plan, change, make (billed), post");
|
|
28090
|
+
ad.command("plan [brief...]").description("a storyboard and what making it costs (one AI generation)").option("--seconds <n>", "15, 20 or 30", "20").option("--landscape", "16:9 instead of vertical 9:16").option("--scenes <n>", "2-5").option("--from-post <id>", "make it from a post's text").option("--reference <youtubeUrl>", "a YouTube ad whose rhythm to follow").action(
|
|
28091
|
+
act(
|
|
28092
|
+
async (opts, brief2) => tool2(opts, "ad_video_plan", {
|
|
28093
|
+
...brief2?.length ? { brief: brief2.join(" ") } : {},
|
|
28094
|
+
...opts.fromPost ? { item_id: ids2([opts.fromPost])[0] } : {},
|
|
28095
|
+
seconds: Number(opts.seconds),
|
|
28096
|
+
aspect: opts.landscape ? "16:9" : "9:16",
|
|
28097
|
+
...opts.scenes ? { scenes: Number(opts.scenes) } : {},
|
|
28098
|
+
...opts.reference ? { reference_url: opts.reference } : {}
|
|
28099
|
+
})
|
|
28100
|
+
)
|
|
28101
|
+
);
|
|
28102
|
+
ad.command("list").description("this website's ad videos").action(act(async (opts) => tool2(opts, "ad_video_list")));
|
|
28103
|
+
ad.command("show <id>").description("one ad video: storyboard, cost, status").action(act(async (opts, id) => tool2(opts, "ad_video_get", { id: ids2([id])[0] })));
|
|
28104
|
+
ad.command("change <id>").description("change the storyboard (only what a change touches is made again)").option("--scene <id=field:text...>", "e.g. s2=line:Book today (fields: still, motion, line, caption, seconds)").option("--redo <sceneIds>", "make these scenes' pictures and clips again (comma-separated)").option("--headline <text>", "the end card's headline").option("--sub <text>", "the end card's line").option("--voice <name>", "a voice name, or none").option("--music <text>", "the music in words, or none").action(
|
|
28105
|
+
act(
|
|
28106
|
+
async (opts, id) => {
|
|
28107
|
+
const scenes = {};
|
|
28108
|
+
for (const spec of opts.scene ?? []) {
|
|
28109
|
+
const m = /^([a-z0-9]+)=(still|motion|line|caption|seconds):(.*)$/s.exec(spec);
|
|
28110
|
+
if (!m) throw new Error(`--scene ${spec}: use <sceneId>=<field>:<text>, e.g. s2=line:Book today`);
|
|
28111
|
+
const [, sid = "", field = "", text3 = ""] = m;
|
|
28112
|
+
scenes[sid] = { ...scenes[sid] ?? {}, [field]: field === "seconds" ? Number(text3) : text3 };
|
|
28113
|
+
}
|
|
28114
|
+
return tool2(opts, "ad_video_change", {
|
|
28115
|
+
id: ids2([id])[0],
|
|
28116
|
+
...Object.keys(scenes).length ? { scenes: Object.entries(scenes).map(([sid, f]) => ({ id: sid, ...f })) } : {},
|
|
28117
|
+
...opts.redo ? { redo_scenes: opts.redo.split(",").map((x) => x.trim()) } : {},
|
|
28118
|
+
...opts.headline ? { cta_headline: opts.headline } : {},
|
|
28119
|
+
...opts.sub ? { cta_sub: opts.sub } : {},
|
|
28120
|
+
...opts.voice ? { voice: opts.voice } : {},
|
|
28121
|
+
...opts.music ? { music: opts.music } : {}
|
|
28122
|
+
});
|
|
28123
|
+
}
|
|
28124
|
+
)
|
|
28125
|
+
);
|
|
28126
|
+
ad.command("make <id> <usd>").description("make it: costs money; <usd> is the estimate you agreed to (from plan / show)").action(
|
|
28127
|
+
act(
|
|
28128
|
+
async (opts, id, usd) => tool2(opts, "ad_video_make", { id: ids2([id])[0], estimate_usd: Number(usd) })
|
|
28129
|
+
)
|
|
28130
|
+
);
|
|
28131
|
+
ad.command("stop <id>").description("stop one being made (what's made is kept), or remove one").action(act(async (opts, id) => tool2(opts, "ad_video_stop", { id: ids2([id])[0] })));
|
|
27803
28132
|
video.command("attach <id> <url>").description("make a LinkedIn / Facebook / Instagram post a video post (its picture is removed)").action(act(async (opts, id, url) => tool2(opts, "social_video_attach", { id: ids2([id])[0], url })));
|
|
27804
28133
|
video.command("detach <id>").description("take the video off a post (it's a text post again)").action(act(async (opts, id) => tool2(opts, "social_video_detach", { id: ids2([id])[0] })));
|
|
27805
28134
|
var tiktok = social.command("tiktok").description("your TikTok account (beta): short videos you approve one by one");
|
|
@@ -27819,6 +28148,15 @@ tiktok.command("add <url>").description("a TikTok post from a direct link to a v
|
|
|
27819
28148
|
})
|
|
27820
28149
|
)
|
|
27821
28150
|
);
|
|
28151
|
+
tiktok.command("photo <id>").description("a TikTok photo post from a post's pictures (a set, a text carousel or a PDF's pages)").option("--note <text>", "what to say in the caption").option("--caption <text>", "your own caption (no AI)").action(
|
|
28152
|
+
act(
|
|
28153
|
+
async (opts, id) => tool2(opts, "social_tiktok_photo", {
|
|
28154
|
+
id: ids2([id])[0],
|
|
28155
|
+
...opts.note ? { note: opts.note } : {},
|
|
28156
|
+
...opts.caption ? { caption: opts.caption } : {}
|
|
28157
|
+
})
|
|
28158
|
+
)
|
|
28159
|
+
);
|
|
27822
28160
|
tiktok.command("set <id>").description("a TikTok post's settings, as you choose them (nothing is preset)").option("--privacy <p>", "everyone, friends, followers or only-me (what your account offers)").option("--comment <yes|no>").option("--duet <yes|no>").option("--stitch <yes|no>").option("--promotes <what>", "none, your-brand, branded or both (content disclosure)").option("--ai <yes|no>", "AI-generated content label").action(
|
|
27823
28161
|
act(
|
|
27824
28162
|
async (opts, id) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
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",
|