@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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +553 -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,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.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`
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.url}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
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.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`;
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) => `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`
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) or 4: a set of 4 pictures telling one story, a line of words on each"
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 && mode !== "edit" ? { 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 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.",
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: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
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", "LI"],
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: 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)."
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, 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.",
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", "url"]
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(String(a.url));
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; 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.",
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.videoFromLink(String(a.url), Object.keys(edit).length ? edit : void 0);
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.9.3",
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("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(
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.9.3",
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",