@cmo-so/cli 0.8.1 → 0.9.1

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 +366 -66
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -7609,8 +7609,11 @@ function domainApi(api2, domain) {
7609
7609
  }),
7610
7610
  /** This website's topics, for assigning an idea to one. */
7611
7611
  topics: () => api2.request(`${base}/topics`),
7612
- /** Add a waiting idea to the end of the plan. */
7613
- planIdea: (id) => post(`${base}/ideas/${encodeURIComponent(id)}/plan`),
7612
+ /** Add a waiting idea to the plan: at the end, or at `position` (1-based in the answer). */
7613
+ planIdea: (id, position) => post(
7614
+ `${base}/ideas/${encodeURIComponent(id)}/plan`,
7615
+ position != null ? { position } : void 0
7616
+ ),
7614
7617
  /** One step up or down the 1-10 priority the planner sorts ideas by. */
7615
7618
  ideaPriority: (id, delta) => post(`${base}/ideas/${encodeURIComponent(id)}/priority`, { delta }),
7616
7619
  /** Take a post out of the plan; its idea can be restored to Ideas. */
@@ -7707,8 +7710,8 @@ function domainApi(api2, domain) {
7707
7710
  ...source ? { sitemap_url: source } : {}
7708
7711
  }),
7709
7712
  discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
7710
- /** OK a draft (or retry a failed post): it goes to the front of the queue; the post comes back with its eta. */
7711
- approveSocial: (id) => post(`${base}/social/${id}/approve`),
7713
+ /** OK a draft (or retry a failed post): it goes to the front of the queue, or to `position`; the post comes back with its eta. */
7714
+ approveSocial: (id, position) => post(`${base}/social/${id}/approve`, position != null ? { position } : void 0),
7712
7715
  skipSocial: (id) => post(`${base}/social/${id}/skip`),
7713
7716
  /** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
7714
7717
  /** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
@@ -7784,9 +7787,15 @@ function domainApi(api2, domain) {
7784
7787
  /** "Looks good, start": setup done, and Maggie writes the first week's posts. */
7785
7788
  socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
7786
7789
  /** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
7787
- approveAllSocial: (channel) => post(
7788
- `${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
7789
- ),
7790
+ approveAllSocial: (channel, position) => {
7791
+ const q2 = new URLSearchParams();
7792
+ if (channel) q2.set("channel", channel);
7793
+ if (position != null) q2.set("position", String(position));
7794
+ const qs = q2.toString();
7795
+ return post(
7796
+ `${base}/social/approve-all${qs ? `?${qs}` : ""}`
7797
+ );
7798
+ },
7790
7799
  /** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
7791
7800
  connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
7792
7801
  /** The Facebook Pages you connected, and the one this website posts to. */
@@ -7902,6 +7911,19 @@ function domainApi(api2, domain) {
7902
7911
  photo,
7903
7912
  ...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
7904
7913
  }),
7914
+ /** Your own set of pictures for the post, in order (up to 20; free, no AI): base64 photos and/or public
7915
+ * https links to pictures. Send a few photos per call (16 MB request limit), `append` after the first.
7916
+ * On LinkedIn a set goes out as a swipeable PDF carousel unless setSocialCarousel says images. */
7917
+ setSocialPictures: (id, pictures) => post(`${base}/social/${id}/pictures`, pictures),
7918
+ /** A LinkedIn post goes out as this PDF (base64 under ~11 MB, or a public https link; up to 300 pages);
7919
+ * its first 20 pages become the post's pictures. */
7920
+ attachSocialCarouselPdf: (id, pdf) => post(`${base}/social/${id}/carousel/pdf`, pdf),
7921
+ /** How a LinkedIn post with a set goes out: "pdf" (swipeable carousel, the default) or "images"; its title. */
7922
+ setSocialCarousel: (id, changes) => api2.request(`${base}/social/${id}/carousel`, {
7923
+ method: "PATCH",
7924
+ headers: { "Content-Type": "application/json" },
7925
+ body: JSON.stringify(changes)
7926
+ }),
7905
7927
  /** A new picture for every post here without one (or these), one after another in the background. */
7906
7928
  fillSocialImages: (opts = {}) => post(
7907
7929
  `${base}/social/images/fill`,
@@ -7935,20 +7957,24 @@ function domainApi(api2, domain) {
7935
7957
  background: true,
7936
7958
  ...channels ? { channels } : {}
7937
7959
  }),
7938
- startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
7960
+ /** `position`: where the posts are queued when approval mode is auto (the front by default). */
7961
+ startSocialFromPost: (sourcePostId, channels, position) => post(`${base}/social/from-post`, {
7939
7962
  source_post_id: sourcePostId,
7940
7963
  background: true,
7941
- ...channels ? { channels } : {}
7964
+ ...channels ? { channels } : {},
7965
+ ...position != null ? { position } : {}
7942
7966
  }),
7943
- startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
7967
+ startSocialPlan: (days, timezone, channels, position) => post(`${base}/social/plan`, {
7944
7968
  days,
7945
7969
  timezone,
7946
7970
  background: true,
7947
- ...channels ? { channels } : {}
7971
+ ...channels ? { channels } : {},
7972
+ ...position != null ? { position } : {}
7948
7973
  }),
7949
- startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
7974
+ startWriteSocialIdea: (id, channels, position) => post(`${base}/social/ideas/${id}/write`, {
7950
7975
  background: true,
7951
- ...channels ? { channels } : {}
7976
+ ...channels ? { channels } : {},
7977
+ ...position != null ? { position } : {}
7952
7978
  }),
7953
7979
  /** Disconnect the website's LinkedIn account. */
7954
7980
  disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
@@ -8121,10 +8147,12 @@ function domainApi(api2, domain) {
8121
8147
  }),
8122
8148
  // docs/19 P3: autopilot cadence and publish-now.
8123
8149
  autopilot: () => api2.request(`${base}/autopilot`),
8124
- /** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
8125
- setPostsPerDay: (postsPerDay) => put(`${base}/autopilot/cadence`, {
8126
- posts_per_day: postsPerDay
8127
- }),
8150
+ /** Posts per day (one of `choices`, up to the plan's limit). Pushed blogs: how many
8151
+ * AI CMO sends a day. Pull sites: the most the site collects a day; null = the plan's. */
8152
+ setPostsPerDay: (postsPerDay) => put(
8153
+ `${base}/autopilot/cadence`,
8154
+ { posts_per_day: postsPerDay }
8155
+ ),
8128
8156
  /** Publish a ready draft to the connected site now (one post from today's quota). */
8129
8157
  publishNow: (id) => post(
8130
8158
  `${base}/posts/${encodeURIComponent(id)}/publish`
@@ -8409,6 +8437,14 @@ var CHANNEL_EN = {
8409
8437
  google_business: "Google Business"
8410
8438
  };
8411
8439
  var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
8440
+ var channelsEn = (a) => {
8441
+ const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_EN[String(c)] ?? String(c));
8442
+ return list2.length ? list2.join(list2.length === 2 ? " and " : ", ") : null;
8443
+ };
8444
+ var channelsZh = (a) => {
8445
+ const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_ZH[String(c)] ?? String(c));
8446
+ return list2.length ? list2.join("\u3001") : null;
8447
+ };
8412
8448
  var named = (title, id) => {
8413
8449
  if (title != null && String(title).trim()) return ` ${q(title)}`;
8414
8450
  const s = String(id ?? "").trim();
@@ -8418,6 +8454,20 @@ var list = (xs, n = 5) => {
8418
8454
  const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
8419
8455
  return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
8420
8456
  };
8457
+ var placeEn = (p, items = "posts") => {
8458
+ const t = String(p ?? "").trim().toLowerCase();
8459
+ if (t === "end") return `at the end of the queue, after the ${items} already waiting`;
8460
+ if (t === "random") return "at a random place in the queue";
8461
+ if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `after the first ${t} in the queue`;
8462
+ return null;
8463
+ };
8464
+ var placeZh = (p) => {
8465
+ const t = String(p ?? "").trim().toLowerCase();
8466
+ if (t === "end") return "\u6392\u5728\u968A\u5217\u6700\u5F8C\uFF0C\u5728\u5DF2\u5728\u7B49\u5F85\u7684\u4E4B\u5F8C";
8467
+ if (t === "random") return "\u96A8\u6A5F\u6392\u9032\u968A\u5217";
8468
+ if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `\u6392\u5728\u968A\u5217\u524D ${t} \u7BC7\u4E4B\u5F8C`;
8469
+ return null;
8470
+ };
8421
8471
  var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
8422
8472
  var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
8423
8473
  var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
@@ -8577,8 +8627,16 @@ var confirmCopy = {
8577
8627
  "zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
8578
8628
  },
8579
8629
  idea_plan: {
8580
- en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
8581
- "zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
8630
+ en: (a) => {
8631
+ const t = String(a.position ?? "").trim().toLowerCase();
8632
+ const where = t === "front" || t === "0" ? "the front of the plan" : t === "random" ? "a random place in the plan" : /^\d+$/.test(t) ? `the plan, after the first ${t} planned posts` : "the end of the plan";
8633
+ return `Add${named(a.title, a.id)} to ${where}.`;
8634
+ },
8635
+ "zh-Hant": (a) => {
8636
+ const t = String(a.position ?? "").trim().toLowerCase();
8637
+ const where = t === "front" || t === "0" ? "\u8A08\u5283\u6700\u524D\u9762" : t === "random" ? "\u8A08\u5283\u4E2D\u7684\u96A8\u6A5F\u4F4D\u7F6E" : /^\d+$/.test(t) ? `\u8A08\u5283\u4E2D\u524D ${t} \u7BC7\u4E4B\u5F8C` : "\u8A08\u5283\u6700\u5F8C";
8638
+ return `\u628A${named(a.title, a.id)} \u52A0\u5230${where}\u3002`;
8639
+ }
8582
8640
  },
8583
8641
  idea_discard: {
8584
8642
  en: (a) => `Remove the idea${named(a.title, a.id)}.`,
@@ -8648,23 +8706,29 @@ var confirmCopy = {
8648
8706
  },
8649
8707
  // ----------------------------------------------------------------- social
8650
8708
  social_update_copy: {
8651
- en: (a) => `Change the text of LinkedIn post #${a.id}.`,
8652
- "zh-Hant": (a) => `\u4FEE\u6539 LinkedIn \u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
8709
+ en: (a) => `Change the text of post #${a.id}.`,
8710
+ "zh-Hant": (a) => `\u4FEE\u6539\u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
8653
8711
  },
8654
8712
  social_approve: {
8655
8713
  en: (a) => {
8656
8714
  const n = ids(a);
8657
- return n.length > 1 ? `Approve ${n.length} LinkedIn posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes out at your next posting times, where your network can see it.` : `Approve LinkedIn post #${n[0]}. It goes out at your next posting time, where your network can see it.`;
8715
+ const at = placeEn(a.position);
8716
+ if (at)
8717
+ return `Approve ${n.length > 1 ? `${n.length} posts: ${n.map((i) => `#${i}`).join(", ")}` : `post #${n[0]}`}. ${n.length > 1 ? "They are" : "It is"} queued ${at}, and go${n.length > 1 ? "" : "es"} out where your network can see ${n.length > 1 ? "them" : "it"}.`;
8718
+ return n.length > 1 ? `Approve ${n.length} posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes out at your next posting times, where your network can see it.` : `Approve post #${n[0]}. It goes out at your next posting time, where your network can see it.`;
8658
8719
  },
8659
8720
  "zh-Hant": (a) => {
8660
8721
  const n = ids(a);
8661
- return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7 LinkedIn \u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6 LinkedIn \u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
8722
+ const at = placeZh(a.position);
8723
+ if (at)
8724
+ return `\u6279\u51C6${n.length > 1 ? ` ${n.length} \u7BC7\u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}` : `\u8CBC\u6587 #${n[0]}`}\u3002${at}\uFF0C\u767C\u51FA\u5F8C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
8725
+ return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7\u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6\u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
8662
8726
  }
8663
8727
  },
8664
- social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
8728
+ social_skip: { en: (a) => `Skip post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E\u8CBC\u6587 #${a.id}\u3002` },
8665
8729
  social_restore: {
8666
- en: (a) => `Bring back LinkedIn post #${a.id}.`,
8667
- "zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
8730
+ en: (a) => `Bring back post #${a.id}.`,
8731
+ "zh-Hant": (a) => `\u6062\u5FA9\u8CBC\u6587 #${a.id}\u3002`
8668
8732
  },
8669
8733
  youtube_connect_link: {
8670
8734
  en: () => "Make a link to connect your YouTube channel.",
@@ -8705,28 +8769,52 @@ var confirmCopy = {
8705
8769
  "zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
8706
8770
  },
8707
8771
  social_image_new: {
8708
- en: (a) => `Make a new picture for LinkedIn 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" : ""}.`,
8709
- "zh-Hant": (a) => `\u70BA LinkedIn \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`
8772
+ 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" : ""}.`,
8773
+ "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`
8710
8774
  },
8711
8775
  social_image_stop: {
8712
- en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
8713
- "zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
8776
+ en: () => "Stop the picture being made; the post keeps the one it had.",
8777
+ "zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684\u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
8778
+ },
8779
+ social_pictures_attach: {
8780
+ en: (a) => {
8781
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8782
+ return `${a.append ? "Add" : "Put"} your ${n} picture${n === 1 ? "" : "s"} ${a.append ? "after the pictures on" : "on"} post #${a.id}${a.append ? "" : ", in place of its picture"} (free). On LinkedIn a set goes out as a swipeable PDF carousel.`;
8783
+ },
8784
+ "zh-Hant": (a) => {
8785
+ const n = (Array.isArray(a.urls) ? a.urls.length : 0) + (Array.isArray(a.files) ? a.files.length : 0);
8786
+ return `${a.append ? `\u5728\u8CBC\u6587 #${a.id} \u73FE\u6709\u5716\u7247\u5F8C\u52A0\u4E0A` : `\u628A\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u63DB\u6210`}\u4F60\u7684 ${n} \u5F35\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002\u5728 LinkedIn \u4E0A\uFF0C\u4E00\u7D44\u5716\u7247\u6703\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA\u3002`;
8787
+ }
8788
+ },
8789
+ social_carousel_pdf_attach: {
8790
+ 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).`,
8791
+ "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`
8792
+ },
8793
+ social_carousel_set: {
8794
+ 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)}` : ""}.`,
8795
+ "zh-Hant": (a) => `LinkedIn \u8CBC\u6587 #${a.id} ${a.format === "images" ? "\u4EE5\u591A\u5F35\u5716\u7247\u767C\u51FA" : a.format === "pdf" ? "\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA" : "\u7167\u76EE\u524D\u8A2D\u5B9A\u767C\u51FA"}${a.title ? `\uFF0C\u6A19\u984C ${q(a.title)}` : ""}\u3002`
8714
8796
  },
8715
8797
  social_move: {
8716
- en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
8717
- "zh-Hant": (a) => `\u628A LinkedIn \u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\u5373\u5C07\u767C\u51FA\u7684\u6700\u524D" : a.to === "up" ? "\u4E0A\u79FB" : "\u4E0B\u79FB"}\u3002`
8798
+ en: (a) => `Move post #${a.id} ${a.to === "top" ? "to the top of its channel's queue" : `${a.to} in its channel's queue`}.`,
8799
+ "zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\u8A72\u6E20\u9053\u968A\u5217\u7684\u6700\u524D" : a.to === "up" ? "\u5728\u8A72\u6E20\u9053\u968A\u5217\u4E2D\u4E0A\u79FB" : "\u5728\u8A72\u6E20\u9053\u968A\u5217\u4E2D\u4E0B\u79FB"}\u3002`
8718
8800
  },
8719
8801
  social_set_paused: {
8720
- en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
8721
- "zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
8802
+ en: (a) => {
8803
+ const on2 = a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} posting` : "posting on every channel";
8804
+ return a.paused ? `Pause ${on2}.` : `Resume ${on2}.`;
8805
+ },
8806
+ "zh-Hant": (a) => {
8807
+ const on2 = a.channel ? `${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\u6240\u6709\u6E20\u9053\u7684";
8808
+ return a.paused ? `\u66AB\u505C${on2}\u767C\u6587\u3002` : `\u6062\u5FA9${on2}\u767C\u6587\u3002`;
8809
+ }
8722
8810
  },
8723
8811
  social_from_article: {
8724
- en: (a) => `Write a LinkedIn post about ${q(a.title || a.source_id)}; it waits for your OK unless your LinkedIn posts go out on their own.`,
8725
- "zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} \u5BEB\u4E00\u7BC7 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
8812
+ en: (a) => `Write ${channelsEn(a) ? `a ${channelsEn(a)} post` : "a post for each connected channel"} about ${q(a.title || a.source_id)}; it waits for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then it is queued ${placeEn(a.position)})` : ""}.`,
8813
+ "zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} ${channelsZh(a) ? `\u5BEB\u4E00\u7BC7 ${channelsZh(a)} \u8CBC\u6587` : "\u70BA\u6BCF\u500B\u5DF2\u9023\u63A5\u7684\u6E20\u9053\u5404\u5BEB\u4E00\u7BC7\u8CBC\u6587"}\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
8726
8814
  },
8727
8815
  social_plan_days: {
8728
- en: (a) => `Write LinkedIn posts for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles; they wait for your OK unless your LinkedIn posts go out on their own.`,
8729
- "zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684 LinkedIn \u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
8816
+ en: (a) => `Write ${channelsEn(a) ? `${channelsEn(a)} posts` : "posts on each connected channel"} for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles; they wait for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then they are queued ${placeEn(a.position)})` : ""}.`,
8817
+ "zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684${channelsZh(a) ? ` ${channelsZh(a)} ` : "\u5404\u6E20\u9053"}\u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
8730
8818
  },
8731
8819
  social_idea_add: {
8732
8820
  en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
@@ -8741,8 +8829,8 @@ var confirmCopy = {
8741
8829
  "zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
8742
8830
  },
8743
8831
  social_idea_write: {
8744
- en: (a) => `Write the LinkedIn post for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}; your own idea's post waits for your OK unless your LinkedIn posts go out on their own.`,
8745
- "zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB LinkedIn \u8CBC\u6587\uFF1B\u4F60\u81EA\u5DF1 idea \u7684\u8CBC\u6587\uFF0C\u9664\u975E\u4F60\u8A2D\u5B9A\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6\u3002`
8832
+ en: (a) => `Write ${channelsEn(a) ? `the ${channelsEn(a)} post` : "a post for each connected channel"} for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}; your own idea's posts wait for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then it is queued ${placeEn(a.position)})` : ""}.`,
8833
+ "zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB${channelsZh(a) ? ` ${channelsZh(a)} ` : "\u5404\u6E20\u9053\u7684"}\u8CBC\u6587\uFF1B\u4F60\u81EA\u5DF1 idea \u7684\u8CBC\u6587\uFF0C\u9664\u975E\u4F60\u8A2D\u5B9A\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
8746
8834
  },
8747
8835
  social_idea_save: {
8748
8836
  en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
@@ -8808,8 +8896,8 @@ var confirmCopy = {
8808
8896
  "zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
8809
8897
  },
8810
8898
  social_approve_all: {
8811
- en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times, where people can see them.`,
8812
- "zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA\uFF0C\u5927\u5BB6\u90FD\u770B\u5F97\u5230\u3002`
8899
+ en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times${placeEn(a.position) ? `, queued ${placeEn(a.position)}` : ""}, where people can see them.`,
8900
+ "zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA${placeZh(a.position) ? `\uFF08${placeZh(a.position)}\uFF09` : ""}\uFF0C\u5927\u5BB6\u90FD\u770B\u5F97\u5230\u3002`
8813
8901
  },
8814
8902
  series_create: {
8815
8903
  en: (a) => `Plan and write a LinkedIn series about ${q(a.brief)}: ${a.count || 3} posts that read as a series. Each waits in Review for your OK. Uses one AI generation a post.`,
@@ -8964,6 +9052,18 @@ var int = (v, min, max, fallback) => {
8964
9052
  const n = Math.trunc(Number(v));
8965
9053
  return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
8966
9054
  };
9055
+ var positionParam = (what, fallback) => ({
9056
+ type: "string",
9057
+ description: `Where ${what} go: "front", "end", "random", or a number of items to go after ("0" = front; more than there are = end). ${fallback} Scheduled or automated runs should use "end" so they don't jump ahead of what is already waiting.`
9058
+ });
9059
+ var positionOf = (value) => {
9060
+ if (value == null || value === "") return void 0;
9061
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
9062
+ const text3 = String(value).trim().toLowerCase();
9063
+ if (text3 === "front" || text3 === "end" || text3 === "random") return text3;
9064
+ if (/^\d+$/.test(text3)) return Number(text3);
9065
+ throw new Error('position must be "front", "end", "random" or a number of items to go after (0 = front).');
9066
+ };
8967
9067
  function tool(t) {
8968
9068
  const out = { ...t };
8969
9069
  if (t.risk !== "R0" && !t.confirm)
@@ -9658,19 +9758,20 @@ var blogTools = [
9658
9758
  }),
9659
9759
  tool({
9660
9760
  name: "idea_plan",
9661
- description: "Add a waiting idea to the end of the plan (the plan holds up to 100).",
9761
+ description: "Add a waiting idea to the plan (the plan holds up to 100): at the end, or where `position` says (front, random, after N planned posts). The answer's position counts from 1.",
9662
9762
  parameters: {
9663
9763
  type: "object",
9664
9764
  properties: {
9665
9765
  id: { type: "string" },
9666
- title: { type: "string", description: "As listed (shown on the approval card and in History)" }
9766
+ title: { type: "string", description: "As listed (shown on the approval card and in History)" },
9767
+ position: positionParam("the idea", "Leave out for the end of the plan.")
9667
9768
  },
9668
9769
  required: ["id", "title"]
9669
9770
  },
9670
9771
  group: "blog",
9671
9772
  need: write,
9672
9773
  risk: "W1",
9673
- run: async (ctx, { id }) => ws(ctx).planIdea(id)
9774
+ run: async (ctx, { id, position }) => ws(ctx).planIdea(id, positionOf(position))
9674
9775
  }),
9675
9776
  tool({
9676
9777
  name: "idea_discard",
@@ -10860,6 +10961,8 @@ var opsTools = [
10860
10961
  characters: String(p.social_copy ?? "").length,
10861
10962
  picture: p.image_url ?? null,
10862
10963
  pictures: p.image_urls ?? [],
10964
+ // LinkedIn with a set (or an uploaded PDF): a PDF carousel or several pictures.
10965
+ carousel: p.carousel ?? null,
10863
10966
  link: p.cta_url ?? p.source_url ?? null,
10864
10967
  held: p.held ?? null,
10865
10968
  failure: p.failure_message ?? null
@@ -10887,7 +10990,7 @@ var publishingTools = [
10887
10990
  }),
10888
10991
  tool({
10889
10992
  name: "posts_per_day_set",
10890
- description: "How many blog posts a day AI CMO publishes to the website (1, 2, 12, 24, 48 or 96, up to the plan's limit). Only for sites AI CMO pushes to; plugin / API sites fetch on their own schedule.",
10993
+ description: "Blog posts a day for the website (1, 2, 12, 24, 48 or 96, up to the plan's limit). Sites AI CMO pushes to: how many it publishes a day. Plugin / API sites: the most the site collects a day (it still decides how often it checks).",
10891
10994
  parameters: {
10892
10995
  type: "object",
10893
10996
  properties: {
@@ -11450,6 +11553,18 @@ var channelsParam = {
11450
11553
  items: { type: "string", enum: CHANNEL_KEYS },
11451
11554
  description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
11452
11555
  };
11556
+ var FILE_SCHEMA = {
11557
+ type: "object",
11558
+ properties: {
11559
+ download_url: { type: "string" },
11560
+ file_id: { type: "string" },
11561
+ mime_type: { type: "string" },
11562
+ file_name: { type: "string" }
11563
+ },
11564
+ required: ["download_url", "file_id"]
11565
+ };
11566
+ var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
11567
+ var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11453
11568
  var channelsOf = (value) => {
11454
11569
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
11455
11570
  return list2.length ? list2 : void 0;
@@ -11542,24 +11657,28 @@ var socialTools = [
11542
11657
  }),
11543
11658
  tool({
11544
11659
  name: "social_approve",
11545
- description: "Approve LinkedIn post drafts: each goes to the front of the user's queue and out at the next posting times. Pass every id to approve in one call.",
11660
+ description: "Approve post drafts: each goes to the front of the user's queue and out at the next posting times, or where `position` says (end, random, after N posts). Pass every id to approve in one call.",
11546
11661
  parameters: {
11547
11662
  type: "object",
11548
- properties: { ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" } },
11663
+ properties: {
11664
+ ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
11665
+ position: queuedAt
11666
+ },
11549
11667
  required: ["ids"]
11550
11668
  },
11551
11669
  group: "social",
11552
11670
  need: write5,
11553
11671
  risk: "W3",
11554
11672
  gates: ["VER", "LI"],
11555
- run: async (ctx, { ids: ids3, id }) => {
11673
+ run: async (ctx, { ids: ids3, id, position }) => {
11556
11674
  const list2 = (Array.isArray(ids3) && ids3.length ? ids3 : id != null ? [id] : []).map(Number).slice(0, 20);
11557
11675
  if (!list2.length) throw new Error("Which post?");
11676
+ const at = positionOf(position);
11558
11677
  const api2 = ws(ctx);
11559
11678
  const results = [];
11560
11679
  for (const n of list2) {
11561
11680
  try {
11562
- await api2.approveSocial(n);
11681
+ await api2.approveSocial(n, at);
11563
11682
  results.push({ id: n, ok: true });
11564
11683
  } catch (e) {
11565
11684
  results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -11812,6 +11931,125 @@ var socialTools = [
11812
11931
  return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
11813
11932
  }
11814
11933
  }),
11934
+ tool({
11935
+ name: "social_pictures_attach",
11936
+ description: "Put the user's own pictures on a post, in order: pictures from this chat (files: ones the user added or that were made here) or public https links to image files (urls), JPEG, PNG or WebP, for example carousel slides. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. If you can pass neither, give the user social_upload_link. Preview first.",
11937
+ parameters: {
11938
+ type: "object",
11939
+ properties: {
11940
+ id: { type: "integer", description: "The post (from social_list), not gone out yet" },
11941
+ files: {
11942
+ type: "array",
11943
+ items: FILE_SCHEMA,
11944
+ description: "Pictures from this chat, in the order they should appear"
11945
+ },
11946
+ urls: {
11947
+ type: "array",
11948
+ items: { type: "string" },
11949
+ description: "Public https links to the image files, in order (after the files, when both are given)"
11950
+ },
11951
+ append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11952
+ },
11953
+ required: ["id"]
11954
+ },
11955
+ fileParams: ["files"],
11956
+ group: "social",
11957
+ need: write5,
11958
+ risk: "W1",
11959
+ gates: ["VER"],
11960
+ run: async (ctx, { id, urls, files, append }) => {
11961
+ const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
11962
+ Boolean
11963
+ );
11964
+ if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
11965
+ if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
11966
+ if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
11967
+ const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
11968
+ return {
11969
+ id: out.id,
11970
+ pictures: out.image_urls?.length ? out.image_urls : [out.image_url],
11971
+ carousel: out.carousel ?? null
11972
+ };
11973
+ }
11974
+ }),
11975
+ tool({
11976
+ name: "social_carousel_pdf_attach",
11977
+ 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.",
11978
+ parameters: {
11979
+ type: "object",
11980
+ properties: {
11981
+ id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
11982
+ file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
11983
+ url: { type: "string", description: "Public https link to the PDF file" },
11984
+ title: {
11985
+ type: "string",
11986
+ description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
11987
+ }
11988
+ },
11989
+ required: ["id"]
11990
+ },
11991
+ fileParams: ["file"],
11992
+ group: "social",
11993
+ need: write5,
11994
+ risk: "W1",
11995
+ gates: ["VER", "LI"],
11996
+ run: async (ctx, { id, url, file, title }) => {
11997
+ const link = fileLinks(file)[0] ?? String(url ?? "").trim();
11998
+ if (!link.startsWith("https://"))
11999
+ throw new Error("Give the PDF (a file from this chat, or a public https link).");
12000
+ const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
12001
+ return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
12002
+ }
12003
+ }),
12004
+ tool({
12005
+ name: "social_upload_link",
12006
+ description: "A link that opens one post's picture in the CMO.so app, where the user uploads their own photos (up to 20, a carousel on LinkedIn) or, on a computer, a PDF carousel themselves. Use it when you can't pass the files (this chat can't send files to tools, or they are on the user's device). Give the link; nothing changes until they upload.",
12007
+ parameters: {
12008
+ type: "object",
12009
+ properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
12010
+ required: ["id"]
12011
+ },
12012
+ group: "social",
12013
+ need: read5,
12014
+ risk: "R0",
12015
+ run: async (ctx, { id }) => {
12016
+ const post = await ws(ctx).socialPost(Number(id));
12017
+ if (["published", "publishing", "cancelled"].includes(String(post.status)))
12018
+ throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
12019
+ const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
12020
+ return {
12021
+ id: post.id,
12022
+ channel: post.provider ?? null,
12023
+ url: `https://app.cmo.so/social?picture=${post.id}${site}`,
12024
+ 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)."
12025
+ };
12026
+ }
12027
+ }),
12028
+ tool({
12029
+ name: "social_carousel_set",
12030
+ description: "How a LinkedIn post with a set of pictures goes out: format pdf (a swipeable PDF carousel made from the pictures, the default) or images (several pictures); and the carousel's title. Preview first.",
12031
+ parameters: {
12032
+ type: "object",
12033
+ properties: {
12034
+ id: { type: "integer", description: "A LinkedIn post with a set of pictures (from social_list)" },
12035
+ format: { type: "string", enum: ["pdf", "images"] },
12036
+ title: { type: "string", description: "Shown on the carousel (up to 120 characters)" }
12037
+ },
12038
+ required: ["id"]
12039
+ },
12040
+ group: "social",
12041
+ need: write5,
12042
+ risk: "W1",
12043
+ gates: ["VER"],
12044
+ run: async (ctx, { id, format, title }) => {
12045
+ if (format == null && title == null) throw new Error("Say the format (pdf or images) or a title.");
12046
+ const out = await ws(ctx).setSocialCarousel(Number(id), {
12047
+ ...format ? { format } : {},
12048
+ ...title != null ? { title: String(title) } : {}
12049
+ });
12050
+ return { id: out.id, carousel: out.carousel ?? null };
12051
+ }
12052
+ }),
11815
12053
  tool({
11816
12054
  name: "social_post_insights",
11817
12055
  description: "How a posted Instagram post is doing: views, reach, likes, comments, saves and shares, read from Instagram (kept 15 minutes; refresh: true reads again). Saves and shares per reach are the best signs of a good post. available: false says why (not posted yet, deleted on Instagram, the account needs reconnecting).",
@@ -11920,13 +12158,14 @@ var socialTools = [
11920
12158
  }),
11921
12159
  tool({
11922
12160
  name: "social_from_article",
11923
- description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it goes to the front of the queue). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
12161
+ description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it is queued: at the front, or where `position` says). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
11924
12162
  parameters: {
11925
12163
  type: "object",
11926
12164
  properties: {
11927
12165
  source_id: { type: "string" },
11928
12166
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
11929
- channels: channelsParam
12167
+ channels: channelsParam,
12168
+ position: queuedAt
11930
12169
  },
11931
12170
  required: ["source_id", "title"]
11932
12171
  },
@@ -11934,17 +12173,18 @@ var socialTools = [
11934
12173
  need: write5,
11935
12174
  risk: "W2",
11936
12175
  gates: ["VER", "LI", "LLM"],
11937
- run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
12176
+ run: async (ctx, { source_id, channels, position }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels), positionOf(position))).job)
11938
12177
  }),
11939
12178
  tool({
11940
12179
  name: "social_plan_days",
11941
- description: "Maggie writes posts for the next 1\u20137 days from the best recent articles, on each channel. They wait for the user's OK as drafts, unless the user's approval mode is auto (then they are queued in order). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
12180
+ description: "Maggie writes posts for the next 1\u20137 days from the best recent articles, on each channel. They wait for the user's OK as drafts, unless the user's approval mode is auto (then they are queued in order: at the front, or where `position` says). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
11942
12181
  parameters: {
11943
12182
  type: "object",
11944
12183
  properties: {
11945
12184
  days: { type: "integer", minimum: 1, maximum: 7 },
11946
12185
  timezone: { type: "string", description: "IANA timezone, if known" },
11947
- channels: channelsParam
12186
+ channels: channelsParam,
12187
+ position: queuedAt
11948
12188
  },
11949
12189
  required: ["days"]
11950
12190
  },
@@ -11952,7 +12192,14 @@ var socialTools = [
11952
12192
  need: write5,
11953
12193
  risk: "W2",
11954
12194
  gates: ["VER", "LI", "LLM"],
11955
- run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
12195
+ run: async (ctx, { days, timezone, channels, position }) => writing(
12196
+ (await ws(ctx).startSocialPlan(
12197
+ int(days, 1, 7, 3),
12198
+ timezone || "UTC",
12199
+ channelsOf(channels),
12200
+ positionOf(position)
12201
+ )).job
12202
+ )
11956
12203
  }),
11957
12204
  tool({
11958
12205
  name: "social_ideas_list",
@@ -12025,13 +12272,14 @@ var socialTools = [
12025
12272
  }),
12026
12273
  tool({
12027
12274
  name: "social_idea_write",
12028
- description: "Maggie writes one idea's post for each channel (1 credit per post). The user's own idea's posts wait for their OK (queued straight away only in approval mode auto); a campaign's wait for the campaign to start. Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
12275
+ description: "Maggie writes one idea's post for each channel (1 credit per post). The user's own idea's posts wait for their OK (queued straight away only in approval mode auto, at the front or where `position` says); a campaign's wait for the campaign to start. Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
12029
12276
  parameters: {
12030
12277
  type: "object",
12031
12278
  properties: {
12032
12279
  id: { type: "integer" },
12033
12280
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
12034
- channels: channelsParam
12281
+ channels: channelsParam,
12282
+ position: queuedAt
12035
12283
  },
12036
12284
  required: ["id", "title"]
12037
12285
  },
@@ -12039,7 +12287,7 @@ var socialTools = [
12039
12287
  need: write5,
12040
12288
  risk: "W2",
12041
12289
  gates: ["VER", "LI", "CR", "LLM"],
12042
- run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
12290
+ run: async (ctx, { id, channels, position }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels), positionOf(position))).job)
12043
12291
  }),
12044
12292
  tool({
12045
12293
  name: "social_idea_brief",
@@ -12165,13 +12413,16 @@ var socialTools = [
12165
12413
  }),
12166
12414
  tool({
12167
12415
  name: "social_approve_all",
12168
- description: "Approve every post waiting for the user's OK (not a series that hasn't been approved), on one channel or all: they go out one by one at each channel's next posting times.",
12169
- parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
12416
+ description: "Approve every post waiting for the user's OK (not a series that hasn't been approved), on one channel or all: they go out one by one at each channel's next posting times (from the front of the queue, or where `position` says).",
12417
+ parameters: {
12418
+ type: "object",
12419
+ properties: { channel: { type: "string", enum: CHANNEL_KEYS }, position: queuedAt }
12420
+ },
12170
12421
  group: "social",
12171
12422
  need: write5,
12172
12423
  risk: "W3",
12173
12424
  gates: ["VER", "LI"],
12174
- run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
12425
+ run: async (ctx, { channel, position }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0], positionOf(position))
12175
12426
  }),
12176
12427
  tool({
12177
12428
  name: "series_create",
@@ -16294,7 +16545,7 @@ var program = new Command();
16294
16545
  // package.json
16295
16546
  var package_default = {
16296
16547
  name: "@cmo-so/cli",
16297
- version: "0.8.1",
16548
+ version: "0.9.1",
16298
16549
  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.",
16299
16550
  type: "module",
16300
16551
  license: "MIT",
@@ -26483,6 +26734,30 @@ function description(t) {
26483
26734
  ].filter(Boolean);
26484
26735
  return `${t.description}${notes.length ? ` (${notes.join("; ")}.)` : ""} Changes data: call without confirm for a preview, then with confirm: true and the confirmation after the user agrees.`;
26485
26736
  }
26737
+ function fileMeta(t) {
26738
+ return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
26739
+ }
26740
+ function withFreshFiles(t, signed, now) {
26741
+ if (!t.fileParams?.length) return signed;
26742
+ const fresh = /* @__PURE__ */ new Map();
26743
+ const collect = (v) => {
26744
+ for (const f of Array.isArray(v) ? v : [v])
26745
+ if (f && typeof f === "object" && typeof f.file_id === "string")
26746
+ fresh.set(f.file_id, f);
26747
+ };
26748
+ for (const p of t.fileParams) collect(now[p]);
26749
+ const swap = (v) => {
26750
+ if (Array.isArray(v)) return v.map(swap);
26751
+ if (v && typeof v === "object" && typeof v.file_id === "string") {
26752
+ const f = fresh.get(v.file_id);
26753
+ return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
26754
+ }
26755
+ return v;
26756
+ };
26757
+ const out = { ...signed };
26758
+ for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
26759
+ return out;
26760
+ }
26486
26761
  function toolArgs(args) {
26487
26762
  const out = {};
26488
26763
  for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
@@ -26612,7 +26887,7 @@ function createMcpServer(deps) {
26612
26887
  description: description(t),
26613
26888
  inputSchema: inputSchema(t),
26614
26889
  annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
26615
- ...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
26890
+ ...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
26616
26891
  }))
26617
26892
  };
26618
26893
  });
@@ -26648,7 +26923,7 @@ function createMcpServer(deps) {
26648
26923
  return fail(
26649
26924
  access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
26650
26925
  );
26651
- const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
26926
+ const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
26652
26927
  const ctx = { api: api2, domain, client: "mcp" };
26653
26928
  if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
26654
26929
  let standing = null;
@@ -27270,7 +27545,32 @@ ${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
27270
27545
  })
27271
27546
  )
27272
27547
  );
27273
- social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").action(act(async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2) })));
27548
+ social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").option("--position <where>", "front (default), end, random, or a number of posts to go after").action(
27549
+ act(
27550
+ async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2), ...opts.position ? { position: opts.position } : {} })
27551
+ )
27552
+ );
27553
+ social.command("pictures <id> <urls...>").description("your own pictures on a post, in order (public https links, up to 20); a PDF carousel on LinkedIn").option("--append", "add after the post's current pictures").action(
27554
+ act(
27555
+ async (opts, id, urls) => tool2(opts, "social_pictures_attach", { id: ids2([id])[0], urls, append: opts.append === true })
27556
+ )
27557
+ );
27558
+ 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(
27559
+ act(async (opts, id) => {
27560
+ const post = ids2([id])[0];
27561
+ if (opts.pdf)
27562
+ return tool2(opts, "social_carousel_pdf_attach", {
27563
+ id: post,
27564
+ url: opts.pdf,
27565
+ ...opts.title ? { title: opts.title } : {}
27566
+ });
27567
+ return tool2(opts, "social_carousel_set", {
27568
+ id: post,
27569
+ ...opts.format ? { format: opts.format } : {},
27570
+ ...opts.title ? { title: opts.title } : {}
27571
+ });
27572
+ })
27573
+ );
27274
27574
  social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
27275
27575
  social.command("restore <id>").description("bring a skipped draft back").action(act(async (opts, id) => tool2(opts, "social_restore", { id: ids2([id])[0] })));
27276
27576
  social.command("draft <topic...>").description("add a post idea for Maggie to write (every connected channel unless --channel)").option("--summary <text>", "what the post should say").option("--channel <names>", "linkedin, facebook, instagram, google_business (comma-separated)").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
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",