@cmo-so/mcp 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-mcp.mjs +448 -94
  2. package/package.json +1 -1
package/dist/cmo-mcp.mjs CHANGED
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
7199
7199
  // package.json
7200
7200
  var package_default = {
7201
7201
  name: "@cmo-so/cli",
7202
- version: "0.9.3",
7202
+ version: "0.11.0",
7203
7203
  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.",
7204
7204
  type: "module",
7205
7205
  license: "MIT",
@@ -7981,8 +7981,9 @@ function domainApi(api2, domain) {
7981
7981
  * https links to pictures. Send a few photos per call (16 MB request limit), `append` after the first.
7982
7982
  * On LinkedIn a set goes out as a swipeable PDF carousel unless setSocialCarousel says images. */
7983
7983
  setSocialPictures: (id, pictures) => post(`${base}/social/${id}/pictures`, pictures),
7984
- /** A LinkedIn post goes out as this PDF (base64 under ~11 MB, or a public https link; up to 300 pages);
7985
- * its first 20 pages become the post's pictures. */
7984
+ /** A PDF carousel (base64 under ~11 MB, or a public https link; up to 300 pages): its first 20 pages become
7985
+ * the post's pictures; a LinkedIn post (or the idea's LinkedIn post) goes out as the PDF itself. LinkedIn,
7986
+ * Instagram, Facebook and TikTok posts. */
7986
7987
  attachSocialCarouselPdf: (id, pdf) => post(`${base}/social/${id}/carousel/pdf`, pdf),
7987
7988
  /** How a LinkedIn post with a set goes out: "pdf" (swipeable carousel, the default) or "images"; its title. */
7988
7989
  setSocialCarousel: (id, changes) => api2.request(`${base}/social/${id}/carousel`, {
@@ -8132,6 +8133,35 @@ function domainApi(api2, domain) {
8132
8133
  /** A post on one video channel (YouTube by default, or TikTok) from a ready video, waiting in Review; Maggie
8133
8134
  * writes the details (YouTube) or the caption (TikTok) unless write is false. */
8134
8135
  postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
8136
+ /** A TikTok photo post (a swipeable set; TikTok adds music) from this post's pictures, waiting in Review;
8137
+ * Maggie writes the caption unless `caption` is given or write is false. Beta, like TikTok. */
8138
+ tiktokPhotoPost: (id, opts = {}) => post(`${base}/social/${id}/tiktok-photo`, opts),
8139
+ /** Ad videos (Beta): this website's, with the voices and lengths to choose from. */
8140
+ adVideos: () => api2.request(
8141
+ `${base}/social/ad-videos`
8142
+ ),
8143
+ adVideo: (adId) => api2.request(`${base}/social/ad-videos/${adId}`),
8144
+ /** A storyboard and its cost (one AI generation; nothing billed by the second yet). */
8145
+ planAdVideo: (opts) => post(`${base}/social/ad-videos`, opts),
8146
+ /** Change the storyboard: only what a change touches is made again (`redo`), with the new cost. */
8147
+ changeAdVideo: (adId, changes) => api2.request(`${base}/social/ad-videos/${adId}`, {
8148
+ method: "PATCH",
8149
+ headers: { "Content-Type": "application/json" },
8150
+ body: JSON.stringify(changes)
8151
+ }),
8152
+ /** Make it (billed): send back the estimate the person agreed to (estimate.pictures_usd for stage
8153
+ * "pictures": only the first frames, to look at before the paid clips). A script that doesn't fit its
8154
+ * timing is refused (409 script_check) unless goAhead. */
8155
+ makeAdVideo: (adId, estimateUsd, opts = {}) => post(`${base}/social/ad-videos/${adId}/make`, {
8156
+ confirm: true,
8157
+ estimate_usd: estimateUsd,
8158
+ ...opts.stage ? { stage: opts.stage } : {},
8159
+ ...opts.goAhead ? { go_ahead: true } : {}
8160
+ }),
8161
+ /** The person's own photo as a scene's first frame (base64, or photo_url: a picture already on CMO.so). */
8162
+ adVideoScenePicture: (adId, sceneId, photo) => post(`${base}/social/ad-videos/${adId}/scenes/${encodeURIComponent(sceneId)}/picture`, photo),
8163
+ /** Stop one being made (what's made is kept), or remove one. */
8164
+ stopAdVideo: (adId) => api2.request(`${base}/social/ad-videos/${adId}`, { method: "DELETE" }),
8135
8165
  /** Make a LinkedIn, Facebook or Instagram post a video post: it goes out as this video (its picture is dropped). */
8136
8166
  attachVideo: (id, videoId) => post(`${base}/social/${id}/video`, { video_id: videoId }),
8137
8167
  /** Take the video off a post: it's a text post again. */
@@ -8539,6 +8569,12 @@ var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
8539
8569
  var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
8540
8570
  (c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
8541
8571
  );
8572
+ var videoFrom = (a, lang = "en") => {
8573
+ const name = a.file?.file_name;
8574
+ if (a.file) return name ? q(String(name)) : lang === "en" ? "your video" : "\u4F60\u7684\u5F71\u7247";
8575
+ if (a.video_id) return lang === "en" ? `video #${a.video_id}` : `\u5F71\u7247 #${a.video_id}`;
8576
+ return String(a.url ?? "");
8577
+ };
8542
8578
  var confirmCopy = {
8543
8579
  // --------------------------------------------------------- account / setup
8544
8580
  website_add: {
@@ -8813,8 +8849,8 @@ var confirmCopy = {
8813
8849
  "zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
8814
8850
  },
8815
8851
  social_video_attach: {
8816
- en: (a) => `Make post #${a.id} a video post with ${a.url} (its picture is removed).`,
8817
- "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`
8852
+ en: (a) => `Make post #${a.id} a video post with ${videoFrom(a)} (its picture is removed).`,
8853
+ "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`
8818
8854
  },
8819
8855
  social_video_detach: {
8820
8856
  en: (a) => `Take the video off post #${a.id} (it's a text post again).`,
@@ -8827,11 +8863,11 @@ var confirmCopy = {
8827
8863
  social_video_add: {
8828
8864
  en: (a) => {
8829
8865
  const names = videoChannels(a).join(" and ");
8830
- return `Make a ${names} post from ${a.url}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
8866
+ return `Make a ${names} post from ${videoFrom(a)}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
8831
8867
  },
8832
8868
  "zh-Hant": (a) => {
8833
8869
  const names = videoChannels(a).join(" \u548C ");
8834
- 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`;
8870
+ 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`;
8835
8871
  }
8836
8872
  },
8837
8873
  social_tiktok_set: {
@@ -8847,8 +8883,8 @@ var confirmCopy = {
8847
8883
  "zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
8848
8884
  },
8849
8885
  social_image_new: {
8850
- 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" : ""}.`,
8851
- "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`
8886
+ 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" : ""}.`,
8887
+ "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`
8852
8888
  },
8853
8889
  social_image_stop: {
8854
8890
  en: () => "Stop the picture being made; the post keeps the one it had.",
@@ -8865,8 +8901,32 @@ var confirmCopy = {
8865
8901
  }
8866
8902
  },
8867
8903
  social_carousel_pdf_attach: {
8868
- 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).`,
8869
- "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`
8904
+ 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).`,
8905
+ "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`
8906
+ },
8907
+ social_tiktok_photo: {
8908
+ 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.`,
8909
+ "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`
8910
+ },
8911
+ ad_video_plan: {
8912
+ 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.`,
8913
+ "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`
8914
+ },
8915
+ ad_video_change: {
8916
+ en: (a) => `Change ad video #${a.id}'s storyboard: ${keys(a).filter((k) => k !== "id").join(", ") || "nothing"}.`,
8917
+ "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`
8918
+ },
8919
+ ad_video_make: {
8920
+ 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." : ""}`,
8921
+ "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" : ""}`
8922
+ },
8923
+ ad_video_picture: {
8924
+ en: (a) => `Use this picture as the first frame of scene ${a.scene} in ad video #${a.id} (its clip is made again).`,
8925
+ "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`
8926
+ },
8927
+ ad_video_stop: {
8928
+ en: (a) => `Stop ad video #${a.id} if it's being made (what's made is kept), or remove it from the list.`,
8929
+ "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`
8870
8930
  },
8871
8931
  social_carousel_set: {
8872
8932
  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)}` : ""}.`,
@@ -9106,75 +9166,6 @@ function costText(cost, lang = "en") {
9106
9166
  return parts.join(" ");
9107
9167
  }
9108
9168
 
9109
- // ../../packages/mcp/src/messages.ts
9110
- var UPPER = /* @__PURE__ */ new Set(["seo", "gsc", "gbp", "cta", "cmo", "dns", "ai", "api", "pdf", "url", "id"]);
9111
- var n = (count, word, plural) => count === 1 ? `1 ${word}` : `${count ? count : "no"} ${plural ?? `${word}s`}`;
9112
- function titleOf(name) {
9113
- const words = name.split("_").map((w) => UPPER.has(w) ? w.toUpperCase() : w);
9114
- const head = words[0] ?? "";
9115
- return [head.charAt(0).toUpperCase() + head.slice(1), ...words.slice(1)].join(" ");
9116
- }
9117
- var LIST_KEYS = ["items", "results", "posts", "ideas", "workspaces", "rows"];
9118
- function items(value) {
9119
- if (Array.isArray(value)) return value;
9120
- if (value && typeof value === "object") {
9121
- for (const k of LIST_KEYS) {
9122
- const v = value[k];
9123
- if (Array.isArray(v)) return v;
9124
- }
9125
- }
9126
- return null;
9127
- }
9128
- function label(x) {
9129
- if (x && typeof x === "object") {
9130
- const o = x;
9131
- for (const k of ["title", "name", "domain", "label", "what", "text", "id"]) {
9132
- if (o[k] !== void 0 && o[k] !== null && o[k] !== "") return String(o[k]);
9133
- }
9134
- return Object.entries(o).filter(([, v]) => typeof v !== "object").slice(0, 3).map(([k, v]) => `${k} ${v}`).join(", ");
9135
- }
9136
- return String(x);
9137
- }
9138
- function bodyLines(value) {
9139
- const list2 = items(value);
9140
- if (list2) {
9141
- const out = list2.slice(0, 8).map((x) => `- ${label(x)}`.slice(0, 200));
9142
- return list2.length > 8 ? [...out, `\u2026and ${list2.length - 8} more`] : out;
9143
- }
9144
- if (value && typeof value === "object") {
9145
- 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));
9146
- }
9147
- return value === null || value === void 0 || value === "" ? [] : [String(value).slice(0, 600)];
9148
- }
9149
- function said(name, value) {
9150
- if (value && typeof value === "object" && !Array.isArray(value)) {
9151
- for (const k of ["say", "headline", "summary", "message"]) {
9152
- const v = value[k];
9153
- if (typeof v === "string" && v.trim()) return (v.trim().split("\n")[0] ?? "").slice(0, 240);
9154
- }
9155
- }
9156
- const list2 = items(value);
9157
- if (list2) {
9158
- let noun = name.endsWith("_list") ? name.slice(0, -5).split("_").pop() ?? "item" : "item";
9159
- if (noun.endsWith("s")) noun = noun.slice(0, -1);
9160
- return list2.length ? `I found ${n(list2.length, noun)}.` : "There's nothing there yet.";
9161
- }
9162
- if (typeof value === "string") return (value.trim().split("\n")[0] ?? "").slice(0, 240) || "Done.";
9163
- return "Here it is.";
9164
- }
9165
- var lowerFirst = (s) => /^[A-Za-z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s;
9166
- function message(name, value, opts = {}) {
9167
- const title = titleOf(name);
9168
- if (opts.error) return { say: `That didn't work: ${opts.error}`.slice(0, 300), title, lines: [] };
9169
- if (opts.preview) return { say: "This needs your yes first; nothing has changed yet.", title, lines: [opts.preview] };
9170
- if (opts.done) return { say: `Done: ${lowerFirst(opts.done)}`, title, lines: bodyLines(value) };
9171
- return { say: said(name, value), title, lines: bodyLines(value) };
9172
- }
9173
- function messageText(m) {
9174
- const body = [m.title ? `**${m.title}**` : "", ...m.lines].filter(Boolean).join("\n");
9175
- return [m.say, body].filter(Boolean).join("\n\n").trim();
9176
- }
9177
-
9178
9169
  // ../../packages/tools/src/lib.ts
9179
9170
  var none = { type: "object", properties: {} };
9180
9171
  function ws(ctx) {
@@ -11695,6 +11686,44 @@ var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
11695
11686
  var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
11696
11687
  var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
11697
11688
  var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
11689
+ var SLIDES = [5, 6, 7, 8, 9, 10];
11690
+ var adVideo = (v) => ({
11691
+ id: v.id,
11692
+ status: v.status,
11693
+ step: v.step,
11694
+ aspect: v.aspect,
11695
+ plan: v.plan ? {
11696
+ title: v.plan.title,
11697
+ style: v.plan.style,
11698
+ voice: v.plan.voice,
11699
+ music: v.plan.music,
11700
+ seconds: v.plan.seconds,
11701
+ end_card: { ...v.plan.cta, seconds: v.plan.card_seconds ?? 2.5 },
11702
+ scenes: v.plan.scenes.map((s) => ({
11703
+ id: s.id,
11704
+ seconds: s.seconds,
11705
+ picture: s.still,
11706
+ motion: s.motion,
11707
+ line: s.line,
11708
+ caption: s.caption,
11709
+ picture_url: s.still_url ?? null,
11710
+ own_picture: s.own_picture ?? false,
11711
+ ...s.caption_in != null ? { caption_in: s.caption_in } : {},
11712
+ ...s.caption_out != null ? { caption_out: s.caption_out } : {},
11713
+ ...s.voice_at != null ? { voice_at: s.voice_at } : {}
11714
+ }))
11715
+ } : null,
11716
+ timeline: v.timeline ?? null,
11717
+ script_check: v.script_check ? { ok: v.script_check.ok, total: v.script_check.total, issues: v.script_check.issues } : null,
11718
+ pictures_ready: v.pictures_ready ?? false,
11719
+ estimate: v.estimate,
11720
+ cost_usd: v.cost_usd,
11721
+ video_id: v.video_id,
11722
+ post: v.item ?? null,
11723
+ video: v.video ? { status: v.video.status, preview_url: v.video.preview_url, poster_url: v.video.poster_url } : null,
11724
+ error: v.error,
11725
+ 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." : ""
11726
+ });
11698
11727
  var channelsParam = {
11699
11728
  type: "array",
11700
11729
  items: { type: "string", enum: CHANNEL_KEYS },
@@ -11711,6 +11740,11 @@ var FILE_SCHEMA = {
11711
11740
  required: ["download_url", "file_id"]
11712
11741
  };
11713
11742
  var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
11743
+ var videoLink = (a) => {
11744
+ const link = fileLinks(a.file)[0] ?? String(a.url ?? "").trim();
11745
+ if (!link.startsWith("https://")) throw new Error("Give the video (a file from this chat, or a direct https link).");
11746
+ return link;
11747
+ };
11714
11748
  var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11715
11749
  var channelsOf = (value) => {
11716
11750
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
@@ -11909,7 +11943,7 @@ var socialTools = [
11909
11943
  }),
11910
11944
  tool({
11911
11945
  name: "social_image_new",
11912
- 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.",
11946
+ 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).",
11913
11947
  parameters: {
11914
11948
  type: "object",
11915
11949
  properties: {
@@ -11926,7 +11960,7 @@ var socialTools = [
11926
11960
  },
11927
11961
  count: {
11928
11962
  type: "integer",
11929
- description: "1 (default) or 4: a set of 4 pictures telling one story, a line of words on each"
11963
+ 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)"
11930
11964
  },
11931
11965
  text_effect: {
11932
11966
  type: "string",
@@ -11948,7 +11982,7 @@ var socialTools = [
11948
11982
  replace: replace === true,
11949
11983
  ...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
11950
11984
  ...mode === "edit" ? { mode: "edit" } : {},
11951
- ...Number(count) === 4 && mode !== "edit" ? { count: 4 } : {},
11985
+ ...mode !== "edit" && (Number(count) === 4 || SLIDES.includes(Number(count))) ? { count: Number(count) } : {},
11952
11986
  ...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
11953
11987
  ...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
11954
11988
  });
@@ -12174,11 +12208,14 @@ var socialTools = [
12174
12208
  }),
12175
12209
  tool({
12176
12210
  name: "social_carousel_pdf_attach",
12177
- 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.",
12211
+ 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.",
12178
12212
  parameters: {
12179
12213
  type: "object",
12180
12214
  properties: {
12181
- id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
12215
+ id: {
12216
+ type: "integer",
12217
+ description: "A LinkedIn, Instagram, Facebook or TikTok post (from social_list), not gone out yet"
12218
+ },
12182
12219
  file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
12183
12220
  url: { type: "string", description: "Public https link to the PDF file" },
12184
12221
  title: {
@@ -12192,7 +12229,7 @@ var socialTools = [
12192
12229
  group: "social",
12193
12230
  need: write5,
12194
12231
  risk: "W1",
12195
- gates: ["VER", "LI"],
12232
+ gates: ["VER"],
12196
12233
  run: async (ctx, { id, url, file, title }) => {
12197
12234
  const link = fileLinks(file)[0] ?? String(url ?? "").trim();
12198
12235
  if (!link.startsWith("https://"))
@@ -12221,7 +12258,7 @@ var socialTools = [
12221
12258
  id: post.id,
12222
12259
  channel: post.provider ?? null,
12223
12260
  url: `https://app.cmo.so/social?picture=${post.id}${site}`,
12224
- 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)."
12261
+ 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)."
12225
12262
  };
12226
12263
  }
12227
12264
  }),
@@ -12919,22 +12956,24 @@ var socialTools = [
12919
12956
  }),
12920
12957
  tool({
12921
12958
  name: "social_video_attach",
12922
- 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.",
12959
+ 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.",
12923
12960
  parameters: {
12924
12961
  type: "object",
12925
12962
  properties: {
12926
12963
  id: { type: "integer", description: "The post (from social_list)" },
12964
+ file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
12927
12965
  url: { type: "string", description: "Direct https link to the video file" }
12928
12966
  },
12929
- required: ["id", "url"]
12967
+ required: ["id"]
12930
12968
  },
12969
+ fileParams: ["file"],
12931
12970
  group: "social",
12932
12971
  need: write5,
12933
12972
  risk: "W1",
12934
12973
  gates: ["VER"],
12935
12974
  run: async (ctx, a) => {
12936
12975
  const d = ws(ctx);
12937
- let v = await d.videoFromLink(String(a.url));
12976
+ let v = await d.videoFromLink(videoLink(a));
12938
12977
  const until = Date.now() + 15e4;
12939
12978
  while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
12940
12979
  await new Promise((r) => setTimeout(r, 3e3));
@@ -13010,11 +13049,13 @@ var socialTools = [
13010
13049
  }),
13011
13050
  tool({
13012
13051
  name: "social_video_add",
13013
- 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.",
13052
+ 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.",
13014
13053
  parameters: {
13015
13054
  type: "object",
13016
13055
  properties: {
13056
+ file: { ...FILE_SCHEMA, description: "The video from this chat (MP4 or MOV)" },
13017
13057
  url: { type: "string", description: "Direct https link to the video file" },
13058
+ video_id: { type: "integer", description: "A video already in CMO.so (an ad video's video_id)" },
13018
13059
  channels: {
13019
13060
  type: "array",
13020
13061
  items: { type: "string", enum: [...VIDEO_KEYS] },
@@ -13026,9 +13067,9 @@ var socialTools = [
13026
13067
  start: { type: "number", description: "Start of the part to use, seconds" },
13027
13068
  end: { type: "number", description: "End of the part to use, seconds" },
13028
13069
  vertical: { type: "string", enum: ["left", "middle", "right"] }
13029
- },
13030
- required: ["url"]
13070
+ }
13031
13071
  },
13072
+ fileParams: ["file"],
13032
13073
  group: "social",
13033
13074
  need: write5,
13034
13075
  risk: "W2",
@@ -13040,7 +13081,7 @@ var socialTools = [
13040
13081
  ...a.end != null ? { end: Number(a.end) } : {},
13041
13082
  ...a.vertical ? { aspect: "9:16", crop_x: a.vertical === "left" ? 0 : a.vertical === "right" ? 1 : 0.5 } : {}
13042
13083
  };
13043
- let v = await d.videoFromLink(String(a.url), Object.keys(edit).length ? edit : void 0);
13084
+ let v = a.video_id ? await d.video(Number(a.video_id)) : await d.videoFromLink(videoLink(a), Object.keys(edit).length ? edit : void 0);
13044
13085
  const until = Date.now() + 15e4;
13045
13086
  while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
13046
13087
  await new Promise((r) => setTimeout(r, 3e3));
@@ -13073,6 +13114,250 @@ var socialTools = [
13073
13114
  };
13074
13115
  }
13075
13116
  }),
13117
+ tool({
13118
+ name: "social_tiktok_photo",
13119
+ 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.",
13120
+ parameters: {
13121
+ type: "object",
13122
+ properties: {
13123
+ id: { type: "integer", description: "A post with pictures (from social_list), not a video post" },
13124
+ note: { type: "string", description: "What to say in the caption, in the user's words" },
13125
+ caption: { type: "string", description: "The caption itself, when the user wrote it (no AI)" }
13126
+ },
13127
+ required: ["id"]
13128
+ },
13129
+ group: "social",
13130
+ need: write5,
13131
+ risk: "W2",
13132
+ gates: ["VER", "LLM"],
13133
+ run: async (ctx, { id, note, caption }) => {
13134
+ const out = await ws(ctx).tiktokPhotoPost(Number(id), {
13135
+ ...note ? { note: String(note).slice(0, 1e3) } : {},
13136
+ ...typeof caption === "string" && caption.trim() ? { caption: caption.trim(), write: false } : {}
13137
+ });
13138
+ return {
13139
+ post: item(out),
13140
+ 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."
13141
+ };
13142
+ }
13143
+ }),
13144
+ tool({
13145
+ name: "ad_video_plan",
13146
+ 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.",
13147
+ parameters: {
13148
+ type: "object",
13149
+ properties: {
13150
+ brief: { type: "string", description: "What the ad should do, who it's for, the offer (user's words)" },
13151
+ item_id: { type: "integer", description: "A post (from social_list) to make the ad from" },
13152
+ aspect: { type: "string", enum: ["9:16", "16:9"] },
13153
+ seconds: { type: "integer", description: "15, 20 or 30" },
13154
+ scenes: { type: "integer", description: "2-5 (default 3, or 4 for 30 seconds)" },
13155
+ reference_url: { type: "string", description: "A public YouTube ad to follow the rhythm of" }
13156
+ }
13157
+ },
13158
+ group: "social",
13159
+ need: write5,
13160
+ risk: "W2",
13161
+ gates: ["VER", "LLM"],
13162
+ run: async (ctx, a) => {
13163
+ const out = await ws(ctx).planAdVideo({
13164
+ ...a.brief ? { brief: String(a.brief).slice(0, 3e3) } : {},
13165
+ ...a.item_id ? { item_id: Number(a.item_id) } : {},
13166
+ ...a.aspect ? { aspect: a.aspect } : {},
13167
+ ...a.seconds ? { seconds: Number(a.seconds) } : {},
13168
+ ...a.scenes ? { scenes: Number(a.scenes) } : {},
13169
+ ...a.reference_url ? { reference_url: String(a.reference_url).trim() } : {}
13170
+ });
13171
+ return adVideo(out);
13172
+ }
13173
+ }),
13174
+ tool({
13175
+ name: "ad_video_list",
13176
+ 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.",
13177
+ parameters: none,
13178
+ group: "social",
13179
+ need: read5,
13180
+ risk: "R0",
13181
+ run: async (ctx) => {
13182
+ const out = await ws(ctx).adVideos();
13183
+ return {
13184
+ ad_videos: out.ad_videos.map((v) => ({
13185
+ id: v.id,
13186
+ status: v.status,
13187
+ step: v.step,
13188
+ title: v.plan?.title ?? null,
13189
+ aspect: v.aspect,
13190
+ cost_usd: v.cost_usd,
13191
+ video_id: v.video_id,
13192
+ video_status: v.video?.status ?? null,
13193
+ post_id: v.item?.id ?? null,
13194
+ on_post: v.item?.attached ?? false,
13195
+ error: v.error
13196
+ })),
13197
+ voices: out.voices
13198
+ };
13199
+ }
13200
+ }),
13201
+ tool({
13202
+ name: "ad_video_get",
13203
+ 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.",
13204
+ parameters: {
13205
+ type: "object",
13206
+ properties: { id: { type: "integer", description: "The ad video (from ad_video_list)" } },
13207
+ required: ["id"]
13208
+ },
13209
+ group: "social",
13210
+ need: read5,
13211
+ risk: "R0",
13212
+ run: async (ctx, { id }) => adVideo(await ws(ctx).adVideo(Number(id)))
13213
+ }),
13214
+ tool({
13215
+ name: "ad_video_change",
13216
+ 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.`,
13217
+ parameters: {
13218
+ type: "object",
13219
+ properties: {
13220
+ id: { type: "integer", description: "The ad video" },
13221
+ scenes: {
13222
+ type: "array",
13223
+ items: {
13224
+ type: "object",
13225
+ properties: {
13226
+ id: { type: "string", description: "Scene id, e.g. s2" },
13227
+ still: { type: "string" },
13228
+ motion: { type: "string" },
13229
+ line: { type: "string" },
13230
+ caption: { type: "string" },
13231
+ seconds: { type: "number" },
13232
+ caption_in: { type: "number", description: "Words show at, seconds into the scene (-1: default)" },
13233
+ caption_out: { type: "number", description: "Words go at, seconds into the scene (-1: default)" },
13234
+ voice_at: { type: "number", description: "Narrator starts at, seconds into the scene (-1: default)" },
13235
+ picture: { type: "string", enum: ["ai"], description: "Back to an AI picture" }
13236
+ },
13237
+ required: ["id"]
13238
+ }
13239
+ },
13240
+ add_scenes: {
13241
+ type: "array",
13242
+ items: {
13243
+ type: "object",
13244
+ properties: {
13245
+ still: { type: "string", description: "The first frame's brief (no words in it)" },
13246
+ motion: { type: "string" },
13247
+ line: { type: "string" },
13248
+ caption: { type: "string" },
13249
+ seconds: { type: "number" },
13250
+ after: { type: "string", description: "Put it after this scene id" }
13251
+ },
13252
+ required: ["still"]
13253
+ }
13254
+ },
13255
+ order: { type: "array", items: { type: "string" }, description: "Scene ids to keep, in order" },
13256
+ redo_scenes: { type: "array", items: { type: "string" } },
13257
+ style: { type: "string", description: "The look of every scene, one sentence" },
13258
+ aspect: { type: "string", enum: ["9:16", "16:9"] },
13259
+ seconds: { type: "integer", description: "15, 20 or 30" },
13260
+ card_seconds: { type: "number", description: "The end card's seconds, 1.5-4" },
13261
+ cta_headline: { type: "string" },
13262
+ cta_sub: { type: "string" },
13263
+ voice: { type: "string", description: "A voice name, or none" },
13264
+ music: { type: "string", description: "The music in words, or none" },
13265
+ title: { type: "string" }
13266
+ },
13267
+ required: ["id"]
13268
+ },
13269
+ group: "social",
13270
+ need: write5,
13271
+ risk: "W1",
13272
+ gates: ["VER"],
13273
+ run: async (ctx, a) => {
13274
+ const cta = {
13275
+ ...a.cta_headline != null ? { headline: a.cta_headline } : {},
13276
+ ...a.cta_sub != null ? { sub: a.cta_sub } : {}
13277
+ };
13278
+ const out = await ws(ctx).changeAdVideo(Number(a.id), {
13279
+ ...a.scenes?.length ? {
13280
+ scenes: a.scenes.map((sc) => {
13281
+ const out2 = { ...sc };
13282
+ for (const k of ["caption_in", "caption_out", "voice_at"]) {
13283
+ if (out2[k] != null && Number(out2[k]) < 0) out2[k] = null;
13284
+ }
13285
+ return out2;
13286
+ })
13287
+ } : {},
13288
+ ...a.add_scenes?.length ? { add_scenes: a.add_scenes } : {},
13289
+ ...a.order?.length ? { order: a.order.map(String) } : {},
13290
+ ...a.redo_scenes?.length ? { redo_scenes: a.redo_scenes } : {},
13291
+ ...a.style ? { style: a.style } : {},
13292
+ ...a.aspect ? { aspect: a.aspect } : {},
13293
+ ...a.seconds ? { seconds: Number(a.seconds) } : {},
13294
+ ...a.card_seconds != null ? { card_seconds: Number(a.card_seconds) } : {},
13295
+ ...Object.keys(cta).length ? { cta } : {},
13296
+ ...a.voice != null ? { voice: /^none$/i.test(a.voice) ? false : a.voice } : {},
13297
+ ...a.music != null ? { music: /^none$/i.test(a.music) ? "" : a.music } : {},
13298
+ ...a.title ? { title: a.title } : {}
13299
+ });
13300
+ return { ...adVideo(out), redo: out.redo ?? [] };
13301
+ }
13302
+ }),
13303
+ tool({
13304
+ name: "ad_video_make",
13305
+ 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.`,
13306
+ parameters: {
13307
+ type: "object",
13308
+ properties: {
13309
+ id: { type: "integer", description: "The ad video" },
13310
+ estimate_usd: { type: "number", description: "The cost the user agreed to (estimate.usd, or pictures_usd)" },
13311
+ stage: { type: "string", enum: ["all", "pictures"], description: "pictures: only the first frames" },
13312
+ go_ahead: { type: "boolean", description: "Make it although the script check found errors" }
13313
+ },
13314
+ required: ["id", "estimate_usd"]
13315
+ },
13316
+ group: "social",
13317
+ need: write5,
13318
+ risk: "W3",
13319
+ gates: ["VER"],
13320
+ run: async (ctx, { id, estimate_usd, stage, go_ahead }) => adVideo(
13321
+ await ws(ctx).makeAdVideo(Number(id), Number(estimate_usd), {
13322
+ ...stage ? { stage } : {},
13323
+ ...go_ahead ? { goAhead: true } : {}
13324
+ })
13325
+ )
13326
+ }),
13327
+ tool({
13328
+ name: "ad_video_picture",
13329
+ 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.`,
13330
+ parameters: {
13331
+ type: "object",
13332
+ properties: {
13333
+ id: { type: "integer", description: "The ad video" },
13334
+ scene: { type: "string", description: "Scene id, e.g. s2" },
13335
+ picture_url: { type: "string", description: "A picture on CMO.so (a post's picture)" }
13336
+ },
13337
+ required: ["id", "scene", "picture_url"]
13338
+ },
13339
+ group: "social",
13340
+ need: write5,
13341
+ risk: "W1",
13342
+ gates: ["VER"],
13343
+ run: async (ctx, a) => adVideo(
13344
+ await ws(ctx).adVideoScenePicture(Number(a.id), String(a.scene), { photo_url: String(a.picture_url).trim() })
13345
+ )
13346
+ }),
13347
+ tool({
13348
+ name: "ad_video_stop",
13349
+ 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.",
13350
+ parameters: {
13351
+ type: "object",
13352
+ properties: { id: { type: "integer", description: "The ad video" } },
13353
+ required: ["id"]
13354
+ },
13355
+ group: "social",
13356
+ need: write5,
13357
+ risk: "W1",
13358
+ gates: ["VER"],
13359
+ run: async (ctx, { id }) => ws(ctx).stopAdVideo(Number(id))
13360
+ }),
13076
13361
  tool({
13077
13362
  name: "social_youtube_set",
13078
13363
  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.",
@@ -23031,6 +23316,75 @@ var Server = class extends Protocol {
23031
23316
  }
23032
23317
  };
23033
23318
 
23319
+ // ../../packages/mcp/src/messages.ts
23320
+ var UPPER = /* @__PURE__ */ new Set(["seo", "gsc", "gbp", "cta", "cmo", "dns", "ai", "api", "pdf", "url", "id"]);
23321
+ var n = (count, word, plural) => count === 1 ? `1 ${word}` : `${count ? count : "no"} ${plural ?? `${word}s`}`;
23322
+ function titleOf(name) {
23323
+ const words = name.split("_").map((w) => UPPER.has(w) ? w.toUpperCase() : w);
23324
+ const head = words[0] ?? "";
23325
+ return [head.charAt(0).toUpperCase() + head.slice(1), ...words.slice(1)].join(" ");
23326
+ }
23327
+ var LIST_KEYS = ["items", "results", "posts", "ideas", "workspaces", "rows"];
23328
+ function items(value) {
23329
+ if (Array.isArray(value)) return value;
23330
+ if (value && typeof value === "object") {
23331
+ for (const k of LIST_KEYS) {
23332
+ const v = value[k];
23333
+ if (Array.isArray(v)) return v;
23334
+ }
23335
+ }
23336
+ return null;
23337
+ }
23338
+ function label(x) {
23339
+ if (x && typeof x === "object") {
23340
+ const o = x;
23341
+ for (const k of ["title", "name", "domain", "label", "what", "text", "id"]) {
23342
+ if (o[k] !== void 0 && o[k] !== null && o[k] !== "") return String(o[k]);
23343
+ }
23344
+ return Object.entries(o).filter(([, v]) => typeof v !== "object").slice(0, 3).map(([k, v]) => `${k} ${v}`).join(", ");
23345
+ }
23346
+ return String(x);
23347
+ }
23348
+ function bodyLines(value) {
23349
+ const list2 = items(value);
23350
+ if (list2) {
23351
+ const out = list2.slice(0, 8).map((x) => `- ${label(x)}`.slice(0, 200));
23352
+ return list2.length > 8 ? [...out, `\u2026and ${list2.length - 8} more`] : out;
23353
+ }
23354
+ if (value && typeof value === "object") {
23355
+ 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));
23356
+ }
23357
+ return value === null || value === void 0 || value === "" ? [] : [String(value).slice(0, 600)];
23358
+ }
23359
+ function said(name, value) {
23360
+ if (value && typeof value === "object" && !Array.isArray(value)) {
23361
+ for (const k of ["say", "headline", "summary", "message"]) {
23362
+ const v = value[k];
23363
+ if (typeof v === "string" && v.trim()) return (v.trim().split("\n")[0] ?? "").slice(0, 240);
23364
+ }
23365
+ }
23366
+ const list2 = items(value);
23367
+ if (list2) {
23368
+ let noun = name.endsWith("_list") ? name.slice(0, -5).split("_").pop() ?? "item" : "item";
23369
+ if (noun.endsWith("s")) noun = noun.slice(0, -1);
23370
+ return list2.length ? `I found ${n(list2.length, noun)}.` : "There's nothing there yet.";
23371
+ }
23372
+ if (typeof value === "string") return (value.trim().split("\n")[0] ?? "").slice(0, 240) || "Done.";
23373
+ return "Here it is.";
23374
+ }
23375
+ var lowerFirst = (s) => /^[A-Za-z][a-z]/.test(s) ? s.charAt(0).toLowerCase() + s.slice(1) : s;
23376
+ function message(name, value, opts = {}) {
23377
+ const title = titleOf(name);
23378
+ if (opts.error) return { say: `That didn't work: ${opts.error}`.slice(0, 300), title, lines: [] };
23379
+ if (opts.preview) return { say: "This needs your yes first; nothing has changed yet.", title, lines: [opts.preview] };
23380
+ if (opts.done) return { say: `Done: ${lowerFirst(opts.done)}`, title, lines: bodyLines(value) };
23381
+ return { say: said(name, value), title, lines: bodyLines(value) };
23382
+ }
23383
+ function messageText(m) {
23384
+ const body = [m.title ? `**${m.title}**` : "", ...m.lines].filter(Boolean).join("\n");
23385
+ return [m.say, body].filter(Boolean).join("\n\n").trim();
23386
+ }
23387
+
23034
23388
  // ../../packages/mcp/src/prompts.ts
23035
23389
  var workspaceArg = {
23036
23390
  name: "workspace",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.9.3",
3
+ "version": "0.11.0",
4
4
  "description": "CMO.so's MCP server for Claude Desktop, Cursor, VS Code and other MCP clients: manage your websites, blog posts, LinkedIn queue and results with Maggie.",
5
5
  "type": "module",
6
6
  "license": "MIT",