@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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +433 -95
  2. 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 LinkedIn post goes out as this PDF (base64 under ~11 MB, or a public https link; up to 300 pages);
7925
- * its first 20 pages become the post's pictures. */
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.url} (its picture is removed).`,
8757
- "zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u6539\u6210\u5F71\u7247\u8CBC\u6587\uFF0C\u7528 ${a.url}\uFF08\u5716\u7247\u6703\u79FB\u9664\uFF09\u3002`
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.url}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
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.url} \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`;
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) => `LinkedIn post #${a.id} goes out as your PDF, a swipeable carousel${a.title ? ` titled ${q(a.title)}` : ""}; its first pages become the post's pictures (free).`,
8809
- "zh-Hant": (a) => `LinkedIn \u8CBC\u6587 #${a.id} \u6703\u4EE5\u4F60\u7684 PDF \u767C\u51FA\uFF0C\u6210\u70BA\u53EF\u6ED1\u52D5\u7684\u8F2A\u64AD${a.title ? `\uFF0C\u6A19\u984C ${q(a.title)}` : ""}\uFF1BPDF \u7684\u524D\u5E7E\u9801\u4E5F\u6703\u6210\u70BA\u8CBC\u6587\u7684\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002`
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) or 4: a set of 4 pictures telling one story, a line of words on each"
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 && mode !== "edit" ? { 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 LinkedIn post goes out as this PDF: a swipeable document carousel. The PDF from this chat (file: one the user added or that was made here) or a public https link to it (url); up to 300 pages and 20 MB, no password. Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
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: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
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", "LI"],
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: post.provider === "linkedin" ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
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, 5-30 seconds; never a platform page) with its text, and its picture is removed (a video post has no picture). Not for Google Business (posts can't carry video). Preview first.",
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", "url"]
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(String(a.url));
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; tell the user to upload the video in the app instead). 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.",
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.videoFromLink(String(a.url), Object.keys(edit).length ? edit : void 0);
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.9.3",
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("carousel <id>").description("a LinkedIn post with a set of pictures: a swipeable PDF carousel or several pictures").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: it goes out as it is").action(
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.9.3",
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",