@cmo-so/cli 0.9.3 → 0.11.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 +553 -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,35 @@ 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 (estimate.pictures_usd for stage
|
|
8093
|
+
* "pictures": only the first frames, to look at before the paid clips). A script that doesn't fit its
|
|
8094
|
+
* timing is refused (409 script_check) unless goAhead. */
|
|
8095
|
+
makeAdVideo: (adId, estimateUsd, opts = {}) => post(`${base}/social/ad-videos/${adId}/make`, {
|
|
8096
|
+
confirm: true,
|
|
8097
|
+
estimate_usd: estimateUsd,
|
|
8098
|
+
...opts.stage ? { stage: opts.stage } : {},
|
|
8099
|
+
...opts.goAhead ? { go_ahead: true } : {}
|
|
8100
|
+
}),
|
|
8101
|
+
/** The person's own photo as a scene's first frame (base64, or photo_url: a picture already on CMO.so). */
|
|
8102
|
+
adVideoScenePicture: (adId, sceneId, photo) => post(`${base}/social/ad-videos/${adId}/scenes/${encodeURIComponent(sceneId)}/picture`, photo),
|
|
8103
|
+
/** Stop one being made (what's made is kept), or remove one. */
|
|
8104
|
+
stopAdVideo: (adId) => api2.request(`${base}/social/ad-videos/${adId}`, { method: "DELETE" }),
|
|
8075
8105
|
/** Make a LinkedIn, Facebook or Instagram post a video post: it goes out as this video (its picture is dropped). */
|
|
8076
8106
|
attachVideo: (id, videoId) => post(`${base}/social/${id}/video`, { video_id: videoId }),
|
|
8077
8107
|
/** Take the video off a post: it's a text post again. */
|
|
@@ -8479,6 +8509,12 @@ var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
|
8479
8509
|
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
8480
8510
|
(c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
|
|
8481
8511
|
);
|
|
8512
|
+
var videoFrom = (a, lang = "en") => {
|
|
8513
|
+
const name = a.file?.file_name;
|
|
8514
|
+
if (a.file) return name ? q(String(name)) : lang === "en" ? "your video" : "\u4F60\u7684\u5F71\u7247";
|
|
8515
|
+
if (a.video_id) return lang === "en" ? `video #${a.video_id}` : `\u5F71\u7247 #${a.video_id}`;
|
|
8516
|
+
return String(a.url ?? "");
|
|
8517
|
+
};
|
|
8482
8518
|
var confirmCopy = {
|
|
8483
8519
|
// --------------------------------------------------------- account / setup
|
|
8484
8520
|
website_add: {
|
|
@@ -8753,8 +8789,8 @@ var confirmCopy = {
|
|
|
8753
8789
|
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8754
8790
|
},
|
|
8755
8791
|
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
|
|
8792
|
+
en: (a) => `Make post #${a.id} a video post with ${videoFrom(a)} (its picture is removed).`,
|
|
8793
|
+
"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
8794
|
},
|
|
8759
8795
|
social_video_detach: {
|
|
8760
8796
|
en: (a) => `Take the video off post #${a.id} (it's a text post again).`,
|
|
@@ -8767,11 +8803,11 @@ var confirmCopy = {
|
|
|
8767
8803
|
social_video_add: {
|
|
8768
8804
|
en: (a) => {
|
|
8769
8805
|
const names = videoChannels(a).join(" and ");
|
|
8770
|
-
return `Make a ${names} post from ${a
|
|
8806
|
+
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
8807
|
},
|
|
8772
8808
|
"zh-Hant": (a) => {
|
|
8773
8809
|
const names = videoChannels(a).join(" \u548C ");
|
|
8774
|
-
return `\u7528 ${a
|
|
8810
|
+
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
8811
|
}
|
|
8776
8812
|
},
|
|
8777
8813
|
social_tiktok_set: {
|
|
@@ -8787,8 +8823,8 @@ var confirmCopy = {
|
|
|
8787
8823
|
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8788
8824
|
},
|
|
8789
8825
|
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`
|
|
8826
|
+
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" : ""}.`,
|
|
8827
|
+
"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
8828
|
},
|
|
8793
8829
|
social_image_stop: {
|
|
8794
8830
|
en: () => "Stop the picture being made; the post keeps the one it had.",
|
|
@@ -8805,8 +8841,32 @@ var confirmCopy = {
|
|
|
8805
8841
|
}
|
|
8806
8842
|
},
|
|
8807
8843
|
social_carousel_pdf_attach: {
|
|
8808
|
-
en: (a) => `
|
|
8809
|
-
"zh-Hant": (a) =>
|
|
8844
|
+
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).`,
|
|
8845
|
+
"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`
|
|
8846
|
+
},
|
|
8847
|
+
social_tiktok_photo: {
|
|
8848
|
+
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.`,
|
|
8849
|
+
"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`
|
|
8850
|
+
},
|
|
8851
|
+
ad_video_plan: {
|
|
8852
|
+
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.`,
|
|
8853
|
+
"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`
|
|
8854
|
+
},
|
|
8855
|
+
ad_video_change: {
|
|
8856
|
+
en: (a) => `Change ad video #${a.id}'s storyboard: ${keys(a).filter((k) => k !== "id").join(", ") || "nothing"}.`,
|
|
8857
|
+
"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`
|
|
8858
|
+
},
|
|
8859
|
+
ad_video_make: {
|
|
8860
|
+
en: (a) => a.stage === "pictures" ? `Make the pictures of ad video #${a.id} now, to look at before the moving clips. This costs about $${Number(a.estimate_usd ?? 0).toFixed(2)}.` : `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.${a.go_ahead ? " Its script doesn't fit its timing everywhere; it's made anyway." : ""}`,
|
|
8861
|
+
"zh-Hant": (a) => a.stage === "pictures" ? `\u73FE\u5728\u5148\u88FD\u4F5C\u5EE3\u544A\u5F71\u7247 #${a.id} \u7684\u5716\u7247\uFF0C\u770B\u904E\u518D\u505A\u52D5\u614B\u7247\u6BB5\u3002\u8CBB\u7528\u7D04 $${Number(a.estimate_usd ?? 0).toFixed(2)}\u3002` : `\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${a.go_ahead ? "\u8173\u672C\u6709\u90E8\u5206\u6642\u9593\u4E0D\u5408\uFF0C\u4ECD\u7167\u6A23\u88FD\u4F5C\u3002" : ""}`
|
|
8862
|
+
},
|
|
8863
|
+
ad_video_picture: {
|
|
8864
|
+
en: (a) => `Use this picture as the first frame of scene ${a.scene} in ad video #${a.id} (its clip is made again).`,
|
|
8865
|
+
"zh-Hant": (a) => `\u628A\u9019\u5F35\u5716\u7247\u7528\u4F5C\u5EE3\u544A\u5F71\u7247 #${a.id} \u7B2C ${a.scene} \u683C\u7684\u7B2C\u4E00\u500B\u756B\u9762\uFF08\u9019\u683C\u7684\u52D5\u614B\u7247\u6BB5\u6703\u91CD\u505A\uFF09\u3002`
|
|
8866
|
+
},
|
|
8867
|
+
ad_video_stop: {
|
|
8868
|
+
en: (a) => `Stop ad video #${a.id} if it's being made (what's made is kept), or remove it from the list.`,
|
|
8869
|
+
"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
8870
|
},
|
|
8811
8871
|
social_carousel_set: {
|
|
8812
8872
|
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 +11626,44 @@ var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
|
11566
11626
|
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
|
|
11567
11627
|
var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
|
|
11568
11628
|
var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
11629
|
+
var SLIDES = [5, 6, 7, 8, 9, 10];
|
|
11630
|
+
var adVideo = (v) => ({
|
|
11631
|
+
id: v.id,
|
|
11632
|
+
status: v.status,
|
|
11633
|
+
step: v.step,
|
|
11634
|
+
aspect: v.aspect,
|
|
11635
|
+
plan: v.plan ? {
|
|
11636
|
+
title: v.plan.title,
|
|
11637
|
+
style: v.plan.style,
|
|
11638
|
+
voice: v.plan.voice,
|
|
11639
|
+
music: v.plan.music,
|
|
11640
|
+
seconds: v.plan.seconds,
|
|
11641
|
+
end_card: { ...v.plan.cta, seconds: v.plan.card_seconds ?? 2.5 },
|
|
11642
|
+
scenes: v.plan.scenes.map((s) => ({
|
|
11643
|
+
id: s.id,
|
|
11644
|
+
seconds: s.seconds,
|
|
11645
|
+
picture: s.still,
|
|
11646
|
+
motion: s.motion,
|
|
11647
|
+
line: s.line,
|
|
11648
|
+
caption: s.caption,
|
|
11649
|
+
picture_url: s.still_url ?? null,
|
|
11650
|
+
own_picture: s.own_picture ?? false,
|
|
11651
|
+
...s.caption_in != null ? { caption_in: s.caption_in } : {},
|
|
11652
|
+
...s.caption_out != null ? { caption_out: s.caption_out } : {},
|
|
11653
|
+
...s.voice_at != null ? { voice_at: s.voice_at } : {}
|
|
11654
|
+
}))
|
|
11655
|
+
} : null,
|
|
11656
|
+
timeline: v.timeline ?? null,
|
|
11657
|
+
script_check: v.script_check ? { ok: v.script_check.ok, total: v.script_check.total, issues: v.script_check.issues } : null,
|
|
11658
|
+
pictures_ready: v.pictures_ready ?? false,
|
|
11659
|
+
estimate: v.estimate,
|
|
11660
|
+
cost_usd: v.cost_usd,
|
|
11661
|
+
video_id: v.video_id,
|
|
11662
|
+
post: v.item ?? null,
|
|
11663
|
+
video: v.video ? { status: v.video.status, preview_url: v.video.preview_url, poster_url: v.video.poster_url } : null,
|
|
11664
|
+
error: v.error,
|
|
11665
|
+
next: v.status === "planned" ? `Show the storyboard, the timeline, any script_check issues and the cost (about $${(v.estimate?.usd ?? 0).toFixed(2)}${v.pictures_ready ? "" : `; the pictures alone about $${(v.estimate?.pictures_usd ?? 0).toFixed(2)}`}); change it with ad_video_change (or ad_video_picture for the user's own photo); ${v.pictures_ready ? "the pictures are made: show them (picture_url), then" : "suggest ad_video_make stage pictures first to see the pictures cheaply, then"} 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" ? v.item?.attached ? `Ready, and it's on the post it was made from (post ${v.item.id}): that post goes out as this video. To post it elsewhere too: social_video_add (video_id and channels) once video.status is ready.` : `Ready: post it with social_video_add (video_id and channels) once video.status is ready.${v.item?.skipped ? ` It wasn't put on post ${v.item.id}: ${v.item.skipped}` : ""}` : v.status === "failed" ? "Failed: say why; ad_video_make again carries on from what was made." : ""
|
|
11666
|
+
});
|
|
11569
11667
|
var channelsParam = {
|
|
11570
11668
|
type: "array",
|
|
11571
11669
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
@@ -11582,6 +11680,11 @@ var FILE_SCHEMA = {
|
|
|
11582
11680
|
required: ["download_url", "file_id"]
|
|
11583
11681
|
};
|
|
11584
11682
|
var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
|
|
11683
|
+
var videoLink = (a) => {
|
|
11684
|
+
const link = fileLinks(a.file)[0] ?? String(a.url ?? "").trim();
|
|
11685
|
+
if (!link.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
|
|
11686
|
+
return link;
|
|
11687
|
+
};
|
|
11585
11688
|
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11586
11689
|
var channelsOf = (value) => {
|
|
11587
11690
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -11780,7 +11883,7 @@ var socialTools = [
|
|
|
11780
11883
|
}),
|
|
11781
11884
|
tool({
|
|
11782
11885
|
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.",
|
|
11886
|
+
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
11887
|
parameters: {
|
|
11785
11888
|
type: "object",
|
|
11786
11889
|
properties: {
|
|
@@ -11797,7 +11900,7 @@ var socialTools = [
|
|
|
11797
11900
|
},
|
|
11798
11901
|
count: {
|
|
11799
11902
|
type: "integer",
|
|
11800
|
-
description: "1 (default)
|
|
11903
|
+
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
11904
|
},
|
|
11802
11905
|
text_effect: {
|
|
11803
11906
|
type: "string",
|
|
@@ -11819,7 +11922,7 @@ var socialTools = [
|
|
|
11819
11922
|
replace: replace === true,
|
|
11820
11923
|
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
11821
11924
|
...mode === "edit" ? { mode: "edit" } : {},
|
|
11822
|
-
...Number(count) === 4
|
|
11925
|
+
...mode !== "edit" && (Number(count) === 4 || SLIDES.includes(Number(count))) ? { count: Number(count) } : {},
|
|
11823
11926
|
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
11824
11927
|
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
11825
11928
|
});
|
|
@@ -12045,11 +12148,14 @@ var socialTools = [
|
|
|
12045
12148
|
}),
|
|
12046
12149
|
tool({
|
|
12047
12150
|
name: "social_carousel_pdf_attach",
|
|
12048
|
-
description: "A
|
|
12151
|
+
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
12152
|
parameters: {
|
|
12050
12153
|
type: "object",
|
|
12051
12154
|
properties: {
|
|
12052
|
-
id: {
|
|
12155
|
+
id: {
|
|
12156
|
+
type: "integer",
|
|
12157
|
+
description: "A LinkedIn, Instagram, Facebook or TikTok post (from social_list), not gone out yet"
|
|
12158
|
+
},
|
|
12053
12159
|
file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
|
|
12054
12160
|
url: { type: "string", description: "Public https link to the PDF file" },
|
|
12055
12161
|
title: {
|
|
@@ -12063,7 +12169,7 @@ var socialTools = [
|
|
|
12063
12169
|
group: "social",
|
|
12064
12170
|
need: write5,
|
|
12065
12171
|
risk: "W1",
|
|
12066
|
-
gates: ["VER"
|
|
12172
|
+
gates: ["VER"],
|
|
12067
12173
|
run: async (ctx, { id, url, file, title }) => {
|
|
12068
12174
|
const link = fileLinks(file)[0] ?? String(url ?? "").trim();
|
|
12069
12175
|
if (!link.startsWith("https://"))
|
|
@@ -12092,7 +12198,7 @@ var socialTools = [
|
|
|
12092
12198
|
id: post.id,
|
|
12093
12199
|
channel: post.provider ?? null,
|
|
12094
12200
|
url: `https://app.cmo.so/social?picture=${post.id}${site}`,
|
|
12095
|
-
how:
|
|
12201
|
+
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
12202
|
};
|
|
12097
12203
|
}
|
|
12098
12204
|
}),
|
|
@@ -12790,22 +12896,24 @@ var socialTools = [
|
|
|
12790
12896
|
}),
|
|
12791
12897
|
tool({
|
|
12792
12898
|
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
|
|
12899
|
+
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
12900
|
parameters: {
|
|
12795
12901
|
type: "object",
|
|
12796
12902
|
properties: {
|
|
12797
12903
|
id: { type: "integer", description: "The post (from social_list)" },
|
|
12904
|
+
file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
|
|
12798
12905
|
url: { type: "string", description: "Direct https link to the video file" }
|
|
12799
12906
|
},
|
|
12800
|
-
required: ["id"
|
|
12907
|
+
required: ["id"]
|
|
12801
12908
|
},
|
|
12909
|
+
fileParams: ["file"],
|
|
12802
12910
|
group: "social",
|
|
12803
12911
|
need: write5,
|
|
12804
12912
|
risk: "W1",
|
|
12805
12913
|
gates: ["VER"],
|
|
12806
12914
|
run: async (ctx, a) => {
|
|
12807
12915
|
const d = ws(ctx);
|
|
12808
|
-
let v = await d.videoFromLink(
|
|
12916
|
+
let v = await d.videoFromLink(videoLink(a));
|
|
12809
12917
|
const until = Date.now() + 15e4;
|
|
12810
12918
|
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
12811
12919
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -12881,11 +12989,13 @@ var socialTools = [
|
|
|
12881
12989
|
}),
|
|
12882
12990
|
tool({
|
|
12883
12991
|
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
|
|
12992
|
+
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
12993
|
parameters: {
|
|
12886
12994
|
type: "object",
|
|
12887
12995
|
properties: {
|
|
12996
|
+
file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
|
|
12888
12997
|
url: { type: "string", description: "Direct https link to the video file" },
|
|
12998
|
+
video_id: { type: "integer", description: "A video already in CMO.so (an ad video's video_id)" },
|
|
12889
12999
|
channels: {
|
|
12890
13000
|
type: "array",
|
|
12891
13001
|
items: { type: "string", enum: [...VIDEO_KEYS] },
|
|
@@ -12897,9 +13007,9 @@ var socialTools = [
|
|
|
12897
13007
|
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
12898
13008
|
end: { type: "number", description: "End of the part to use, seconds" },
|
|
12899
13009
|
vertical: { type: "string", enum: ["left", "middle", "right"] }
|
|
12900
|
-
}
|
|
12901
|
-
required: ["url"]
|
|
13010
|
+
}
|
|
12902
13011
|
},
|
|
13012
|
+
fileParams: ["file"],
|
|
12903
13013
|
group: "social",
|
|
12904
13014
|
need: write5,
|
|
12905
13015
|
risk: "W2",
|
|
@@ -12911,7 +13021,7 @@ var socialTools = [
|
|
|
12911
13021
|
...a.end != null ? { end: Number(a.end) } : {},
|
|
12912
13022
|
...a.vertical ? { aspect: "9:16", crop_x: a.vertical === "left" ? 0 : a.vertical === "right" ? 1 : 0.5 } : {}
|
|
12913
13023
|
};
|
|
12914
|
-
let v = await d.
|
|
13024
|
+
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
13025
|
const until = Date.now() + 15e4;
|
|
12916
13026
|
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
12917
13027
|
await new Promise((r) => setTimeout(r, 3e3));
|
|
@@ -12944,6 +13054,250 @@ var socialTools = [
|
|
|
12944
13054
|
};
|
|
12945
13055
|
}
|
|
12946
13056
|
}),
|
|
13057
|
+
tool({
|
|
13058
|
+
name: "social_tiktok_photo",
|
|
13059
|
+
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.",
|
|
13060
|
+
parameters: {
|
|
13061
|
+
type: "object",
|
|
13062
|
+
properties: {
|
|
13063
|
+
id: { type: "integer", description: "A post with pictures (from social_list), not a video post" },
|
|
13064
|
+
note: { type: "string", description: "What to say in the caption, in the user's words" },
|
|
13065
|
+
caption: { type: "string", description: "The caption itself, when the user wrote it (no AI)" }
|
|
13066
|
+
},
|
|
13067
|
+
required: ["id"]
|
|
13068
|
+
},
|
|
13069
|
+
group: "social",
|
|
13070
|
+
need: write5,
|
|
13071
|
+
risk: "W2",
|
|
13072
|
+
gates: ["VER", "LLM"],
|
|
13073
|
+
run: async (ctx, { id, note, caption }) => {
|
|
13074
|
+
const out = await ws(ctx).tiktokPhotoPost(Number(id), {
|
|
13075
|
+
...note ? { note: String(note).slice(0, 1e3) } : {},
|
|
13076
|
+
...typeof caption === "string" && caption.trim() ? { caption: caption.trim(), write: false } : {}
|
|
13077
|
+
});
|
|
13078
|
+
return {
|
|
13079
|
+
post: item(out),
|
|
13080
|
+
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."
|
|
13081
|
+
};
|
|
13082
|
+
}
|
|
13083
|
+
}),
|
|
13084
|
+
tool({
|
|
13085
|
+
name: "ad_video_plan",
|
|
13086
|
+
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 (once made, the video goes on that post if it's still to go out on LinkedIn, Facebook or Instagram). 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.",
|
|
13087
|
+
parameters: {
|
|
13088
|
+
type: "object",
|
|
13089
|
+
properties: {
|
|
13090
|
+
brief: { type: "string", description: "What the ad should do, who it's for, the offer (user's words)" },
|
|
13091
|
+
item_id: { type: "integer", description: "A post (from social_list) to make the ad from" },
|
|
13092
|
+
aspect: { type: "string", enum: ["9:16", "16:9"] },
|
|
13093
|
+
seconds: { type: "integer", description: "15, 20 or 30" },
|
|
13094
|
+
scenes: { type: "integer", description: "2-5 (default 3, or 4 for 30 seconds)" },
|
|
13095
|
+
reference_url: { type: "string", description: "A public YouTube ad to follow the rhythm of" }
|
|
13096
|
+
}
|
|
13097
|
+
},
|
|
13098
|
+
group: "social",
|
|
13099
|
+
need: write5,
|
|
13100
|
+
risk: "W2",
|
|
13101
|
+
gates: ["VER", "LLM"],
|
|
13102
|
+
run: async (ctx, a) => {
|
|
13103
|
+
const out = await ws(ctx).planAdVideo({
|
|
13104
|
+
...a.brief ? { brief: String(a.brief).slice(0, 3e3) } : {},
|
|
13105
|
+
...a.item_id ? { item_id: Number(a.item_id) } : {},
|
|
13106
|
+
...a.aspect ? { aspect: a.aspect } : {},
|
|
13107
|
+
...a.seconds ? { seconds: Number(a.seconds) } : {},
|
|
13108
|
+
...a.scenes ? { scenes: Number(a.scenes) } : {},
|
|
13109
|
+
...a.reference_url ? { reference_url: String(a.reference_url).trim() } : {}
|
|
13110
|
+
});
|
|
13111
|
+
return adVideo(out);
|
|
13112
|
+
}
|
|
13113
|
+
}),
|
|
13114
|
+
tool({
|
|
13115
|
+
name: "ad_video_list",
|
|
13116
|
+
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.",
|
|
13117
|
+
parameters: none,
|
|
13118
|
+
group: "social",
|
|
13119
|
+
need: read5,
|
|
13120
|
+
risk: "R0",
|
|
13121
|
+
run: async (ctx) => {
|
|
13122
|
+
const out = await ws(ctx).adVideos();
|
|
13123
|
+
return {
|
|
13124
|
+
ad_videos: out.ad_videos.map((v) => ({
|
|
13125
|
+
id: v.id,
|
|
13126
|
+
status: v.status,
|
|
13127
|
+
step: v.step,
|
|
13128
|
+
title: v.plan?.title ?? null,
|
|
13129
|
+
aspect: v.aspect,
|
|
13130
|
+
cost_usd: v.cost_usd,
|
|
13131
|
+
video_id: v.video_id,
|
|
13132
|
+
video_status: v.video?.status ?? null,
|
|
13133
|
+
post_id: v.item?.id ?? null,
|
|
13134
|
+
on_post: v.item?.attached ?? false,
|
|
13135
|
+
error: v.error
|
|
13136
|
+
})),
|
|
13137
|
+
voices: out.voices
|
|
13138
|
+
};
|
|
13139
|
+
}
|
|
13140
|
+
}),
|
|
13141
|
+
tool({
|
|
13142
|
+
name: "ad_video_get",
|
|
13143
|
+
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.",
|
|
13144
|
+
parameters: {
|
|
13145
|
+
type: "object",
|
|
13146
|
+
properties: { id: { type: "integer", description: "The ad video (from ad_video_list)" } },
|
|
13147
|
+
required: ["id"]
|
|
13148
|
+
},
|
|
13149
|
+
group: "social",
|
|
13150
|
+
need: read5,
|
|
13151
|
+
risk: "R0",
|
|
13152
|
+
run: async (ctx, { id }) => adVideo(await ws(ctx).adVideo(Number(id)))
|
|
13153
|
+
}),
|
|
13154
|
+
tool({
|
|
13155
|
+
name: "ad_video_change",
|
|
13156
|
+
description: `Change an ad video's storyboard (beta) as the user asks, before making it (or after: only what changed is made again). A scene (scenes[]): its picture brief (still), movement (motion), the narrator's line, the words on screen (caption), its seconds (3-8), and timing inside it: caption_in / caption_out (when the words show and go, seconds into the scene) and voice_at (when the narrator starts); -1 puts a time back to its default; picture "ai" swaps the user's own photo back to an AI picture. add_scenes (still required; after: a scene id, else at the end); order: the scene ids to keep, in order (removes and reorders; 2-5 scenes); redo_scenes: make scenes' pictures and clips again. The whole video: style (the look; every AI picture and clip again), aspect 9:16 or 16:9 (every picture and clip again), seconds 15/20/30 (scenes rescaled), card_seconds (1.5-4); the end card (cta_headline, cta_sub); the voice (a name from ad_video_list voices, or none); the music (words, or none). redo says what's made again: a picture brief, the look or the shape mean new paid pictures and clips, a movement a new paid clip; lines, words, timing, order, end card, voice or music are cheap. Returns the new cost, the timeline and the script check (lines that don't fit their time, captions too fast to read, length); fix errors before ad_video_make. Not while it's being made. Preview first.`,
|
|
13157
|
+
parameters: {
|
|
13158
|
+
type: "object",
|
|
13159
|
+
properties: {
|
|
13160
|
+
id: { type: "integer", description: "The ad video" },
|
|
13161
|
+
scenes: {
|
|
13162
|
+
type: "array",
|
|
13163
|
+
items: {
|
|
13164
|
+
type: "object",
|
|
13165
|
+
properties: {
|
|
13166
|
+
id: { type: "string", description: "Scene id, e.g. s2" },
|
|
13167
|
+
still: { type: "string" },
|
|
13168
|
+
motion: { type: "string" },
|
|
13169
|
+
line: { type: "string" },
|
|
13170
|
+
caption: { type: "string" },
|
|
13171
|
+
seconds: { type: "number" },
|
|
13172
|
+
caption_in: { type: "number", description: "Words show at, seconds into the scene (-1: default)" },
|
|
13173
|
+
caption_out: { type: "number", description: "Words go at, seconds into the scene (-1: default)" },
|
|
13174
|
+
voice_at: { type: "number", description: "Narrator starts at, seconds into the scene (-1: default)" },
|
|
13175
|
+
picture: { type: "string", enum: ["ai"], description: "Back to an AI picture" }
|
|
13176
|
+
},
|
|
13177
|
+
required: ["id"]
|
|
13178
|
+
}
|
|
13179
|
+
},
|
|
13180
|
+
add_scenes: {
|
|
13181
|
+
type: "array",
|
|
13182
|
+
items: {
|
|
13183
|
+
type: "object",
|
|
13184
|
+
properties: {
|
|
13185
|
+
still: { type: "string", description: "The first frame's brief (no words in it)" },
|
|
13186
|
+
motion: { type: "string" },
|
|
13187
|
+
line: { type: "string" },
|
|
13188
|
+
caption: { type: "string" },
|
|
13189
|
+
seconds: { type: "number" },
|
|
13190
|
+
after: { type: "string", description: "Put it after this scene id" }
|
|
13191
|
+
},
|
|
13192
|
+
required: ["still"]
|
|
13193
|
+
}
|
|
13194
|
+
},
|
|
13195
|
+
order: { type: "array", items: { type: "string" }, description: "Scene ids to keep, in order" },
|
|
13196
|
+
redo_scenes: { type: "array", items: { type: "string" } },
|
|
13197
|
+
style: { type: "string", description: "The look of every scene, one sentence" },
|
|
13198
|
+
aspect: { type: "string", enum: ["9:16", "16:9"] },
|
|
13199
|
+
seconds: { type: "integer", description: "15, 20 or 30" },
|
|
13200
|
+
card_seconds: { type: "number", description: "The end card's seconds, 1.5-4" },
|
|
13201
|
+
cta_headline: { type: "string" },
|
|
13202
|
+
cta_sub: { type: "string" },
|
|
13203
|
+
voice: { type: "string", description: "A voice name, or none" },
|
|
13204
|
+
music: { type: "string", description: "The music in words, or none" },
|
|
13205
|
+
title: { type: "string" }
|
|
13206
|
+
},
|
|
13207
|
+
required: ["id"]
|
|
13208
|
+
},
|
|
13209
|
+
group: "social",
|
|
13210
|
+
need: write5,
|
|
13211
|
+
risk: "W1",
|
|
13212
|
+
gates: ["VER"],
|
|
13213
|
+
run: async (ctx, a) => {
|
|
13214
|
+
const cta = {
|
|
13215
|
+
...a.cta_headline != null ? { headline: a.cta_headline } : {},
|
|
13216
|
+
...a.cta_sub != null ? { sub: a.cta_sub } : {}
|
|
13217
|
+
};
|
|
13218
|
+
const out = await ws(ctx).changeAdVideo(Number(a.id), {
|
|
13219
|
+
...a.scenes?.length ? {
|
|
13220
|
+
scenes: a.scenes.map((sc) => {
|
|
13221
|
+
const out2 = { ...sc };
|
|
13222
|
+
for (const k of ["caption_in", "caption_out", "voice_at"]) {
|
|
13223
|
+
if (out2[k] != null && Number(out2[k]) < 0) out2[k] = null;
|
|
13224
|
+
}
|
|
13225
|
+
return out2;
|
|
13226
|
+
})
|
|
13227
|
+
} : {},
|
|
13228
|
+
...a.add_scenes?.length ? { add_scenes: a.add_scenes } : {},
|
|
13229
|
+
...a.order?.length ? { order: a.order.map(String) } : {},
|
|
13230
|
+
...a.redo_scenes?.length ? { redo_scenes: a.redo_scenes } : {},
|
|
13231
|
+
...a.style ? { style: a.style } : {},
|
|
13232
|
+
...a.aspect ? { aspect: a.aspect } : {},
|
|
13233
|
+
...a.seconds ? { seconds: Number(a.seconds) } : {},
|
|
13234
|
+
...a.card_seconds != null ? { card_seconds: Number(a.card_seconds) } : {},
|
|
13235
|
+
...Object.keys(cta).length ? { cta } : {},
|
|
13236
|
+
...a.voice != null ? { voice: /^none$/i.test(a.voice) ? false : a.voice } : {},
|
|
13237
|
+
...a.music != null ? { music: /^none$/i.test(a.music) ? "" : a.music } : {},
|
|
13238
|
+
...a.title ? { title: a.title } : {}
|
|
13239
|
+
});
|
|
13240
|
+
return { ...adVideo(out), redo: out.redo ?? [] };
|
|
13241
|
+
}
|
|
13242
|
+
}),
|
|
13243
|
+
tool({
|
|
13244
|
+
name: "ad_video_make",
|
|
13245
|
+
description: `Make the ad video (beta). stage "pictures" makes only the first frames (cheap: estimate.pictures_usd) so the user sees them (picture_url) and changes any before the paid clips; then make it again (stage all) for the rest. Stage all: 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 (or estimate.pictures_usd for stage pictures) 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. A script that doesn't fit its timing (script_check errors) is refused: fix it with ad_video_change, or go_ahead true only when the user says to make it anyway. 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.`,
|
|
13246
|
+
parameters: {
|
|
13247
|
+
type: "object",
|
|
13248
|
+
properties: {
|
|
13249
|
+
id: { type: "integer", description: "The ad video" },
|
|
13250
|
+
estimate_usd: { type: "number", description: "The cost the user agreed to (estimate.usd, or pictures_usd)" },
|
|
13251
|
+
stage: { type: "string", enum: ["all", "pictures"], description: "pictures: only the first frames" },
|
|
13252
|
+
go_ahead: { type: "boolean", description: "Make it although the script check found errors" }
|
|
13253
|
+
},
|
|
13254
|
+
required: ["id", "estimate_usd"]
|
|
13255
|
+
},
|
|
13256
|
+
group: "social",
|
|
13257
|
+
need: write5,
|
|
13258
|
+
risk: "W3",
|
|
13259
|
+
gates: ["VER"],
|
|
13260
|
+
run: async (ctx, { id, estimate_usd, stage, go_ahead }) => adVideo(
|
|
13261
|
+
await ws(ctx).makeAdVideo(Number(id), Number(estimate_usd), {
|
|
13262
|
+
...stage ? { stage } : {},
|
|
13263
|
+
...go_ahead ? { goAhead: true } : {}
|
|
13264
|
+
})
|
|
13265
|
+
)
|
|
13266
|
+
}),
|
|
13267
|
+
tool({
|
|
13268
|
+
name: "ad_video_picture",
|
|
13269
|
+
description: `Use the user's own photo as an ad video scene's first frame (beta): a picture already on CMO.so (picture_url: a post's picture from social_list / social_post_get). It is cropped to the video's shape; no AI picture, no cost for it; the scene's moving clip is made again. For a photo on their device, they use the Ad video screen in the CMO.so app. Back to an AI picture: ad_video_change scenes [{id, picture: "ai"}]. Not while it's being made. Preview first.`,
|
|
13270
|
+
parameters: {
|
|
13271
|
+
type: "object",
|
|
13272
|
+
properties: {
|
|
13273
|
+
id: { type: "integer", description: "The ad video" },
|
|
13274
|
+
scene: { type: "string", description: "Scene id, e.g. s2" },
|
|
13275
|
+
picture_url: { type: "string", description: "A picture on CMO.so (a post's picture)" }
|
|
13276
|
+
},
|
|
13277
|
+
required: ["id", "scene", "picture_url"]
|
|
13278
|
+
},
|
|
13279
|
+
group: "social",
|
|
13280
|
+
need: write5,
|
|
13281
|
+
risk: "W1",
|
|
13282
|
+
gates: ["VER"],
|
|
13283
|
+
run: async (ctx, a) => adVideo(
|
|
13284
|
+
await ws(ctx).adVideoScenePicture(Number(a.id), String(a.scene), { photo_url: String(a.picture_url).trim() })
|
|
13285
|
+
)
|
|
13286
|
+
}),
|
|
13287
|
+
tool({
|
|
13288
|
+
name: "ad_video_stop",
|
|
13289
|
+
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.",
|
|
13290
|
+
parameters: {
|
|
13291
|
+
type: "object",
|
|
13292
|
+
properties: { id: { type: "integer", description: "The ad video" } },
|
|
13293
|
+
required: ["id"]
|
|
13294
|
+
},
|
|
13295
|
+
group: "social",
|
|
13296
|
+
need: write5,
|
|
13297
|
+
risk: "W1",
|
|
13298
|
+
gates: ["VER"],
|
|
13299
|
+
run: async (ctx, { id }) => ws(ctx).stopAdVideo(Number(id))
|
|
13300
|
+
}),
|
|
12947
13301
|
tool({
|
|
12948
13302
|
name: "social_youtube_set",
|
|
12949
13303
|
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 +16970,7 @@ var program = new Command();
|
|
|
16616
16970
|
// package.json
|
|
16617
16971
|
var package_default = {
|
|
16618
16972
|
name: "@cmo-so/cli",
|
|
16619
|
-
version: "0.
|
|
16973
|
+
version: "0.11.0",
|
|
16620
16974
|
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
16975
|
type: "module",
|
|
16622
16976
|
license: "MIT",
|
|
@@ -16826,75 +17180,6 @@ import { randomBytes } from "node:crypto";
|
|
|
16826
17180
|
// ../../packages/mcp/src/index.ts
|
|
16827
17181
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
16828
17182
|
|
|
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
17183
|
// ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js
|
|
16899
17184
|
var util_exports = {};
|
|
16900
17185
|
__export(util_exports, {
|
|
@@ -26552,6 +26837,75 @@ var Server = class extends Protocol {
|
|
|
26552
26837
|
}
|
|
26553
26838
|
};
|
|
26554
26839
|
|
|
26840
|
+
// ../../packages/mcp/src/messages.ts
|
|
26841
|
+
var UPPER = /* @__PURE__ */ new Set(["seo", "gsc", "gbp", "cta", "cmo", "dns", "ai", "api", "pdf", "url", "id"]);
|
|
26842
|
+
var n = (count, word, plural) => count === 1 ? `1 ${word}` : `${count ? count : "no"} ${plural ?? `${word}s`}`;
|
|
26843
|
+
function titleOf(name) {
|
|
26844
|
+
const words = name.split("_").map((w) => UPPER.has(w) ? w.toUpperCase() : w);
|
|
26845
|
+
const head = words[0] ?? "";
|
|
26846
|
+
return [head.charAt(0).toUpperCase() + head.slice(1), ...words.slice(1)].join(" ");
|
|
26847
|
+
}
|
|
26848
|
+
var LIST_KEYS = ["items", "results", "posts", "ideas", "workspaces", "rows"];
|
|
26849
|
+
function items(value) {
|
|
26850
|
+
if (Array.isArray(value)) return value;
|
|
26851
|
+
if (value && typeof value === "object") {
|
|
26852
|
+
for (const k of LIST_KEYS) {
|
|
26853
|
+
const v = value[k];
|
|
26854
|
+
if (Array.isArray(v)) return v;
|
|
26855
|
+
}
|
|
26856
|
+
}
|
|
26857
|
+
return null;
|
|
26858
|
+
}
|
|
26859
|
+
function label(x) {
|
|
26860
|
+
if (x && typeof x === "object") {
|
|
26861
|
+
const o = x;
|
|
26862
|
+
for (const k of ["title", "name", "domain", "label", "what", "text", "id"]) {
|
|
26863
|
+
if (o[k] !== void 0 && o[k] !== null && o[k] !== "") return String(o[k]);
|
|
26864
|
+
}
|
|
26865
|
+
return Object.entries(o).filter(([, v]) => typeof v !== "object").slice(0, 3).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
26866
|
+
}
|
|
26867
|
+
return String(x);
|
|
26868
|
+
}
|
|
26869
|
+
function bodyLines(value) {
|
|
26870
|
+
const list2 = items(value);
|
|
26871
|
+
if (list2) {
|
|
26872
|
+
const out = list2.slice(0, 8).map((x) => `- ${label(x)}`.slice(0, 200));
|
|
26873
|
+
return list2.length > 8 ? [...out, `\u2026and ${list2.length - 8} more`] : out;
|
|
26874
|
+
}
|
|
26875
|
+
if (value && typeof value === "object") {
|
|
26876
|
+
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));
|
|
26877
|
+
}
|
|
26878
|
+
return value === null || value === void 0 || value === "" ? [] : [String(value).slice(0, 600)];
|
|
26879
|
+
}
|
|
26880
|
+
function said(name, value) {
|
|
26881
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
26882
|
+
for (const k of ["say", "headline", "summary", "message"]) {
|
|
26883
|
+
const v = value[k];
|
|
26884
|
+
if (typeof v === "string" && v.trim()) return (v.trim().split("\n")[0] ?? "").slice(0, 240);
|
|
26885
|
+
}
|
|
26886
|
+
}
|
|
26887
|
+
const list2 = items(value);
|
|
26888
|
+
if (list2) {
|
|
26889
|
+
let noun = name.endsWith("_list") ? name.slice(0, -5).split("_").pop() ?? "item" : "item";
|
|
26890
|
+
if (noun.endsWith("s")) noun = noun.slice(0, -1);
|
|
26891
|
+
return list2.length ? `I found ${n(list2.length, noun)}.` : "There's nothing there yet.";
|
|
26892
|
+
}
|
|
26893
|
+
if (typeof value === "string") return (value.trim().split("\n")[0] ?? "").slice(0, 240) || "Done.";
|
|
26894
|
+
return "Here it is.";
|
|
26895
|
+
}
|
|
26896
|
+
var lowerFirst = (s) => /^[A-Za-z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s;
|
|
26897
|
+
function message(name, value, opts = {}) {
|
|
26898
|
+
const title = titleOf(name);
|
|
26899
|
+
if (opts.error) return { say: `That didn't work: ${opts.error}`.slice(0, 300), title, lines: [] };
|
|
26900
|
+
if (opts.preview) return { say: "This needs your yes first; nothing has changed yet.", title, lines: [opts.preview] };
|
|
26901
|
+
if (opts.done) return { say: `Done: ${lowerFirst(opts.done)}`, title, lines: bodyLines(value) };
|
|
26902
|
+
return { say: said(name, value), title, lines: bodyLines(value) };
|
|
26903
|
+
}
|
|
26904
|
+
function messageText(m) {
|
|
26905
|
+
const body = [m.title ? `**${m.title}**` : "", ...m.lines].filter(Boolean).join("\n");
|
|
26906
|
+
return [m.say, body].filter(Boolean).join("\n\n").trim();
|
|
26907
|
+
}
|
|
26908
|
+
|
|
26555
26909
|
// ../../packages/mcp/src/prompts.ts
|
|
26556
26910
|
var workspaceArg = {
|
|
26557
26911
|
name: "workspace",
|
|
@@ -27708,7 +28062,18 @@ social.command("pictures <id> <urls...>").description("your own pictures on a po
|
|
|
27708
28062
|
async (opts, id, urls) => tool2(opts, "social_pictures_attach", { id: ids2([id])[0], urls, append: opts.append === true })
|
|
27709
28063
|
)
|
|
27710
28064
|
);
|
|
27711
|
-
social.command("
|
|
28065
|
+
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(
|
|
28066
|
+
act(
|
|
28067
|
+
async (opts, id, count) => tool2(opts, "social_image_new", {
|
|
28068
|
+
id: ids2([id])[0],
|
|
28069
|
+
count: Number(count ?? 7),
|
|
28070
|
+
...opts.style ? { style: opts.style } : {}
|
|
28071
|
+
})
|
|
28072
|
+
)
|
|
28073
|
+
);
|
|
28074
|
+
social.command("carousel <id>").description(
|
|
28075
|
+
"a post's carousel: --pdf for LinkedIn, Instagram, Facebook or TikTok; --format / --title for LinkedIn's"
|
|
28076
|
+
).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
28077
|
act(async (opts, id) => {
|
|
27713
28078
|
const post = ids2([id])[0];
|
|
27714
28079
|
if (opts.pdf)
|
|
@@ -27800,6 +28165,90 @@ video.command("add <url>").description("posts from a direct link to a video file
|
|
|
27800
28165
|
})
|
|
27801
28166
|
)
|
|
27802
28167
|
);
|
|
28168
|
+
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(
|
|
28169
|
+
act(
|
|
28170
|
+
async (opts, videoId) => tool2(opts, "social_video_add", {
|
|
28171
|
+
video_id: ids2([videoId])[0],
|
|
28172
|
+
channels: (channelsOpt(opts.channel) ?? ["youtube"]).filter((c) => c !== "google_business"),
|
|
28173
|
+
...opts.note ? { note: opts.note } : {}
|
|
28174
|
+
})
|
|
28175
|
+
)
|
|
28176
|
+
);
|
|
28177
|
+
var ad = video.command("ad").description("ad videos (beta): plan, change, make (billed), post");
|
|
28178
|
+
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(
|
|
28179
|
+
act(
|
|
28180
|
+
async (opts, brief2) => tool2(opts, "ad_video_plan", {
|
|
28181
|
+
...brief2?.length ? { brief: brief2.join(" ") } : {},
|
|
28182
|
+
...opts.fromPost ? { item_id: ids2([opts.fromPost])[0] } : {},
|
|
28183
|
+
seconds: Number(opts.seconds),
|
|
28184
|
+
aspect: opts.landscape ? "16:9" : "9:16",
|
|
28185
|
+
...opts.scenes ? { scenes: Number(opts.scenes) } : {},
|
|
28186
|
+
...opts.reference ? { reference_url: opts.reference } : {}
|
|
28187
|
+
})
|
|
28188
|
+
)
|
|
28189
|
+
);
|
|
28190
|
+
ad.command("list").description("this website's ad videos").action(act(async (opts) => tool2(opts, "ad_video_list")));
|
|
28191
|
+
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] })));
|
|
28192
|
+
ad.command("change <id>").description("change the storyboard (only what a change touches is made again)").option(
|
|
28193
|
+
"--scene <id=field:text...>",
|
|
28194
|
+
"e.g. s2=line:Book today (fields: still, motion, line, caption, seconds, caption_in, caption_out, voice_at; picture:ai)"
|
|
28195
|
+
).option(
|
|
28196
|
+
"--add <field:text...>",
|
|
28197
|
+
"a new scene, e.g. --add still:A wide shot of the shop --add line:Come in (after:s1)"
|
|
28198
|
+
).option("--order <sceneIds>", "the scenes to keep, in order (comma-separated): removes and reorders").option("--redo <sceneIds>", "make these scenes' pictures and clips again (comma-separated)").option("--style <text>", "the look of every scene (every AI picture again)").option("--shape <aspect>", "9:16 or 16:9 (every picture again)").option("--length <seconds>", "15, 20 or 30 (scenes rescaled)").option("--card-seconds <n>", "the end card's seconds (1.5-4)").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(
|
|
28199
|
+
act(
|
|
28200
|
+
async (opts, id) => {
|
|
28201
|
+
const scenes = {};
|
|
28202
|
+
for (const spec of opts.scene ?? []) {
|
|
28203
|
+
const m = /^([a-z0-9]+)=(still|motion|line|caption|seconds|caption_in|caption_out|voice_at|picture):(.*)$/s.exec(
|
|
28204
|
+
spec
|
|
28205
|
+
);
|
|
28206
|
+
if (!m) throw new Error(`--scene ${spec}: use <sceneId>=<field>:<text>, e.g. s2=line:Book today`);
|
|
28207
|
+
const [, sid = "", field = "", text3 = ""] = m;
|
|
28208
|
+
const numeric = ["seconds", "caption_in", "caption_out", "voice_at"].includes(field);
|
|
28209
|
+
scenes[sid] = { ...scenes[sid] ?? {}, [field]: numeric ? Number(text3) : text3 };
|
|
28210
|
+
}
|
|
28211
|
+
const added = {};
|
|
28212
|
+
for (const spec of opts.add ?? []) {
|
|
28213
|
+
const m = /^(still|motion|line|caption|seconds|after):(.*)$/s.exec(spec);
|
|
28214
|
+
if (!m) throw new Error(`--add ${spec}: use <field>:<text>, e.g. still:A wide shot of the shop`);
|
|
28215
|
+
const [, field = "", text3 = ""] = m;
|
|
28216
|
+
added[field] = field === "seconds" ? Number(text3) : text3;
|
|
28217
|
+
}
|
|
28218
|
+
return tool2(opts, "ad_video_change", {
|
|
28219
|
+
id: ids2([id])[0],
|
|
28220
|
+
...Object.keys(scenes).length ? { scenes: Object.entries(scenes).map(([sid, f]) => ({ id: sid, ...f })) } : {},
|
|
28221
|
+
...Object.keys(added).length ? { add_scenes: [added] } : {},
|
|
28222
|
+
...opts.order ? { order: opts.order.split(",").map((x) => x.trim()) } : {},
|
|
28223
|
+
...opts.redo ? { redo_scenes: opts.redo.split(",").map((x) => x.trim()) } : {},
|
|
28224
|
+
...opts.style ? { style: opts.style } : {},
|
|
28225
|
+
...opts.shape ? { aspect: opts.shape } : {},
|
|
28226
|
+
...opts.length ? { seconds: Number(opts.length) } : {},
|
|
28227
|
+
...opts.cardSeconds ? { card_seconds: Number(opts.cardSeconds) } : {},
|
|
28228
|
+
...opts.headline ? { cta_headline: opts.headline } : {},
|
|
28229
|
+
...opts.sub ? { cta_sub: opts.sub } : {},
|
|
28230
|
+
...opts.voice ? { voice: opts.voice } : {},
|
|
28231
|
+
...opts.music ? { music: opts.music } : {}
|
|
28232
|
+
});
|
|
28233
|
+
}
|
|
28234
|
+
)
|
|
28235
|
+
);
|
|
28236
|
+
ad.command("make <id> <usd>").description("make it: costs money; <usd> is the estimate you agreed to (from plan / show)").option("--pictures", "only the pictures first (<usd> = estimate.pictures_usd), to look at before the clips").option("--go-ahead", "make it although the script check found lines that don't fit").action(
|
|
28237
|
+
act(
|
|
28238
|
+
async (opts, id, usd) => tool2(opts, "ad_video_make", {
|
|
28239
|
+
id: ids2([id])[0],
|
|
28240
|
+
estimate_usd: Number(usd),
|
|
28241
|
+
...opts.pictures ? { stage: "pictures" } : {},
|
|
28242
|
+
...opts.goAhead ? { go_ahead: true } : {}
|
|
28243
|
+
})
|
|
28244
|
+
)
|
|
28245
|
+
);
|
|
28246
|
+
ad.command("picture <id> <scene> <url>").description("your own photo (a picture on CMO.so, e.g. a post's) as a scene's first frame").action(
|
|
28247
|
+
act(
|
|
28248
|
+
async (opts, id, scene, url) => tool2(opts, "ad_video_picture", { id: ids2([id])[0], scene, picture_url: url })
|
|
28249
|
+
)
|
|
28250
|
+
);
|
|
28251
|
+
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
28252
|
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
28253
|
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
28254
|
var tiktok = social.command("tiktok").description("your TikTok account (beta): short videos you approve one by one");
|
|
@@ -27819,6 +28268,15 @@ tiktok.command("add <url>").description("a TikTok post from a direct link to a v
|
|
|
27819
28268
|
})
|
|
27820
28269
|
)
|
|
27821
28270
|
);
|
|
28271
|
+
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(
|
|
28272
|
+
act(
|
|
28273
|
+
async (opts, id) => tool2(opts, "social_tiktok_photo", {
|
|
28274
|
+
id: ids2([id])[0],
|
|
28275
|
+
...opts.note ? { note: opts.note } : {},
|
|
28276
|
+
...opts.caption ? { caption: opts.caption } : {}
|
|
28277
|
+
})
|
|
28278
|
+
)
|
|
28279
|
+
);
|
|
27822
28280
|
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
28281
|
act(
|
|
27824
28282
|
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.11.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",
|