@cmo-so/mcp 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-mcp.mjs +294 -63
  2. package/package.json +1 -1
package/dist/cmo-mcp.mjs CHANGED
@@ -7199,7 +7199,7 @@ var require_dist = __commonJS({
7199
7199
  // package.json
7200
7200
  var package_default = {
7201
7201
  name: "@cmo-so/cli",
7202
- version: "0.8.0",
7202
+ version: "0.9.0",
7203
7203
  description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
7204
7204
  type: "module",
7205
7205
  license: "MIT",
@@ -7669,8 +7669,11 @@ function domainApi(api2, domain) {
7669
7669
  }),
7670
7670
  /** This website's topics, for assigning an idea to one. */
7671
7671
  topics: () => api2.request(`${base}/topics`),
7672
- /** Add a waiting idea to the end of the plan. */
7673
- planIdea: (id) => post(`${base}/ideas/${encodeURIComponent(id)}/plan`),
7672
+ /** Add a waiting idea to the plan: at the end, or at `position` (1-based in the answer). */
7673
+ planIdea: (id, position) => post(
7674
+ `${base}/ideas/${encodeURIComponent(id)}/plan`,
7675
+ position != null ? { position } : void 0
7676
+ ),
7674
7677
  /** One step up or down the 1-10 priority the planner sorts ideas by. */
7675
7678
  ideaPriority: (id, delta) => post(`${base}/ideas/${encodeURIComponent(id)}/priority`, { delta }),
7676
7679
  /** Take a post out of the plan; its idea can be restored to Ideas. */
@@ -7767,8 +7770,8 @@ function domainApi(api2, domain) {
7767
7770
  ...source ? { sitemap_url: source } : {}
7768
7771
  }),
7769
7772
  discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
7770
- /** OK a draft (or retry a failed post): it goes to the front of the queue; the post comes back with its eta. */
7771
- approveSocial: (id) => post(`${base}/social/${id}/approve`),
7773
+ /** 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. */
7774
+ approveSocial: (id, position) => post(`${base}/social/${id}/approve`, position != null ? { position } : void 0),
7772
7775
  skipSocial: (id) => post(`${base}/social/${id}/skip`),
7773
7776
  /** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
7774
7777
  /** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
@@ -7844,9 +7847,15 @@ function domainApi(api2, domain) {
7844
7847
  /** "Looks good, start": setup done, and Maggie writes the first week's posts. */
7845
7848
  socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
7846
7849
  /** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
7847
- approveAllSocial: (channel) => post(
7848
- `${base}/social/approve-all${channel ? `?channel=${channel}` : ""}`
7849
- ),
7850
+ approveAllSocial: (channel, position) => {
7851
+ const q2 = new URLSearchParams();
7852
+ if (channel) q2.set("channel", channel);
7853
+ if (position != null) q2.set("position", String(position));
7854
+ const qs = q2.toString();
7855
+ return post(
7856
+ `${base}/social/approve-all${qs ? `?${qs}` : ""}`
7857
+ );
7858
+ },
7850
7859
  /** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
7851
7860
  connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
7852
7861
  /** The Facebook Pages you connected, and the one this website posts to. */
@@ -7962,6 +7971,19 @@ function domainApi(api2, domain) {
7962
7971
  photo,
7963
7972
  ...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
7964
7973
  }),
7974
+ /** Your own set of pictures for the post, in order (up to 20; free, no AI): base64 photos and/or public
7975
+ * https links to pictures. Send a few photos per call (16 MB request limit), `append` after the first.
7976
+ * On LinkedIn a set goes out as a swipeable PDF carousel unless setSocialCarousel says images. */
7977
+ setSocialPictures: (id, pictures) => post(`${base}/social/${id}/pictures`, pictures),
7978
+ /** A LinkedIn post goes out as this PDF (base64 under ~11 MB, or a public https link; up to 300 pages);
7979
+ * its first 20 pages become the post's pictures. */
7980
+ attachSocialCarouselPdf: (id, pdf) => post(`${base}/social/${id}/carousel/pdf`, pdf),
7981
+ /** How a LinkedIn post with a set goes out: "pdf" (swipeable carousel, the default) or "images"; its title. */
7982
+ setSocialCarousel: (id, changes) => api2.request(`${base}/social/${id}/carousel`, {
7983
+ method: "PATCH",
7984
+ headers: { "Content-Type": "application/json" },
7985
+ body: JSON.stringify(changes)
7986
+ }),
7965
7987
  /** A new picture for every post here without one (or these), one after another in the background. */
7966
7988
  fillSocialImages: (opts = {}) => post(
7967
7989
  `${base}/social/images/fill`,
@@ -7995,20 +8017,24 @@ function domainApi(api2, domain) {
7995
8017
  background: true,
7996
8018
  ...channels ? { channels } : {}
7997
8019
  }),
7998
- startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
8020
+ /** `position`: where the posts are queued when approval mode is auto (the front by default). */
8021
+ startSocialFromPost: (sourcePostId, channels, position) => post(`${base}/social/from-post`, {
7999
8022
  source_post_id: sourcePostId,
8000
8023
  background: true,
8001
- ...channels ? { channels } : {}
8024
+ ...channels ? { channels } : {},
8025
+ ...position != null ? { position } : {}
8002
8026
  }),
8003
- startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
8027
+ startSocialPlan: (days, timezone, channels, position) => post(`${base}/social/plan`, {
8004
8028
  days,
8005
8029
  timezone,
8006
8030
  background: true,
8007
- ...channels ? { channels } : {}
8031
+ ...channels ? { channels } : {},
8032
+ ...position != null ? { position } : {}
8008
8033
  }),
8009
- startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
8034
+ startWriteSocialIdea: (id, channels, position) => post(`${base}/social/ideas/${id}/write`, {
8010
8035
  background: true,
8011
- ...channels ? { channels } : {}
8036
+ ...channels ? { channels } : {},
8037
+ ...position != null ? { position } : {}
8012
8038
  }),
8013
8039
  /** Disconnect the website's LinkedIn account. */
8014
8040
  disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
@@ -8181,10 +8207,12 @@ function domainApi(api2, domain) {
8181
8207
  }),
8182
8208
  // docs/19 P3: autopilot cadence and publish-now.
8183
8209
  autopilot: () => api2.request(`${base}/autopilot`),
8184
- /** Posts per day for pushed blogs (one of `choices`, up to the plan's limit). */
8185
- setPostsPerDay: (postsPerDay) => put(`${base}/autopilot/cadence`, {
8186
- posts_per_day: postsPerDay
8187
- }),
8210
+ /** Posts per day (one of `choices`, up to the plan's limit). Pushed blogs: how many
8211
+ * AI CMO sends a day. Pull sites: the most the site collects a day; null = the plan's. */
8212
+ setPostsPerDay: (postsPerDay) => put(
8213
+ `${base}/autopilot/cadence`,
8214
+ { posts_per_day: postsPerDay }
8215
+ ),
8188
8216
  /** Publish a ready draft to the connected site now (one post from today's quota). */
8189
8217
  publishNow: (id) => post(
8190
8218
  `${base}/posts/${encodeURIComponent(id)}/publish`
@@ -8255,6 +8283,8 @@ function domainApi(api2, domain) {
8255
8283
  inboxReport: (id) => api2.request(`${base}/reports/${id}`),
8256
8284
  saveReport: (r) => post(`${base}/reports`, r),
8257
8285
  readAllReports: () => post(`${base}/reports/read-all`, {}),
8286
+ /** One social post in full (social_list clips its text). */
8287
+ socialPost: (id) => api2.request(`${base}/social/${id}`),
8258
8288
  // Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
8259
8289
  ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
8260
8290
  restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
@@ -8467,6 +8497,14 @@ var CHANNEL_EN = {
8467
8497
  google_business: "Google Business"
8468
8498
  };
8469
8499
  var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
8500
+ var channelsEn = (a) => {
8501
+ const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_EN[String(c)] ?? String(c));
8502
+ return list2.length ? list2.join(list2.length === 2 ? " and " : ", ") : null;
8503
+ };
8504
+ var channelsZh = (a) => {
8505
+ const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_ZH[String(c)] ?? String(c));
8506
+ return list2.length ? list2.join("\u3001") : null;
8507
+ };
8470
8508
  var named = (title, id) => {
8471
8509
  if (title != null && String(title).trim()) return ` ${q(title)}`;
8472
8510
  const s = String(id ?? "").trim();
@@ -8476,6 +8514,20 @@ var list = (xs, n = 5) => {
8476
8514
  const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
8477
8515
  return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
8478
8516
  };
8517
+ var placeEn = (p, items = "posts") => {
8518
+ const t = String(p ?? "").trim().toLowerCase();
8519
+ if (t === "end") return `at the end of the queue, after the ${items} already waiting`;
8520
+ if (t === "random") return "at a random place in the queue";
8521
+ if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `after the first ${t} in the queue`;
8522
+ return null;
8523
+ };
8524
+ var placeZh = (p) => {
8525
+ const t = String(p ?? "").trim().toLowerCase();
8526
+ if (t === "end") return "\u6392\u5728\u968A\u5217\u6700\u5F8C\uFF0C\u5728\u5DF2\u5728\u7B49\u5F85\u7684\u4E4B\u5F8C";
8527
+ if (t === "random") return "\u96A8\u6A5F\u6392\u9032\u968A\u5217";
8528
+ if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `\u6392\u5728\u968A\u5217\u524D ${t} \u7BC7\u4E4B\u5F8C`;
8529
+ return null;
8530
+ };
8479
8531
  var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
8480
8532
  var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
8481
8533
  var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
@@ -8635,8 +8687,16 @@ var confirmCopy = {
8635
8687
  "zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
8636
8688
  },
8637
8689
  idea_plan: {
8638
- en: (a) => `Add${named(a.title, a.id)} to the end of the plan.`,
8639
- "zh-Hant": (a) => `\u628A${named(a.title, a.id)} \u52A0\u5230\u8A08\u5283\u6700\u5F8C\u3002`
8690
+ en: (a) => {
8691
+ const t = String(a.position ?? "").trim().toLowerCase();
8692
+ 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";
8693
+ return `Add${named(a.title, a.id)} to ${where}.`;
8694
+ },
8695
+ "zh-Hant": (a) => {
8696
+ const t = String(a.position ?? "").trim().toLowerCase();
8697
+ 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";
8698
+ return `\u628A${named(a.title, a.id)} \u52A0\u5230${where}\u3002`;
8699
+ }
8640
8700
  },
8641
8701
  idea_discard: {
8642
8702
  en: (a) => `Remove the idea${named(a.title, a.id)}.`,
@@ -8706,23 +8766,29 @@ var confirmCopy = {
8706
8766
  },
8707
8767
  // ----------------------------------------------------------------- social
8708
8768
  social_update_copy: {
8709
- en: (a) => `Change the text of LinkedIn post #${a.id}.`,
8710
- "zh-Hant": (a) => `\u4FEE\u6539 LinkedIn \u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
8769
+ en: (a) => `Change the text of post #${a.id}.`,
8770
+ "zh-Hant": (a) => `\u4FEE\u6539\u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
8711
8771
  },
8712
8772
  social_approve: {
8713
8773
  en: (a) => {
8714
8774
  const n = ids(a);
8715
- 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.`;
8775
+ const at = placeEn(a.position);
8776
+ if (at)
8777
+ 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"}.`;
8778
+ 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.`;
8716
8779
  },
8717
8780
  "zh-Hant": (a) => {
8718
8781
  const n = ids(a);
8719
- 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`;
8782
+ const at = placeZh(a.position);
8783
+ if (at)
8784
+ 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`;
8785
+ 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`;
8720
8786
  }
8721
8787
  },
8722
- social_skip: { en: (a) => `Skip LinkedIn post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E LinkedIn \u8CBC\u6587 #${a.id}\u3002` },
8788
+ social_skip: { en: (a) => `Skip post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E\u8CBC\u6587 #${a.id}\u3002` },
8723
8789
  social_restore: {
8724
- en: (a) => `Bring back LinkedIn post #${a.id}.`,
8725
- "zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
8790
+ en: (a) => `Bring back post #${a.id}.`,
8791
+ "zh-Hant": (a) => `\u6062\u5FA9\u8CBC\u6587 #${a.id}\u3002`
8726
8792
  },
8727
8793
  youtube_connect_link: {
8728
8794
  en: () => "Make a link to connect your YouTube channel.",
@@ -8763,28 +8829,52 @@ var confirmCopy = {
8763
8829
  "zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
8764
8830
  },
8765
8831
  social_image_new: {
8766
- 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" : ""}.`,
8767
- "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`
8832
+ 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" : ""}.`,
8833
+ "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`
8768
8834
  },
8769
8835
  social_image_stop: {
8770
- en: () => "Stop the LinkedIn picture being made; the post keeps the one it had.",
8771
- "zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684 LinkedIn \u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
8836
+ en: () => "Stop the picture being made; the post keeps the one it had.",
8837
+ "zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684\u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
8838
+ },
8839
+ social_pictures_attach: {
8840
+ en: (a) => {
8841
+ const n = Array.isArray(a.urls) ? a.urls.length : 0;
8842
+ 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.`;
8843
+ },
8844
+ "zh-Hant": (a) => {
8845
+ const n = Array.isArray(a.urls) ? a.urls.length : 0;
8846
+ 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`;
8847
+ }
8848
+ },
8849
+ social_carousel_pdf_attach: {
8850
+ 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).`,
8851
+ "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`
8852
+ },
8853
+ social_carousel_set: {
8854
+ 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)}` : ""}.`,
8855
+ "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`
8772
8856
  },
8773
8857
  social_move: {
8774
- en: (a) => `Move LinkedIn post #${a.id} ${a.to === "top" ? "to the top of" : a.to} Upcoming.`,
8775
- "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`
8858
+ en: (a) => `Move post #${a.id} ${a.to === "top" ? "to the top of its channel's queue" : `${a.to} in its channel's queue`}.`,
8859
+ "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`
8776
8860
  },
8777
8861
  social_set_paused: {
8778
- en: (a) => a.paused ? "Pause all LinkedIn posting." : "Resume LinkedIn posting.",
8779
- "zh-Hant": (a) => a.paused ? "\u66AB\u505C\u6240\u6709 LinkedIn \u767C\u6587\u3002" : "\u6062\u5FA9 LinkedIn \u767C\u6587\u3002"
8862
+ en: (a) => {
8863
+ const on2 = a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} posting` : "posting on every channel";
8864
+ return a.paused ? `Pause ${on2}.` : `Resume ${on2}.`;
8865
+ },
8866
+ "zh-Hant": (a) => {
8867
+ const on2 = a.channel ? `${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\u6240\u6709\u6E20\u9053\u7684";
8868
+ return a.paused ? `\u66AB\u505C${on2}\u767C\u6587\u3002` : `\u6062\u5FA9${on2}\u767C\u6587\u3002`;
8869
+ }
8780
8870
  },
8781
8871
  social_from_article: {
8782
- 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.`,
8783
- "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`
8872
+ 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)})` : ""}.`,
8873
+ "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`
8784
8874
  },
8785
8875
  social_plan_days: {
8786
- 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.`,
8787
- "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`
8876
+ 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)})` : ""}.`,
8877
+ "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`
8788
8878
  },
8789
8879
  social_idea_add: {
8790
8880
  en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
@@ -8799,8 +8889,8 @@ var confirmCopy = {
8799
8889
  "zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
8800
8890
  },
8801
8891
  social_idea_write: {
8802
- 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.`,
8803
- "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`
8892
+ 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)})` : ""}.`,
8893
+ "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`
8804
8894
  },
8805
8895
  social_idea_save: {
8806
8896
  en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
@@ -8866,8 +8956,8 @@ var confirmCopy = {
8866
8956
  "zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
8867
8957
  },
8868
8958
  social_approve_all: {
8869
- 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.`,
8870
- "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`
8959
+ 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.`,
8960
+ "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`
8871
8961
  },
8872
8962
  series_create: {
8873
8963
  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.`,
@@ -9022,6 +9112,18 @@ var int = (v, min, max, fallback) => {
9022
9112
  const n = Math.trunc(Number(v));
9023
9113
  return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
9024
9114
  };
9115
+ var positionParam = (what, fallback) => ({
9116
+ type: "string",
9117
+ 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.`
9118
+ });
9119
+ var positionOf = (value) => {
9120
+ if (value == null || value === "") return void 0;
9121
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
9122
+ const text3 = String(value).trim().toLowerCase();
9123
+ if (text3 === "front" || text3 === "end" || text3 === "random") return text3;
9124
+ if (/^\d+$/.test(text3)) return Number(text3);
9125
+ throw new Error('position must be "front", "end", "random" or a number of items to go after (0 = front).');
9126
+ };
9025
9127
  function tool(t) {
9026
9128
  const out = { ...t };
9027
9129
  if (t.risk !== "R0" && !t.confirm)
@@ -9716,19 +9818,20 @@ var blogTools = [
9716
9818
  }),
9717
9819
  tool({
9718
9820
  name: "idea_plan",
9719
- description: "Add a waiting idea to the end of the plan (the plan holds up to 100).",
9821
+ 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.",
9720
9822
  parameters: {
9721
9823
  type: "object",
9722
9824
  properties: {
9723
9825
  id: { type: "string" },
9724
- title: { type: "string", description: "As listed (shown on the approval card and in History)" }
9826
+ title: { type: "string", description: "As listed (shown on the approval card and in History)" },
9827
+ position: positionParam("the idea", "Leave out for the end of the plan.")
9725
9828
  },
9726
9829
  required: ["id", "title"]
9727
9830
  },
9728
9831
  group: "blog",
9729
9832
  need: write,
9730
9833
  risk: "W1",
9731
- run: async (ctx, { id }) => ws(ctx).planIdea(id)
9834
+ run: async (ctx, { id, position }) => ws(ctx).planIdea(id, positionOf(position))
9732
9835
  }),
9733
9836
  tool({
9734
9837
  name: "idea_discard",
@@ -10899,6 +11002,32 @@ var opsTools = [
10899
11002
  const { reviewer, comment, ...rest } = r;
10900
11003
  return { ...rest, ...untrusted({ reviewer, comment }) };
10901
11004
  }
11005
+ }),
11006
+ tool({
11007
+ name: "social_post_get",
11008
+ 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.",
11009
+ parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
11010
+ group: "social",
11011
+ need: { resource: "autopilot", level: "read" },
11012
+ risk: "R0",
11013
+ run: async (ctx, { id }) => {
11014
+ const p = await ws(ctx).socialPost(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
11015
+ return {
11016
+ id: p.id,
11017
+ channel: p.provider,
11018
+ status: p.status,
11019
+ eta: p.eta ?? null,
11020
+ text: p.social_copy,
11021
+ characters: String(p.social_copy ?? "").length,
11022
+ picture: p.image_url ?? null,
11023
+ pictures: p.image_urls ?? [],
11024
+ // LinkedIn with a set (or an uploaded PDF): a PDF carousel or several pictures.
11025
+ carousel: p.carousel ?? null,
11026
+ link: p.cta_url ?? p.source_url ?? null,
11027
+ held: p.held ?? null,
11028
+ failure: p.failure_message ?? null
11029
+ };
11030
+ }
10902
11031
  })
10903
11032
  ];
10904
11033
 
@@ -10921,7 +11050,7 @@ var publishingTools = [
10921
11050
  }),
10922
11051
  tool({
10923
11052
  name: "posts_per_day_set",
10924
- 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.",
11053
+ 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).",
10925
11054
  parameters: {
10926
11055
  type: "object",
10927
11056
  properties: {
@@ -11484,6 +11613,7 @@ var channelsParam = {
11484
11613
  items: { type: "string", enum: CHANNEL_KEYS },
11485
11614
  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."
11486
11615
  };
11616
+ var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
11487
11617
  var channelsOf = (value) => {
11488
11618
  const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
11489
11619
  return list2.length ? list2 : void 0;
@@ -11576,24 +11706,28 @@ var socialTools = [
11576
11706
  }),
11577
11707
  tool({
11578
11708
  name: "social_approve",
11579
- 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.",
11709
+ 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.",
11580
11710
  parameters: {
11581
11711
  type: "object",
11582
- properties: { ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" } },
11712
+ properties: {
11713
+ ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
11714
+ position: queuedAt
11715
+ },
11583
11716
  required: ["ids"]
11584
11717
  },
11585
11718
  group: "social",
11586
11719
  need: write5,
11587
11720
  risk: "W3",
11588
11721
  gates: ["VER", "LI"],
11589
- run: async (ctx, { ids: ids2, id }) => {
11722
+ run: async (ctx, { ids: ids2, id, position }) => {
11590
11723
  const list2 = (Array.isArray(ids2) && ids2.length ? ids2 : id != null ? [id] : []).map(Number).slice(0, 20);
11591
11724
  if (!list2.length) throw new Error("Which post?");
11725
+ const at = positionOf(position);
11592
11726
  const api2 = ws(ctx);
11593
11727
  const results = [];
11594
11728
  for (const n of list2) {
11595
11729
  try {
11596
- await api2.approveSocial(n);
11730
+ await api2.approveSocial(n, at);
11597
11731
  results.push({ id: n, ok: true });
11598
11732
  } catch (e) {
11599
11733
  results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -11846,6 +11980,90 @@ var socialTools = [
11846
11980
  return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
11847
11981
  }
11848
11982
  }),
11983
+ tool({
11984
+ name: "social_pictures_attach",
11985
+ 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.",
11986
+ parameters: {
11987
+ type: "object",
11988
+ properties: {
11989
+ id: { type: "integer", description: "The post (from social_list), not gone out yet" },
11990
+ urls: {
11991
+ type: "array",
11992
+ items: { type: "string" },
11993
+ description: "Public https links to the image files, in the order they should appear"
11994
+ },
11995
+ append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
11996
+ },
11997
+ required: ["id", "urls"]
11998
+ },
11999
+ group: "social",
12000
+ need: write5,
12001
+ risk: "W1",
12002
+ gates: ["VER"],
12003
+ run: async (ctx, { id, urls, append }) => {
12004
+ const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(Boolean);
12005
+ if (!list2.length) throw new Error("Give at least one https link to a picture.");
12006
+ if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
12007
+ if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
12008
+ const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
12009
+ return {
12010
+ id: out.id,
12011
+ pictures: out.image_urls?.length ? out.image_urls : [out.image_url],
12012
+ carousel: out.carousel ?? null
12013
+ };
12014
+ }
12015
+ }),
12016
+ tool({
12017
+ name: "social_carousel_pdf_attach",
12018
+ 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.",
12019
+ parameters: {
12020
+ type: "object",
12021
+ properties: {
12022
+ id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
12023
+ url: { type: "string", description: "Public https link to the PDF file" },
12024
+ title: {
12025
+ type: "string",
12026
+ description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
12027
+ }
12028
+ },
12029
+ required: ["id", "url"]
12030
+ },
12031
+ group: "social",
12032
+ need: write5,
12033
+ risk: "W1",
12034
+ gates: ["VER", "LI"],
12035
+ run: async (ctx, { id, url, title }) => {
12036
+ const link = String(url ?? "").trim();
12037
+ if (!link.startsWith("https://")) throw new Error("Give a public https link to the PDF.");
12038
+ const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
12039
+ return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
12040
+ }
12041
+ }),
12042
+ tool({
12043
+ name: "social_carousel_set",
12044
+ 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.",
12045
+ parameters: {
12046
+ type: "object",
12047
+ properties: {
12048
+ id: { type: "integer", description: "A LinkedIn post with a set of pictures (from social_list)" },
12049
+ format: { type: "string", enum: ["pdf", "images"] },
12050
+ title: { type: "string", description: "Shown on the carousel (up to 120 characters)" }
12051
+ },
12052
+ required: ["id"]
12053
+ },
12054
+ group: "social",
12055
+ need: write5,
12056
+ risk: "W1",
12057
+ gates: ["VER"],
12058
+ run: async (ctx, { id, format, title }) => {
12059
+ if (format == null && title == null) throw new Error("Say the format (pdf or images) or a title.");
12060
+ const out = await ws(ctx).setSocialCarousel(Number(id), {
12061
+ ...format ? { format } : {},
12062
+ ...title != null ? { title: String(title) } : {}
12063
+ });
12064
+ return { id: out.id, carousel: out.carousel ?? null };
12065
+ }
12066
+ }),
11849
12067
  tool({
11850
12068
  name: "social_post_insights",
11851
12069
  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).",
@@ -11954,13 +12172,14 @@ var socialTools = [
11954
12172
  }),
11955
12173
  tool({
11956
12174
  name: "social_from_article",
11957
- 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).",
12175
+ 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).",
11958
12176
  parameters: {
11959
12177
  type: "object",
11960
12178
  properties: {
11961
12179
  source_id: { type: "string" },
11962
12180
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
11963
- channels: channelsParam
12181
+ channels: channelsParam,
12182
+ position: queuedAt
11964
12183
  },
11965
12184
  required: ["source_id", "title"]
11966
12185
  },
@@ -11968,17 +12187,18 @@ var socialTools = [
11968
12187
  need: write5,
11969
12188
  risk: "W2",
11970
12189
  gates: ["VER", "LI", "LLM"],
11971
- run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
12190
+ run: async (ctx, { source_id, channels, position }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels), positionOf(position))).job)
11972
12191
  }),
11973
12192
  tool({
11974
12193
  name: "social_plan_days",
11975
- 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).",
12194
+ 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).",
11976
12195
  parameters: {
11977
12196
  type: "object",
11978
12197
  properties: {
11979
12198
  days: { type: "integer", minimum: 1, maximum: 7 },
11980
12199
  timezone: { type: "string", description: "IANA timezone, if known" },
11981
- channels: channelsParam
12200
+ channels: channelsParam,
12201
+ position: queuedAt
11982
12202
  },
11983
12203
  required: ["days"]
11984
12204
  },
@@ -11986,7 +12206,14 @@ var socialTools = [
11986
12206
  need: write5,
11987
12207
  risk: "W2",
11988
12208
  gates: ["VER", "LI", "LLM"],
11989
- run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
12209
+ run: async (ctx, { days, timezone, channels, position }) => writing(
12210
+ (await ws(ctx).startSocialPlan(
12211
+ int(days, 1, 7, 3),
12212
+ timezone || "UTC",
12213
+ channelsOf(channels),
12214
+ positionOf(position)
12215
+ )).job
12216
+ )
11990
12217
  }),
11991
12218
  tool({
11992
12219
  name: "social_ideas_list",
@@ -12059,13 +12286,14 @@ var socialTools = [
12059
12286
  }),
12060
12287
  tool({
12061
12288
  name: "social_idea_write",
12062
- 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).",
12289
+ 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).",
12063
12290
  parameters: {
12064
12291
  type: "object",
12065
12292
  properties: {
12066
12293
  id: { type: "integer" },
12067
12294
  title: { type: "string", description: "As listed (shown on the approval card and in History)" },
12068
- channels: channelsParam
12295
+ channels: channelsParam,
12296
+ position: queuedAt
12069
12297
  },
12070
12298
  required: ["id", "title"]
12071
12299
  },
@@ -12073,7 +12301,7 @@ var socialTools = [
12073
12301
  need: write5,
12074
12302
  risk: "W2",
12075
12303
  gates: ["VER", "LI", "CR", "LLM"],
12076
- run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
12304
+ run: async (ctx, { id, channels, position }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels), positionOf(position))).job)
12077
12305
  }),
12078
12306
  tool({
12079
12307
  name: "social_idea_brief",
@@ -12199,13 +12427,16 @@ var socialTools = [
12199
12427
  }),
12200
12428
  tool({
12201
12429
  name: "social_approve_all",
12202
- 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.",
12203
- parameters: { type: "object", properties: { channel: { type: "string", enum: CHANNEL_KEYS } } },
12430
+ 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).",
12431
+ parameters: {
12432
+ type: "object",
12433
+ properties: { channel: { type: "string", enum: CHANNEL_KEYS }, position: queuedAt }
12434
+ },
12204
12435
  group: "social",
12205
12436
  need: write5,
12206
12437
  risk: "W3",
12207
12438
  gates: ["VER", "LI"],
12208
- run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
12439
+ run: async (ctx, { channel, position }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0], positionOf(position))
12209
12440
  }),
12210
12441
  tool({
12211
12442
  name: "series_create",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "CMO.so's MCP server for Claude Desktop, Cursor, VS Code and other MCP clients: manage your websites, blog posts, LinkedIn queue and results with Maggie.",
5
5
  "type": "module",
6
6
  "license": "MIT",