@cmo-so/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cmo.mjs +320 -64
  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`
@@ -8195,6 +8223,8 @@ function domainApi(api2, domain) {
8195
8223
  inboxReport: (id) => api2.request(`${base}/reports/${id}`),
8196
8224
  saveReport: (r) => post(`${base}/reports`, r),
8197
8225
  readAllReports: () => post(`${base}/reports/read-all`, {}),
8226
+ /** One social post in full (social_list clips its text). */
8227
+ socialPost: (id) => api2.request(`${base}/social/${id}`),
8198
8228
  // Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
8199
8229
  ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
8200
8230
  restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
@@ -8407,6 +8437,14 @@ var CHANNEL_EN = {
8407
8437
  google_business: "Google Business"
8408
8438
  };
8409
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
+ };
8410
8448
  var named = (title, id) => {
8411
8449
  if (title != null && String(title).trim()) return ` ${q(title)}`;
8412
8450
  const s = String(id ?? "").trim();
@@ -8416,6 +8454,20 @@ var list = (xs, n = 5) => {
8416
8454
  const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
8417
8455
  return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
8418
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
+ };
8419
8471
  var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
8420
8472
  var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
8421
8473
  var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
@@ -8575,8 +8627,16 @@ var confirmCopy = {
8575
8627
  "zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
8576
8628
  },
8577
8629
  idea_plan: {
8578
- en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
8579
- "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
+ }
8580
8640
  },
8581
8641
  idea_discard: {
8582
8642
  en: (a) => `Remove the idea${named(a.title, a.id)}.`,
@@ -8646,23 +8706,29 @@ var confirmCopy = {
8646
8706
  },
8647
8707
  // ----------------------------------------------------------------- social
8648
8708
  social_update_copy: {
8649
- en: (a) => `Change the text of LinkedIn post #${a.id}.`,
8650
- "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`
8651
8711
  },
8652
8712
  social_approve: {
8653
8713
  en: (a) => {
8654
8714
  const n = ids(a);
8655
- 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.`;
8656
8719
  },
8657
8720
  "zh-Hant": (a) => {
8658
8721
  const n = ids(a);
8659
- 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`;
8660
8726
  }
8661
8727
  },
8662
- 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` },
8663
8729
  social_restore: {
8664
- en: (a) => `Bring back LinkedIn post #${a.id}.`,
8665
- "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`
8666
8732
  },
8667
8733
  youtube_connect_link: {
8668
8734
  en: () => "Make a link to connect your YouTube channel.",
@@ -8703,28 +8769,52 @@ var confirmCopy = {
8703
8769
  "zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
8704
8770
  },
8705
8771
  social_image_new: {
8706
- 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" : ""}.`,
8707
- "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`
8708
8774
  },
8709
8775
  social_image_stop: {
8710
- en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
8711
- "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;
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;
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`
8712
8796
  },
8713
8797
  social_move: {
8714
- en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
8715
- "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`
8716
8800
  },
8717
8801
  social_set_paused: {
8718
- en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
8719
- "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
+ }
8720
8810
  },
8721
8811
  social_from_article: {
8722
- 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.`,
8723
- "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`
8724
8814
  },
8725
8815
  social_plan_days: {
8726
- 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.`,
8727
- "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`
8728
8818
  },
8729
8819
  social_idea_add: {
8730
8820
  en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
@@ -8739,8 +8829,8 @@ var confirmCopy = {
8739
8829
  "zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
8740
8830
  },
8741
8831
  social_idea_write: {
8742
- 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.`,
8743
- "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`
8744
8834
  },
8745
8835
  social_idea_save: {
8746
8836
  en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
@@ -8806,8 +8896,8 @@ var confirmCopy = {
8806
8896
  "zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
8807
8897
  },
8808
8898
  social_approve_all: {
8809
- 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.`,
8810
- "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`
8811
8901
  },
8812
8902
  series_create: {
8813
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.`,
@@ -8962,6 +9052,18 @@ var int = (v, min, max, fallback) => {
8962
9052
  const n = Math.trunc(Number(v));
8963
9053
  return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
8964
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
+ };
8965
9067
  function tool(t) {
8966
9068
  const out = { ...t };
8967
9069
  if (t.risk !== "R0" && !t.confirm)
@@ -9656,19 +9758,20 @@ var blogTools = [
9656
9758
  }),
9657
9759
  tool({
9658
9760
  name: "idea_plan",
9659
- 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.",
9660
9762
  parameters: {
9661
9763
  type: "object",
9662
9764
  properties: {
9663
9765
  id: { type: "string" },
9664
- 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.")
9665
9768
  },
9666
9769
  required: ["id", "title"]
9667
9770
  },
9668
9771
  group: "blog",
9669
9772
  need: write,
9670
9773
  risk: "W1",
9671
- run: async (ctx, { id }) => ws(ctx).planIdea(id)
9774
+ run: async (ctx, { id, position }) => ws(ctx).planIdea(id, positionOf(position))
9672
9775
  }),
9673
9776
  tool({
9674
9777
  name: "idea_discard",
@@ -10839,6 +10942,32 @@ var opsTools = [
10839
10942
  const { reviewer, comment, ...rest } = r;
10840
10943
  return { ...rest, ...untrusted({ reviewer, comment }) };
10841
10944
  }
10945
+ }),
10946
+ tool({
10947
+ name: "social_post_get",
10948
+ description: "One social post in full: its whole text (social_list shortens it to about 300 characters), its pictures, its link, its channel and status, when it goes out, and why it was held before going out, if it was. Use it to check a post's ending (a web address or phone number on Google, a cut-off sentence) before approving or fixing it.",
10949
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
10950
+ group: "social",
10951
+ need: { resource: "autopilot", level: "read" },
10952
+ risk: "R0",
10953
+ run: async (ctx, { id }) => {
10954
+ const p = await ws(ctx).socialPost(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
10955
+ return {
10956
+ id: p.id,
10957
+ channel: p.provider,
10958
+ status: p.status,
10959
+ eta: p.eta ?? null,
10960
+ text: p.social_copy,
10961
+ characters: String(p.social_copy ?? "").length,
10962
+ picture: p.image_url ?? null,
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,
10966
+ link: p.cta_url ?? p.source_url ?? null,
10967
+ held: p.held ?? null,
10968
+ failure: p.failure_message ?? null
10969
+ };
10970
+ }
10842
10971
  })
10843
10972
  ];
10844
10973
 
@@ -10861,7 +10990,7 @@ var publishingTools = [
10861
10990
  }),
10862
10991
  tool({
10863
10992
  name: "posts_per_day_set",
10864
- 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).",
10865
10994
  parameters: {
10866
10995
  type: "object",
10867
10996
  properties: {
@@ -11424,6 +11553,7 @@ var channelsParam = {
11424
11553
  items: { type: "string", enum: CHANNEL_KEYS },
11425
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."
11426
11555
  };
11556
+ var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11427
11557
  var channelsOf = (value) => {
11428
11558
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
11429
11559
  return list2.length ? list2 : void 0;
@@ -11516,24 +11646,28 @@ var socialTools = [
11516
11646
  }),
11517
11647
  tool({
11518
11648
  name: "social_approve",
11519
- 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.",
11649
+ 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.",
11520
11650
  parameters: {
11521
11651
  type: "object",
11522
- properties: { ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" } },
11652
+ properties: {
11653
+ ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
11654
+ position: queuedAt
11655
+ },
11523
11656
  required: ["ids"]
11524
11657
  },
11525
11658
  group: "social",
11526
11659
  need: write5,
11527
11660
  risk: "W3",
11528
11661
  gates: ["VER", "LI"],
11529
- run: async (ctx, { ids: ids3, id }) => {
11662
+ run: async (ctx, { ids: ids3, id, position }) => {
11530
11663
  const list2 = (Array.isArray(ids3) && ids3.length ? ids3 : id != null ? [id] : []).map(Number).slice(0, 20);
11531
11664
  if (!list2.length) throw new Error("Which post?");
11665
+ const at = positionOf(position);
11532
11666
  const api2 = ws(ctx);
11533
11667
  const results = [];
11534
11668
  for (const n of list2) {
11535
11669
  try {
11536
- await api2.approveSocial(n);
11670
+ await api2.approveSocial(n, at);
11537
11671
  results.push({ id: n, ok: true });
11538
11672
  } catch (e) {
11539
11673
  results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -11786,6 +11920,90 @@ var socialTools = [
11786
11920
  return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
11787
11921
  }
11788
11922
  }),
11923
+ tool({
11924
+ name: "social_pictures_attach",
11925
+ description: "Put the user's own pictures on a post, in order: public https links to image files (JPEG, PNG or WebP), for example slides another assistant made. 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. Preview first.",
11926
+ parameters: {
11927
+ type: "object",
11928
+ properties: {
11929
+ id: { type: "integer", description: "The post (from social_list), not gone out yet" },
11930
+ urls: {
11931
+ type: "array",
11932
+ items: { type: "string" },
11933
+ description: "Public https links to the image files, in the order they should appear"
11934
+ },
11935
+ append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11936
+ },
11937
+ required: ["id", "urls"]
11938
+ },
11939
+ group: "social",
11940
+ need: write5,
11941
+ risk: "W1",
11942
+ gates: ["VER"],
11943
+ run: async (ctx, { id, urls, append }) => {
11944
+ const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(Boolean);
11945
+ if (!list2.length) throw new Error("Give at least one https link to a picture.");
11946
+ if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
11947
+ if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
11948
+ const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
11949
+ return {
11950
+ id: out.id,
11951
+ pictures: out.image_urls?.length ? out.image_urls : [out.image_url],
11952
+ carousel: out.carousel ?? null
11953
+ };
11954
+ }
11955
+ }),
11956
+ tool({
11957
+ name: "social_carousel_pdf_attach",
11958
+ description: "A LinkedIn post goes out as this PDF: a swipeable document carousel (a public https link to the PDF, 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. Preview first.",
11959
+ parameters: {
11960
+ type: "object",
11961
+ properties: {
11962
+ id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
11963
+ url: { type: "string", description: "Public https link to the PDF file" },
11964
+ title: {
11965
+ type: "string",
11966
+ description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
11967
+ }
11968
+ },
11969
+ required: ["id", "url"]
11970
+ },
11971
+ group: "social",
11972
+ need: write5,
11973
+ risk: "W1",
11974
+ gates: ["VER", "LI"],
11975
+ run: async (ctx, { id, url, title }) => {
11976
+ const link = String(url ?? "").trim();
11977
+ if (!link.startsWith("https://")) throw new Error("Give a public https link to the PDF.");
11978
+ const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
11979
+ return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
11980
+ }
11981
+ }),
11982
+ tool({
11983
+ name: "social_carousel_set",
11984
+ 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.",
11985
+ parameters: {
11986
+ type: "object",
11987
+ properties: {
11988
+ id: { type: "integer", description: "A LinkedIn post with a set of pictures (from social_list)" },
11989
+ format: { type: "string", enum: ["pdf", "images"] },
11990
+ title: { type: "string", description: "Shown on the carousel (up to 120 characters)" }
11991
+ },
11992
+ required: ["id"]
11993
+ },
11994
+ group: "social",
11995
+ need: write5,
11996
+ risk: "W1",
11997
+ gates: ["VER"],
11998
+ run: async (ctx, { id, format, title }) => {
11999
+ if (format == null && title == null) throw new Error("Say the format (pdf or images) or a title.");
12000
+ const out = await ws(ctx).setSocialCarousel(Number(id), {
12001
+ ...format ? { format } : {},
12002
+ ...title != null ? { title: String(title) } : {}
12003
+ });
12004
+ return { id: out.id, carousel: out.carousel ?? null };
12005
+ }
12006
+ }),
11789
12007
  tool({
11790
12008
  name: "social_post_insights",
11791
12009
  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).",
@@ -11894,13 +12112,14 @@ var socialTools = [
11894
12112
  }),
11895
12113
  tool({
11896
12114
  name: "social_from_article",
11897
- 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).",
12115
+ 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).",
11898
12116
  parameters: {
11899
12117
  type: "object",
11900
12118
  properties: {
11901
12119
  source_id: { type: "string" },
11902
12120
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
11903
- channels: channelsParam
12121
+ channels: channelsParam,
12122
+ position: queuedAt
11904
12123
  },
11905
12124
  required: ["source_id", "title"]
11906
12125
  },
@@ -11908,17 +12127,18 @@ var socialTools = [
11908
12127
  need: write5,
11909
12128
  risk: "W2",
11910
12129
  gates: ["VER", "LI", "LLM"],
11911
- run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
12130
+ run: async (ctx, { source_id, channels, position }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels), positionOf(position))).job)
11912
12131
  }),
11913
12132
  tool({
11914
12133
  name: "social_plan_days",
11915
- 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).",
12134
+ 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).",
11916
12135
  parameters: {
11917
12136
  type: "object",
11918
12137
  properties: {
11919
12138
  days: { type: "integer", minimum: 1, maximum: 7 },
11920
12139
  timezone: { type: "string", description: "IANA timezone, if known" },
11921
- channels: channelsParam
12140
+ channels: channelsParam,
12141
+ position: queuedAt
11922
12142
  },
11923
12143
  required: ["days"]
11924
12144
  },
@@ -11926,7 +12146,14 @@ var socialTools = [
11926
12146
  need: write5,
11927
12147
  risk: "W2",
11928
12148
  gates: ["VER", "LI", "LLM"],
11929
- run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
12149
+ run: async (ctx, { days, timezone, channels, position }) => writing(
12150
+ (await ws(ctx).startSocialPlan(
12151
+ int(days, 1, 7, 3),
12152
+ timezone || "UTC",
12153
+ channelsOf(channels),
12154
+ positionOf(position)
12155
+ )).job
12156
+ )
11930
12157
  }),
11931
12158
  tool({
11932
12159
  name: "social_ideas_list",
@@ -11999,13 +12226,14 @@ var socialTools = [
11999
12226
  }),
12000
12227
  tool({
12001
12228
  name: "social_idea_write",
12002
- 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).",
12229
+ 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).",
12003
12230
  parameters: {
12004
12231
  type: "object",
12005
12232
  properties: {
12006
12233
  id: { type: "integer" },
12007
12234
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
12008
- channels: channelsParam
12235
+ channels: channelsParam,
12236
+ position: queuedAt
12009
12237
  },
12010
12238
  required: ["id", "title"]
12011
12239
  },
@@ -12013,7 +12241,7 @@ var socialTools = [
12013
12241
  need: write5,
12014
12242
  risk: "W2",
12015
12243
  gates: ["VER", "LI", "CR", "LLM"],
12016
- run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
12244
+ run: async (ctx, { id, channels, position }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels), positionOf(position))).job)
12017
12245
  }),
12018
12246
  tool({
12019
12247
  name: "social_idea_brief",
@@ -12139,13 +12367,16 @@ var socialTools = [
12139
12367
  }),
12140
12368
  tool({
12141
12369
  name: "social_approve_all",
12142
- 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.",
12143
- parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
12370
+ 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).",
12371
+ parameters: {
12372
+ type: "object",
12373
+ properties: { channel: { type: "string", enum: CHANNEL_KEYS }, position: queuedAt }
12374
+ },
12144
12375
  group: "social",
12145
12376
  need: write5,
12146
12377
  risk: "W3",
12147
12378
  gates: ["VER", "LI"],
12148
- run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
12379
+ run: async (ctx, { channel, position }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0], positionOf(position))
12149
12380
  }),
12150
12381
  tool({
12151
12382
  name: "series_create",
@@ -16268,7 +16499,7 @@ var program = new Command();
16268
16499
  // package.json
16269
16500
  var package_default = {
16270
16501
  name: "@cmo-so/cli",
16271
- version: "0.8.0",
16502
+ version: "0.9.0",
16272
16503
  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.",
16273
16504
  type: "module",
16274
16505
  license: "MIT",
@@ -27244,7 +27475,32 @@ ${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
27244
27475
  })
27245
27476
  )
27246
27477
  );
27247
- 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) })));
27478
+ 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(
27479
+ act(
27480
+ async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2), ...opts.position ? { position: opts.position } : {} })
27481
+ )
27482
+ );
27483
+ 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(
27484
+ act(
27485
+ async (opts, id, urls) => tool2(opts, "social_pictures_attach", { id: ids2([id])[0], urls, append: opts.append === true })
27486
+ )
27487
+ );
27488
+ 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(
27489
+ act(async (opts, id) => {
27490
+ const post = ids2([id])[0];
27491
+ if (opts.pdf)
27492
+ return tool2(opts, "social_carousel_pdf_attach", {
27493
+ id: post,
27494
+ url: opts.pdf,
27495
+ ...opts.title ? { title: opts.title } : {}
27496
+ });
27497
+ return tool2(opts, "social_carousel_set", {
27498
+ id: post,
27499
+ ...opts.format ? { format: opts.format } : {},
27500
+ ...opts.title ? { title: opts.title } : {}
27501
+ });
27502
+ })
27503
+ );
27248
27504
  social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
27249
27505
  social.command("restore <id>").description("bring a skipped draft back").action(act(async (opts, id) => tool2(opts, "social_restore", { id: ids2([id])[0] })));
27250
27506
  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.0",
3
+ "version": "0.9.0",
4
4
  "description": "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
5
5
  "type": "module",
6
6
  "license": "MIT",