@cmo-so/mcp 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmo-mcp.mjs +340 -65
- 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.1",
|
|
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) + (Array.isArray(a.files) ? a.files.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) + (Array.isArray(a.files) ? a.files.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,18 @@ 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 FILE_SCHEMA = {
|
|
11617
|
+
type: "object",
|
|
11618
|
+
properties: {
|
|
11619
|
+
download_url: { type: "string" },
|
|
11620
|
+
file_id: { type: "string" },
|
|
11621
|
+
mime_type: { type: "string" },
|
|
11622
|
+
file_name: { type: "string" }
|
|
11623
|
+
},
|
|
11624
|
+
required: ["download_url", "file_id"]
|
|
11625
|
+
};
|
|
11626
|
+
var fileLinks = (value) => (Array.isArray(value) ? value : value ? [value] : []).map((f) => String(f?.download_url ?? "").trim()).filter(Boolean);
|
|
11627
|
+
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11513
11628
|
var channelsOf = (value) => {
|
|
11514
11629
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
11515
11630
|
return list2.length ? list2 : void 0;
|
|
@@ -11602,24 +11717,28 @@ var socialTools = [
|
|
|
11602
11717
|
}),
|
|
11603
11718
|
tool({
|
|
11604
11719
|
name: "social_approve",
|
|
11605
|
-
description: "Approve
|
|
11720
|
+
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
11721
|
parameters: {
|
|
11607
11722
|
type: "object",
|
|
11608
|
-
properties: {
|
|
11723
|
+
properties: {
|
|
11724
|
+
ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
|
|
11725
|
+
position: queuedAt
|
|
11726
|
+
},
|
|
11609
11727
|
required: ["ids"]
|
|
11610
11728
|
},
|
|
11611
11729
|
group: "social",
|
|
11612
11730
|
need: write5,
|
|
11613
11731
|
risk: "W3",
|
|
11614
11732
|
gates: ["VER", "LI"],
|
|
11615
|
-
run: async (ctx, { ids: ids2, id }) => {
|
|
11733
|
+
run: async (ctx, { ids: ids2, id, position }) => {
|
|
11616
11734
|
const list2 = (Array.isArray(ids2) && ids2.length ? ids2 : id != null ? [id] : []).map(Number).slice(0, 20);
|
|
11617
11735
|
if (!list2.length) throw new Error("Which post?");
|
|
11736
|
+
const at = positionOf(position);
|
|
11618
11737
|
const api2 = ws(ctx);
|
|
11619
11738
|
const results = [];
|
|
11620
11739
|
for (const n of list2) {
|
|
11621
11740
|
try {
|
|
11622
|
-
await api2.approveSocial(n);
|
|
11741
|
+
await api2.approveSocial(n, at);
|
|
11623
11742
|
results.push({ id: n, ok: true });
|
|
11624
11743
|
} catch (e) {
|
|
11625
11744
|
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
@@ -11872,6 +11991,125 @@ var socialTools = [
|
|
|
11872
11991
|
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
11873
11992
|
}
|
|
11874
11993
|
}),
|
|
11994
|
+
tool({
|
|
11995
|
+
name: "social_pictures_attach",
|
|
11996
|
+
description: "Put the user's own pictures on a post, in order: pictures from this chat (files: ones the user added or that were made here) or public https links to image files (urls), JPEG, PNG or WebP, for example carousel slides. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. If you can pass neither, give the user social_upload_link. Preview first.",
|
|
11997
|
+
parameters: {
|
|
11998
|
+
type: "object",
|
|
11999
|
+
properties: {
|
|
12000
|
+
id: { type: "integer", description: "The post (from social_list), not gone out yet" },
|
|
12001
|
+
files: {
|
|
12002
|
+
type: "array",
|
|
12003
|
+
items: FILE_SCHEMA,
|
|
12004
|
+
description: "Pictures from this chat, in the order they should appear"
|
|
12005
|
+
},
|
|
12006
|
+
urls: {
|
|
12007
|
+
type: "array",
|
|
12008
|
+
items: { type: "string" },
|
|
12009
|
+
description: "Public https links to the image files, in order (after the files, when both are given)"
|
|
12010
|
+
},
|
|
12011
|
+
append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
|
|
12012
|
+
},
|
|
12013
|
+
required: ["id"]
|
|
12014
|
+
},
|
|
12015
|
+
fileParams: ["files"],
|
|
12016
|
+
group: "social",
|
|
12017
|
+
need: write5,
|
|
12018
|
+
risk: "W1",
|
|
12019
|
+
gates: ["VER"],
|
|
12020
|
+
run: async (ctx, { id, urls, files, append }) => {
|
|
12021
|
+
const list2 = [...fileLinks(files), ...(Array.isArray(urls) ? urls : []).map((u) => String(u).trim())].filter(
|
|
12022
|
+
Boolean
|
|
12023
|
+
);
|
|
12024
|
+
if (!list2.length) throw new Error("Give the pictures (files from this chat, or https links).");
|
|
12025
|
+
if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
|
|
12026
|
+
if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
|
|
12027
|
+
const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
|
|
12028
|
+
return {
|
|
12029
|
+
id: out.id,
|
|
12030
|
+
pictures: out.image_urls?.length ? out.image_urls : [out.image_url],
|
|
12031
|
+
carousel: out.carousel ?? null
|
|
12032
|
+
};
|
|
12033
|
+
}
|
|
12034
|
+
}),
|
|
12035
|
+
tool({
|
|
12036
|
+
name: "social_carousel_pdf_attach",
|
|
12037
|
+
description: "A LinkedIn post goes out as this PDF: a swipeable document carousel. The PDF from this chat (file: one the user added or that was made here) or a public https link to it (url); up to 300 pages and 20 MB, no password. Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. If you can pass neither, give the user social_upload_link. Preview first.",
|
|
12038
|
+
parameters: {
|
|
12039
|
+
type: "object",
|
|
12040
|
+
properties: {
|
|
12041
|
+
id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
|
|
12042
|
+
file: { ...FILE_SCHEMA, description: "The PDF from this chat" },
|
|
12043
|
+
url: { type: "string", description: "Public https link to the PDF file" },
|
|
12044
|
+
title: {
|
|
12045
|
+
type: "string",
|
|
12046
|
+
description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
|
|
12047
|
+
}
|
|
12048
|
+
},
|
|
12049
|
+
required: ["id"]
|
|
12050
|
+
},
|
|
12051
|
+
fileParams: ["file"],
|
|
12052
|
+
group: "social",
|
|
12053
|
+
need: write5,
|
|
12054
|
+
risk: "W1",
|
|
12055
|
+
gates: ["VER", "LI"],
|
|
12056
|
+
run: async (ctx, { id, url, file, title }) => {
|
|
12057
|
+
const link = fileLinks(file)[0] ?? String(url ?? "").trim();
|
|
12058
|
+
if (!link.startsWith("https://"))
|
|
12059
|
+
throw new Error("Give the PDF (a file from this chat, or a public https link).");
|
|
12060
|
+
const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
|
|
12061
|
+
return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
|
|
12062
|
+
}
|
|
12063
|
+
}),
|
|
12064
|
+
tool({
|
|
12065
|
+
name: "social_upload_link",
|
|
12066
|
+
description: "A link that opens one post's picture in the CMO.so app, where the user uploads their own photos (up to 20, a carousel on LinkedIn) or, on a computer, a PDF carousel themselves. Use it when you can't pass the files (this chat can't send files to tools, or they are on the user's device). Give the link; nothing changes until they upload.",
|
|
12067
|
+
parameters: {
|
|
12068
|
+
type: "object",
|
|
12069
|
+
properties: { id: { type: "integer", description: "The post (from social_list), not gone out yet" } },
|
|
12070
|
+
required: ["id"]
|
|
12071
|
+
},
|
|
12072
|
+
group: "social",
|
|
12073
|
+
need: read5,
|
|
12074
|
+
risk: "R0",
|
|
12075
|
+
run: async (ctx, { id }) => {
|
|
12076
|
+
const post = await ws(ctx).socialPost(Number(id));
|
|
12077
|
+
if (["published", "publishing", "cancelled"].includes(String(post.status)))
|
|
12078
|
+
throw new Error("That post has already gone out (or was skipped): its pictures can't change.");
|
|
12079
|
+
const site = ctx.domain ? `&site=${encodeURIComponent(ctx.domain)}` : "";
|
|
12080
|
+
return {
|
|
12081
|
+
id: post.id,
|
|
12082
|
+
channel: post.provider ?? null,
|
|
12083
|
+
url: `https://app.cmo.so/social?picture=${post.id}${site}`,
|
|
12084
|
+
how: post.provider === "linkedin" ? "Open it, then Upload several photos (in order), or on a computer Upload a PDF carousel." : "Open it, then Upload several photos (in order)."
|
|
12085
|
+
};
|
|
12086
|
+
}
|
|
12087
|
+
}),
|
|
12088
|
+
tool({
|
|
12089
|
+
name: "social_carousel_set",
|
|
12090
|
+
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.",
|
|
12091
|
+
parameters: {
|
|
12092
|
+
type: "object",
|
|
12093
|
+
properties: {
|
|
12094
|
+
id: { type: "integer", description: "A LinkedIn post with a set of pictures (from social_list)" },
|
|
12095
|
+
format: { type: "string", enum: ["pdf", "images"] },
|
|
12096
|
+
title: { type: "string", description: "Shown on the carousel (up to 120 characters)" }
|
|
12097
|
+
},
|
|
12098
|
+
required: ["id"]
|
|
12099
|
+
},
|
|
12100
|
+
group: "social",
|
|
12101
|
+
need: write5,
|
|
12102
|
+
risk: "W1",
|
|
12103
|
+
gates: ["VER"],
|
|
12104
|
+
run: async (ctx, { id, format, title }) => {
|
|
12105
|
+
if (format == null && title == null) throw new Error("Say the format (pdf or images) or a title.");
|
|
12106
|
+
const out = await ws(ctx).setSocialCarousel(Number(id), {
|
|
12107
|
+
...format ? { format } : {},
|
|
12108
|
+
...title != null ? { title: String(title) } : {}
|
|
12109
|
+
});
|
|
12110
|
+
return { id: out.id, carousel: out.carousel ?? null };
|
|
12111
|
+
}
|
|
12112
|
+
}),
|
|
11875
12113
|
tool({
|
|
11876
12114
|
name: "social_post_insights",
|
|
11877
12115
|
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 +12218,14 @@ var socialTools = [
|
|
|
11980
12218
|
}),
|
|
11981
12219
|
tool({
|
|
11982
12220
|
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
|
|
12221
|
+
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
12222
|
parameters: {
|
|
11985
12223
|
type: "object",
|
|
11986
12224
|
properties: {
|
|
11987
12225
|
source_id: { type: "string" },
|
|
11988
12226
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
11989
|
-
channels: channelsParam
|
|
12227
|
+
channels: channelsParam,
|
|
12228
|
+
position: queuedAt
|
|
11990
12229
|
},
|
|
11991
12230
|
required: ["source_id", "title"]
|
|
11992
12231
|
},
|
|
@@ -11994,17 +12233,18 @@ var socialTools = [
|
|
|
11994
12233
|
need: write5,
|
|
11995
12234
|
risk: "W2",
|
|
11996
12235
|
gates: ["VER", "LI", "LLM"],
|
|
11997
|
-
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
12236
|
+
run: async (ctx, { source_id, channels, position }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels), positionOf(position))).job)
|
|
11998
12237
|
}),
|
|
11999
12238
|
tool({
|
|
12000
12239
|
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).",
|
|
12240
|
+
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
12241
|
parameters: {
|
|
12003
12242
|
type: "object",
|
|
12004
12243
|
properties: {
|
|
12005
12244
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
12006
12245
|
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
12007
|
-
channels: channelsParam
|
|
12246
|
+
channels: channelsParam,
|
|
12247
|
+
position: queuedAt
|
|
12008
12248
|
},
|
|
12009
12249
|
required: ["days"]
|
|
12010
12250
|
},
|
|
@@ -12012,7 +12252,14 @@ var socialTools = [
|
|
|
12012
12252
|
need: write5,
|
|
12013
12253
|
risk: "W2",
|
|
12014
12254
|
gates: ["VER", "LI", "LLM"],
|
|
12015
|
-
run: async (ctx, { days, timezone, channels }) => writing(
|
|
12255
|
+
run: async (ctx, { days, timezone, channels, position }) => writing(
|
|
12256
|
+
(await ws(ctx).startSocialPlan(
|
|
12257
|
+
int(days, 1, 7, 3),
|
|
12258
|
+
timezone || "UTC",
|
|
12259
|
+
channelsOf(channels),
|
|
12260
|
+
positionOf(position)
|
|
12261
|
+
)).job
|
|
12262
|
+
)
|
|
12016
12263
|
}),
|
|
12017
12264
|
tool({
|
|
12018
12265
|
name: "social_ideas_list",
|
|
@@ -12085,13 +12332,14 @@ var socialTools = [
|
|
|
12085
12332
|
}),
|
|
12086
12333
|
tool({
|
|
12087
12334
|
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).",
|
|
12335
|
+
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
12336
|
parameters: {
|
|
12090
12337
|
type: "object",
|
|
12091
12338
|
properties: {
|
|
12092
12339
|
id: { type: "integer" },
|
|
12093
12340
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
12094
|
-
channels: channelsParam
|
|
12341
|
+
channels: channelsParam,
|
|
12342
|
+
position: queuedAt
|
|
12095
12343
|
},
|
|
12096
12344
|
required: ["id", "title"]
|
|
12097
12345
|
},
|
|
@@ -12099,7 +12347,7 @@ var socialTools = [
|
|
|
12099
12347
|
need: write5,
|
|
12100
12348
|
risk: "W2",
|
|
12101
12349
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
12102
|
-
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
12350
|
+
run: async (ctx, { id, channels, position }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels), positionOf(position))).job)
|
|
12103
12351
|
}),
|
|
12104
12352
|
tool({
|
|
12105
12353
|
name: "social_idea_brief",
|
|
@@ -12225,13 +12473,16 @@ var socialTools = [
|
|
|
12225
12473
|
}),
|
|
12226
12474
|
tool({
|
|
12227
12475
|
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: {
|
|
12476
|
+
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).",
|
|
12477
|
+
parameters: {
|
|
12478
|
+
type: "object",
|
|
12479
|
+
properties: { channel: { type: "string", enum: CHANNEL_KEYS }, position: queuedAt }
|
|
12480
|
+
},
|
|
12230
12481
|
group: "social",
|
|
12231
12482
|
need: write5,
|
|
12232
12483
|
risk: "W3",
|
|
12233
12484
|
gates: ["VER", "LI"],
|
|
12234
|
-
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
12485
|
+
run: async (ctx, { channel, position }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0], positionOf(position))
|
|
12235
12486
|
}),
|
|
12236
12487
|
tool({
|
|
12237
12488
|
name: "series_create",
|
|
@@ -22962,6 +23213,30 @@ function description(t) {
|
|
|
22962
23213
|
].filter(Boolean);
|
|
22963
23214
|
return `${t.description}${notes.length ? ` (${notes.join("; ")}.)` : ""} Changes data: call without confirm for a preview, then with confirm: true and the confirmation after the user agrees.`;
|
|
22964
23215
|
}
|
|
23216
|
+
function fileMeta(t) {
|
|
23217
|
+
return t.fileParams?.length ? { "openai/fileParams": t.fileParams } : void 0;
|
|
23218
|
+
}
|
|
23219
|
+
function withFreshFiles(t, signed, now) {
|
|
23220
|
+
if (!t.fileParams?.length) return signed;
|
|
23221
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
23222
|
+
const collect = (v) => {
|
|
23223
|
+
for (const f of Array.isArray(v) ? v : [v])
|
|
23224
|
+
if (f && typeof f === "object" && typeof f.file_id === "string")
|
|
23225
|
+
fresh.set(f.file_id, f);
|
|
23226
|
+
};
|
|
23227
|
+
for (const p of t.fileParams) collect(now[p]);
|
|
23228
|
+
const swap = (v) => {
|
|
23229
|
+
if (Array.isArray(v)) return v.map(swap);
|
|
23230
|
+
if (v && typeof v === "object" && typeof v.file_id === "string") {
|
|
23231
|
+
const f = fresh.get(v.file_id);
|
|
23232
|
+
return f && typeof f.download_url === "string" ? { ...v, download_url: f.download_url } : v;
|
|
23233
|
+
}
|
|
23234
|
+
return v;
|
|
23235
|
+
};
|
|
23236
|
+
const out = { ...signed };
|
|
23237
|
+
for (const p of t.fileParams) if (p in out) out[p] = swap(out[p]);
|
|
23238
|
+
return out;
|
|
23239
|
+
}
|
|
22965
23240
|
function toolArgs(args) {
|
|
22966
23241
|
const out = {};
|
|
22967
23242
|
for (const [k, v] of Object.entries(args ?? {})) if (!CONTROL_ARGS.includes(k) && v !== void 0) out[k] = v;
|
|
@@ -23091,7 +23366,7 @@ function createMcpServer(deps) {
|
|
|
23091
23366
|
description: description(t),
|
|
23092
23367
|
inputSchema: inputSchema(t),
|
|
23093
23368
|
annotations: { title: t.name.replace(/_/g, " "), ...annotations(t) },
|
|
23094
|
-
...deps.widgets && toolMeta(t.name) ? { _meta: toolMeta(t.name) } : {}
|
|
23369
|
+
...fileMeta(t) || deps.widgets && toolMeta(t.name) ? { _meta: { ...deps.widgets ? toolMeta(t.name) : {}, ...fileMeta(t) } } : {}
|
|
23095
23370
|
}))
|
|
23096
23371
|
};
|
|
23097
23372
|
});
|
|
@@ -23127,7 +23402,7 @@ function createMcpServer(deps) {
|
|
|
23127
23402
|
return fail(
|
|
23128
23403
|
access.keyAccess === "read" && isWrite(t) ? "This key can only read. Reconnect with write access to make changes." : `You don't have permission to do this on ${domain ?? "this website"}.`
|
|
23129
23404
|
);
|
|
23130
|
-
const callArgs = approval ? approval.calls[0]?.args ?? {} : toolArgs(args);
|
|
23405
|
+
const callArgs = approval ? withFreshFiles(t, approval.calls[0]?.args ?? {}, toolArgs(args)) : toolArgs(args);
|
|
23131
23406
|
const ctx = { api: api2, domain, client: "mcp" };
|
|
23132
23407
|
if (!isWrite(t)) return ok(await t.run(ctx, callArgs), viewForTool(t.name) ? { workspace: domain } : void 0);
|
|
23133
23408
|
let standing = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|