@cmo-so/mcp 0.8.1 → 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.
- package/dist/cmo-mcp.mjs +268 -63
- 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.
|
|
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
|
|
7673
|
-
planIdea: (id) => post(
|
|
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
|
|
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) =>
|
|
7848
|
-
|
|
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
|
-
|
|
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
|
|
8185
|
-
|
|
8186
|
-
|
|
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`
|
|
@@ -8469,6 +8497,14 @@ var CHANNEL_EN = {
|
|
|
8469
8497
|
google_business: "Google Business"
|
|
8470
8498
|
};
|
|
8471
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
|
+
};
|
|
8472
8508
|
var named = (title, id) => {
|
|
8473
8509
|
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8474
8510
|
const s = String(id ?? "").trim();
|
|
@@ -8478,6 +8514,20 @@ var list = (xs, n = 5) => {
|
|
|
8478
8514
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
8479
8515
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
8480
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
|
+
};
|
|
8481
8531
|
var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
|
|
8482
8532
|
var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
8483
8533
|
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
@@ -8637,8 +8687,16 @@ var confirmCopy = {
|
|
|
8637
8687
|
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
8638
8688
|
},
|
|
8639
8689
|
idea_plan: {
|
|
8640
|
-
en: (a) =>
|
|
8641
|
-
|
|
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
|
+
}
|
|
8642
8700
|
},
|
|
8643
8701
|
idea_discard: {
|
|
8644
8702
|
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
@@ -8708,23 +8766,29 @@ var confirmCopy = {
|
|
|
8708
8766
|
},
|
|
8709
8767
|
// ----------------------------------------------------------------- social
|
|
8710
8768
|
social_update_copy: {
|
|
8711
|
-
en: (a) => `Change the text of
|
|
8712
|
-
"zh-Hant": (a) => `\u4FEE\u6539
|
|
8769
|
+
en: (a) => `Change the text of post #${a.id}.`,
|
|
8770
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
|
|
8713
8771
|
},
|
|
8714
8772
|
social_approve: {
|
|
8715
8773
|
en: (a) => {
|
|
8716
8774
|
const n = ids(a);
|
|
8717
|
-
|
|
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.`;
|
|
8718
8779
|
},
|
|
8719
8780
|
"zh-Hant": (a) => {
|
|
8720
8781
|
const n = ids(a);
|
|
8721
|
-
|
|
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`;
|
|
8722
8786
|
}
|
|
8723
8787
|
},
|
|
8724
|
-
social_skip: { en: (a) => `Skip
|
|
8788
|
+
social_skip: { en: (a) => `Skip post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E\u8CBC\u6587 #${a.id}\u3002` },
|
|
8725
8789
|
social_restore: {
|
|
8726
|
-
en: (a) => `Bring back
|
|
8727
|
-
"zh-Hant": (a) => `\u6062\u5FA9
|
|
8790
|
+
en: (a) => `Bring back post #${a.id}.`,
|
|
8791
|
+
"zh-Hant": (a) => `\u6062\u5FA9\u8CBC\u6587 #${a.id}\u3002`
|
|
8728
8792
|
},
|
|
8729
8793
|
youtube_connect_link: {
|
|
8730
8794
|
en: () => "Make a link to connect your YouTube channel.",
|
|
@@ -8765,28 +8829,52 @@ var confirmCopy = {
|
|
|
8765
8829
|
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8766
8830
|
},
|
|
8767
8831
|
social_image_new: {
|
|
8768
|
-
en: (a) => `Make a new picture for
|
|
8769
|
-
"zh-Hant": (a) => `\u70BA
|
|
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`
|
|
8770
8834
|
},
|
|
8771
8835
|
social_image_stop: {
|
|
8772
|
-
en: () => "Stop the
|
|
8773
|
-
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684
|
|
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`
|
|
8774
8856
|
},
|
|
8775
8857
|
social_move: {
|
|
8776
|
-
en: (a) => `Move
|
|
8777
|
-
"zh-Hant": (a) => `\u628A
|
|
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`
|
|
8778
8860
|
},
|
|
8779
8861
|
social_set_paused: {
|
|
8780
|
-
en: (a) =>
|
|
8781
|
-
|
|
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
|
+
}
|
|
8782
8870
|
},
|
|
8783
8871
|
social_from_article: {
|
|
8784
|
-
en: (a) => `Write a
|
|
8785
|
-
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)}
|
|
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`
|
|
8786
8874
|
},
|
|
8787
8875
|
social_plan_days: {
|
|
8788
|
-
en: (a) => `Write
|
|
8789
|
-
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684
|
|
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`
|
|
8790
8878
|
},
|
|
8791
8879
|
social_idea_add: {
|
|
8792
8880
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -8801,8 +8889,8 @@ var confirmCopy = {
|
|
|
8801
8889
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
8802
8890
|
},
|
|
8803
8891
|
social_idea_write: {
|
|
8804
|
-
en: (a) => `Write the
|
|
8805
|
-
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB
|
|
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`
|
|
8806
8894
|
},
|
|
8807
8895
|
social_idea_save: {
|
|
8808
8896
|
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
@@ -8868,8 +8956,8 @@ var confirmCopy = {
|
|
|
8868
8956
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8869
8957
|
},
|
|
8870
8958
|
social_approve_all: {
|
|
8871
|
-
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.`,
|
|
8872
|
-
"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\
|
|
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`
|
|
8873
8961
|
},
|
|
8874
8962
|
series_create: {
|
|
8875
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.`,
|
|
@@ -9024,6 +9112,18 @@ var int = (v, min, max, fallback) => {
|
|
|
9024
9112
|
const n = Math.trunc(Number(v));
|
|
9025
9113
|
return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
|
|
9026
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
|
+
};
|
|
9027
9127
|
function tool(t) {
|
|
9028
9128
|
const out = { ...t };
|
|
9029
9129
|
if (t.risk !== "R0" && !t.confirm)
|
|
@@ -9718,19 +9818,20 @@ var blogTools = [
|
|
|
9718
9818
|
}),
|
|
9719
9819
|
tool({
|
|
9720
9820
|
name: "idea_plan",
|
|
9721
|
-
description: "Add a waiting idea to the
|
|
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.",
|
|
9722
9822
|
parameters: {
|
|
9723
9823
|
type: "object",
|
|
9724
9824
|
properties: {
|
|
9725
9825
|
id: { type: "string" },
|
|
9726
|
-
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.")
|
|
9727
9828
|
},
|
|
9728
9829
|
required: ["id", "title"]
|
|
9729
9830
|
},
|
|
9730
9831
|
group: "blog",
|
|
9731
9832
|
need: write,
|
|
9732
9833
|
risk: "W1",
|
|
9733
|
-
run: async (ctx, { id }) => ws(ctx).planIdea(id)
|
|
9834
|
+
run: async (ctx, { id, position }) => ws(ctx).planIdea(id, positionOf(position))
|
|
9734
9835
|
}),
|
|
9735
9836
|
tool({
|
|
9736
9837
|
name: "idea_discard",
|
|
@@ -10920,6 +11021,8 @@ var opsTools = [
|
|
|
10920
11021
|
characters: String(p.social_copy ?? "").length,
|
|
10921
11022
|
picture: p.image_url ?? null,
|
|
10922
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,
|
|
10923
11026
|
link: p.cta_url ?? p.source_url ?? null,
|
|
10924
11027
|
held: p.held ?? null,
|
|
10925
11028
|
failure: p.failure_message ?? null
|
|
@@ -10947,7 +11050,7 @@ var publishingTools = [
|
|
|
10947
11050
|
}),
|
|
10948
11051
|
tool({
|
|
10949
11052
|
name: "posts_per_day_set",
|
|
10950
|
-
description: "
|
|
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).",
|
|
10951
11054
|
parameters: {
|
|
10952
11055
|
type: "object",
|
|
10953
11056
|
properties: {
|
|
@@ -11510,6 +11613,7 @@ var channelsParam = {
|
|
|
11510
11613
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
11511
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."
|
|
11512
11615
|
};
|
|
11616
|
+
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11513
11617
|
var channelsOf = (value) => {
|
|
11514
11618
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
11515
11619
|
return list2.length ? list2 : void 0;
|
|
@@ -11602,24 +11706,28 @@ var socialTools = [
|
|
|
11602
11706
|
}),
|
|
11603
11707
|
tool({
|
|
11604
11708
|
name: "social_approve",
|
|
11605
|
-
description: "Approve
|
|
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.",
|
|
11606
11710
|
parameters: {
|
|
11607
11711
|
type: "object",
|
|
11608
|
-
properties: {
|
|
11712
|
+
properties: {
|
|
11713
|
+
ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
|
|
11714
|
+
position: queuedAt
|
|
11715
|
+
},
|
|
11609
11716
|
required: ["ids"]
|
|
11610
11717
|
},
|
|
11611
11718
|
group: "social",
|
|
11612
11719
|
need: write5,
|
|
11613
11720
|
risk: "W3",
|
|
11614
11721
|
gates: ["VER", "LI"],
|
|
11615
|
-
run: async (ctx, { ids: ids2, id }) => {
|
|
11722
|
+
run: async (ctx, { ids: ids2, id, position }) => {
|
|
11616
11723
|
const list2 = (Array.isArray(ids2) && ids2.length ? ids2 : id != null ? [id] : []).map(Number).slice(0, 20);
|
|
11617
11724
|
if (!list2.length) throw new Error("Which post?");
|
|
11725
|
+
const at = positionOf(position);
|
|
11618
11726
|
const api2 = ws(ctx);
|
|
11619
11727
|
const results = [];
|
|
11620
11728
|
for (const n of list2) {
|
|
11621
11729
|
try {
|
|
11622
|
-
await api2.approveSocial(n);
|
|
11730
|
+
await api2.approveSocial(n, at);
|
|
11623
11731
|
results.push({ id: n, ok: true });
|
|
11624
11732
|
} catch (e) {
|
|
11625
11733
|
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
@@ -11872,6 +11980,90 @@ var socialTools = [
|
|
|
11872
11980
|
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
11873
11981
|
}
|
|
11874
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
|
+
}),
|
|
11875
12067
|
tool({
|
|
11876
12068
|
name: "social_post_insights",
|
|
11877
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).",
|
|
@@ -11980,13 +12172,14 @@ var socialTools = [
|
|
|
11980
12172
|
}),
|
|
11981
12173
|
tool({
|
|
11982
12174
|
name: "social_from_article",
|
|
11983
|
-
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
|
|
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).",
|
|
11984
12176
|
parameters: {
|
|
11985
12177
|
type: "object",
|
|
11986
12178
|
properties: {
|
|
11987
12179
|
source_id: { type: "string" },
|
|
11988
12180
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
11989
|
-
channels: channelsParam
|
|
12181
|
+
channels: channelsParam,
|
|
12182
|
+
position: queuedAt
|
|
11990
12183
|
},
|
|
11991
12184
|
required: ["source_id", "title"]
|
|
11992
12185
|
},
|
|
@@ -11994,17 +12187,18 @@ var socialTools = [
|
|
|
11994
12187
|
need: write5,
|
|
11995
12188
|
risk: "W2",
|
|
11996
12189
|
gates: ["VER", "LI", "LLM"],
|
|
11997
|
-
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)
|
|
11998
12191
|
}),
|
|
11999
12192
|
tool({
|
|
12000
12193
|
name: "social_plan_days",
|
|
12001
|
-
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).",
|
|
12002
12195
|
parameters: {
|
|
12003
12196
|
type: "object",
|
|
12004
12197
|
properties: {
|
|
12005
12198
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
12006
12199
|
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
12007
|
-
channels: channelsParam
|
|
12200
|
+
channels: channelsParam,
|
|
12201
|
+
position: queuedAt
|
|
12008
12202
|
},
|
|
12009
12203
|
required: ["days"]
|
|
12010
12204
|
},
|
|
@@ -12012,7 +12206,14 @@ var socialTools = [
|
|
|
12012
12206
|
need: write5,
|
|
12013
12207
|
risk: "W2",
|
|
12014
12208
|
gates: ["VER", "LI", "LLM"],
|
|
12015
|
-
run: async (ctx, { days, timezone, channels }) => writing(
|
|
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
|
+
)
|
|
12016
12217
|
}),
|
|
12017
12218
|
tool({
|
|
12018
12219
|
name: "social_ideas_list",
|
|
@@ -12085,13 +12286,14 @@ var socialTools = [
|
|
|
12085
12286
|
}),
|
|
12086
12287
|
tool({
|
|
12087
12288
|
name: "social_idea_write",
|
|
12088
|
-
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).",
|
|
12089
12290
|
parameters: {
|
|
12090
12291
|
type: "object",
|
|
12091
12292
|
properties: {
|
|
12092
12293
|
id: { type: "integer" },
|
|
12093
12294
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
12094
|
-
channels: channelsParam
|
|
12295
|
+
channels: channelsParam,
|
|
12296
|
+
position: queuedAt
|
|
12095
12297
|
},
|
|
12096
12298
|
required: ["id", "title"]
|
|
12097
12299
|
},
|
|
@@ -12099,7 +12301,7 @@ var socialTools = [
|
|
|
12099
12301
|
need: write5,
|
|
12100
12302
|
risk: "W2",
|
|
12101
12303
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
12102
|
-
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)
|
|
12103
12305
|
}),
|
|
12104
12306
|
tool({
|
|
12105
12307
|
name: "social_idea_brief",
|
|
@@ -12225,13 +12427,16 @@ var socialTools = [
|
|
|
12225
12427
|
}),
|
|
12226
12428
|
tool({
|
|
12227
12429
|
name: "social_approve_all",
|
|
12228
|
-
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.",
|
|
12229
|
-
parameters: {
|
|
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
|
+
},
|
|
12230
12435
|
group: "social",
|
|
12231
12436
|
need: write5,
|
|
12232
12437
|
risk: "W3",
|
|
12233
12438
|
gates: ["VER", "LI"],
|
|
12234
|
-
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))
|
|
12235
12440
|
}),
|
|
12236
12441
|
tool({
|
|
12237
12442
|
name: "series_create",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "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",
|