@cmo-so/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmo.mjs +320 -64
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7609,8 +7609,11 @@ function domainApi(api2, domain) {
|
|
|
7609
7609
|
}),
|
|
7610
7610
|
/** This website's topics, for assigning an idea to one. */
|
|
7611
7611
|
topics: () => api2.request(`${base}/topics`),
|
|
7612
|
-
/** Add a waiting idea to the end
|
|
7613
|
-
planIdea: (id) => post(
|
|
7612
|
+
/** Add a waiting idea to the plan: at the end, or at `position` (1-based in the answer). */
|
|
7613
|
+
planIdea: (id, position) => post(
|
|
7614
|
+
`${base}/ideas/${encodeURIComponent(id)}/plan`,
|
|
7615
|
+
position != null ? { position } : void 0
|
|
7616
|
+
),
|
|
7614
7617
|
/** One step up or down the 1-10 priority the planner sorts ideas by. */
|
|
7615
7618
|
ideaPriority: (id, delta) => post(`${base}/ideas/${encodeURIComponent(id)}/priority`, { delta }),
|
|
7616
7619
|
/** Take a post out of the plan; its idea can be restored to Ideas. */
|
|
@@ -7707,8 +7710,8 @@ function domainApi(api2, domain) {
|
|
|
7707
7710
|
...source ? { sitemap_url: source } : {}
|
|
7708
7711
|
}),
|
|
7709
7712
|
discardIdea: (id, restore = false) => post(`${base}/ideas/${encodeURIComponent(id)}/discard${restore ? "?restore=true" : ""}`),
|
|
7710
|
-
/** OK a draft (or retry a failed post): it goes to the front of the queue
|
|
7711
|
-
approveSocial: (id) => post(`${base}/social/${id}/approve
|
|
7713
|
+
/** OK a draft (or retry a failed post): it goes to the front of the queue, or to `position`; the post comes back with its eta. */
|
|
7714
|
+
approveSocial: (id, position) => post(`${base}/social/${id}/approve`, position != null ? { position } : void 0),
|
|
7712
7715
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7713
7716
|
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7714
7717
|
/** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
|
|
@@ -7784,9 +7787,15 @@ function domainApi(api2, domain) {
|
|
|
7784
7787
|
/** "Looks good, start": setup done, and Maggie writes the first week's posts. */
|
|
7785
7788
|
socialSetupConfirm: () => post(`${base}/social/setup/confirm`),
|
|
7786
7789
|
/** OK every post waiting for you (not a series that hasn't been approved), on one channel or all. */
|
|
7787
|
-
approveAllSocial: (channel) =>
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
+
approveAllSocial: (channel, position) => {
|
|
7791
|
+
const q2 = new URLSearchParams();
|
|
7792
|
+
if (channel) q2.set("channel", channel);
|
|
7793
|
+
if (position != null) q2.set("position", String(position));
|
|
7794
|
+
const qs = q2.toString();
|
|
7795
|
+
return post(
|
|
7796
|
+
`${base}/social/approve-all${qs ? `?${qs}` : ""}`
|
|
7797
|
+
);
|
|
7798
|
+
},
|
|
7790
7799
|
/** A Facebook sign-in link: after it the user lands on returnTo with ?facebook=connected (or =error). */
|
|
7791
7800
|
connectFacebook: (returnTo = "close") => post(`${base}/social/facebook/connect`, { return_to: returnTo }),
|
|
7792
7801
|
/** The Facebook Pages you connected, and the one this website posts to. */
|
|
@@ -7902,6 +7911,19 @@ function domainApi(api2, domain) {
|
|
|
7902
7911
|
photo,
|
|
7903
7912
|
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7904
7913
|
}),
|
|
7914
|
+
/** Your own set of pictures for the post, in order (up to 20; free, no AI): base64 photos and/or public
|
|
7915
|
+
* https links to pictures. Send a few photos per call (16 MB request limit), `append` after the first.
|
|
7916
|
+
* On LinkedIn a set goes out as a swipeable PDF carousel unless setSocialCarousel says images. */
|
|
7917
|
+
setSocialPictures: (id, pictures) => post(`${base}/social/${id}/pictures`, pictures),
|
|
7918
|
+
/** A LinkedIn post goes out as this PDF (base64 under ~11 MB, or a public https link; up to 300 pages);
|
|
7919
|
+
* its first 20 pages become the post's pictures. */
|
|
7920
|
+
attachSocialCarouselPdf: (id, pdf) => post(`${base}/social/${id}/carousel/pdf`, pdf),
|
|
7921
|
+
/** How a LinkedIn post with a set goes out: "pdf" (swipeable carousel, the default) or "images"; its title. */
|
|
7922
|
+
setSocialCarousel: (id, changes) => api2.request(`${base}/social/${id}/carousel`, {
|
|
7923
|
+
method: "PATCH",
|
|
7924
|
+
headers: { "Content-Type": "application/json" },
|
|
7925
|
+
body: JSON.stringify(changes)
|
|
7926
|
+
}),
|
|
7905
7927
|
/** A new picture for every post here without one (or these), one after another in the background. */
|
|
7906
7928
|
fillSocialImages: (opts = {}) => post(
|
|
7907
7929
|
`${base}/social/images/fill`,
|
|
@@ -7935,20 +7957,24 @@ function domainApi(api2, domain) {
|
|
|
7935
7957
|
background: true,
|
|
7936
7958
|
...channels ? { channels } : {}
|
|
7937
7959
|
}),
|
|
7938
|
-
|
|
7960
|
+
/** `position`: where the posts are queued when approval mode is auto (the front by default). */
|
|
7961
|
+
startSocialFromPost: (sourcePostId, channels, position) => post(`${base}/social/from-post`, {
|
|
7939
7962
|
source_post_id: sourcePostId,
|
|
7940
7963
|
background: true,
|
|
7941
|
-
...channels ? { channels } : {}
|
|
7964
|
+
...channels ? { channels } : {},
|
|
7965
|
+
...position != null ? { position } : {}
|
|
7942
7966
|
}),
|
|
7943
|
-
startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7967
|
+
startSocialPlan: (days, timezone, channels, position) => post(`${base}/social/plan`, {
|
|
7944
7968
|
days,
|
|
7945
7969
|
timezone,
|
|
7946
7970
|
background: true,
|
|
7947
|
-
...channels ? { channels } : {}
|
|
7971
|
+
...channels ? { channels } : {},
|
|
7972
|
+
...position != null ? { position } : {}
|
|
7948
7973
|
}),
|
|
7949
|
-
startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
|
|
7974
|
+
startWriteSocialIdea: (id, channels, position) => post(`${base}/social/ideas/${id}/write`, {
|
|
7950
7975
|
background: true,
|
|
7951
|
-
...channels ? { channels } : {}
|
|
7976
|
+
...channels ? { channels } : {},
|
|
7977
|
+
...position != null ? { position } : {}
|
|
7952
7978
|
}),
|
|
7953
7979
|
/** Disconnect the website's LinkedIn account. */
|
|
7954
7980
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
@@ -8121,10 +8147,12 @@ function domainApi(api2, domain) {
|
|
|
8121
8147
|
}),
|
|
8122
8148
|
// docs/19 P3: autopilot cadence and publish-now.
|
|
8123
8149
|
autopilot: () => api2.request(`${base}/autopilot`),
|
|
8124
|
-
/** Posts per day
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8150
|
+
/** Posts per day (one of `choices`, up to the plan's limit). Pushed blogs: how many
|
|
8151
|
+
* AI CMO sends a day. Pull sites: the most the site collects a day; null = the plan's. */
|
|
8152
|
+
setPostsPerDay: (postsPerDay) => put(
|
|
8153
|
+
`${base}/autopilot/cadence`,
|
|
8154
|
+
{ posts_per_day: postsPerDay }
|
|
8155
|
+
),
|
|
8128
8156
|
/** Publish a ready draft to the connected site now (one post from today's quota). */
|
|
8129
8157
|
publishNow: (id) => post(
|
|
8130
8158
|
`${base}/posts/${encodeURIComponent(id)}/publish`
|
|
@@ -8195,6 +8223,8 @@ function domainApi(api2, domain) {
|
|
|
8195
8223
|
inboxReport: (id) => api2.request(`${base}/reports/${id}`),
|
|
8196
8224
|
saveReport: (r) => post(`${base}/reports`, r),
|
|
8197
8225
|
readAllReports: () => post(`${base}/reports/read-all`, {}),
|
|
8226
|
+
/** One social post in full (social_list clips its text). */
|
|
8227
|
+
socialPost: (id) => api2.request(`${base}/social/${id}`),
|
|
8198
8228
|
// Ops Autopilot: ideas the weekly clean-up archived (restorable), post health, Google reviews.
|
|
8199
8229
|
ideaCleanup: (limit = 50) => api2.request(`${base}/idea-cleanup?limit=${limit}`),
|
|
8200
8230
|
restoreIdeaCleanup: (id) => post(`${base}/idea-cleanup/${id}/restore`, {}),
|
|
@@ -8407,6 +8437,14 @@ var CHANNEL_EN = {
|
|
|
8407
8437
|
google_business: "Google Business"
|
|
8408
8438
|
};
|
|
8409
8439
|
var CHANNEL_ZH = { ...CHANNEL_EN, google_business: "Google \u5546\u5BB6" };
|
|
8440
|
+
var channelsEn = (a) => {
|
|
8441
|
+
const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_EN[String(c)] ?? String(c));
|
|
8442
|
+
return list2.length ? list2.join(list2.length === 2 ? " and " : ", ") : null;
|
|
8443
|
+
};
|
|
8444
|
+
var channelsZh = (a) => {
|
|
8445
|
+
const list2 = (Array.isArray(a.channels) ? a.channels : []).map((c) => CHANNEL_ZH[String(c)] ?? String(c));
|
|
8446
|
+
return list2.length ? list2.join("\u3001") : null;
|
|
8447
|
+
};
|
|
8410
8448
|
var named = (title, id) => {
|
|
8411
8449
|
if (title != null && String(title).trim()) return ` ${q(title)}`;
|
|
8412
8450
|
const s = String(id ?? "").trim();
|
|
@@ -8416,6 +8454,20 @@ var list = (xs, n = 5) => {
|
|
|
8416
8454
|
const a = (Array.isArray(xs) ? xs : xs == null ? [] : [xs]).map(String);
|
|
8417
8455
|
return a.length > n ? `${a.slice(0, n).join(", ")} +${a.length - n}` : a.join(", ");
|
|
8418
8456
|
};
|
|
8457
|
+
var placeEn = (p, items = "posts") => {
|
|
8458
|
+
const t = String(p ?? "").trim().toLowerCase();
|
|
8459
|
+
if (t === "end") return `at the end of the queue, after the ${items} already waiting`;
|
|
8460
|
+
if (t === "random") return "at a random place in the queue";
|
|
8461
|
+
if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `after the first ${t} in the queue`;
|
|
8462
|
+
return null;
|
|
8463
|
+
};
|
|
8464
|
+
var placeZh = (p) => {
|
|
8465
|
+
const t = String(p ?? "").trim().toLowerCase();
|
|
8466
|
+
if (t === "end") return "\u6392\u5728\u968A\u5217\u6700\u5F8C\uFF0C\u5728\u5DF2\u5728\u7B49\u5F85\u7684\u4E4B\u5F8C";
|
|
8467
|
+
if (t === "random") return "\u96A8\u6A5F\u6392\u9032\u968A\u5217";
|
|
8468
|
+
if (/^\d+$/.test(t)) return Number(t) === 0 ? null : `\u6392\u5728\u968A\u5217\u524D ${t} \u7BC7\u4E4B\u5F8C`;
|
|
8469
|
+
return null;
|
|
8470
|
+
};
|
|
8419
8471
|
var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
|
|
8420
8472
|
var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
8421
8473
|
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
@@ -8575,8 +8627,16 @@ var confirmCopy = {
|
|
|
8575
8627
|
"zh-Hant": (a) => `${Number(a.delta) > 0 ? "\u63D0\u9AD8" : "\u964D\u4F4E"}${named(a.title, a.id)} \u7684\u512A\u5148\u7D1A\u3002`
|
|
8576
8628
|
},
|
|
8577
8629
|
idea_plan: {
|
|
8578
|
-
en: (a) =>
|
|
8579
|
-
|
|
8630
|
+
en: (a) => {
|
|
8631
|
+
const t = String(a.position ?? "").trim().toLowerCase();
|
|
8632
|
+
const where = t === "front" || t === "0" ? "the front of the plan" : t === "random" ? "a random place in the plan" : /^\d+$/.test(t) ? `the plan, after the first ${t} planned posts` : "the end of the plan";
|
|
8633
|
+
return `Add${named(a.title, a.id)} to ${where}.`;
|
|
8634
|
+
},
|
|
8635
|
+
"zh-Hant": (a) => {
|
|
8636
|
+
const t = String(a.position ?? "").trim().toLowerCase();
|
|
8637
|
+
const where = t === "front" || t === "0" ? "\u8A08\u5283\u6700\u524D\u9762" : t === "random" ? "\u8A08\u5283\u4E2D\u7684\u96A8\u6A5F\u4F4D\u7F6E" : /^\d+$/.test(t) ? `\u8A08\u5283\u4E2D\u524D ${t} \u7BC7\u4E4B\u5F8C` : "\u8A08\u5283\u6700\u5F8C";
|
|
8638
|
+
return `\u628A${named(a.title, a.id)} \u52A0\u5230${where}\u3002`;
|
|
8639
|
+
}
|
|
8580
8640
|
},
|
|
8581
8641
|
idea_discard: {
|
|
8582
8642
|
en: (a) => `Remove the idea${named(a.title, a.id)}.`,
|
|
@@ -8646,23 +8706,29 @@ var confirmCopy = {
|
|
|
8646
8706
|
},
|
|
8647
8707
|
// ----------------------------------------------------------------- social
|
|
8648
8708
|
social_update_copy: {
|
|
8649
|
-
en: (a) => `Change the text of
|
|
8650
|
-
"zh-Hant": (a) => `\u4FEE\u6539
|
|
8709
|
+
en: (a) => `Change the text of post #${a.id}.`,
|
|
8710
|
+
"zh-Hant": (a) => `\u4FEE\u6539\u8CBC\u6587 #${a.id} \u7684\u5167\u5BB9\u3002`
|
|
8651
8711
|
},
|
|
8652
8712
|
social_approve: {
|
|
8653
8713
|
en: (a) => {
|
|
8654
8714
|
const n = ids(a);
|
|
8655
|
-
|
|
8715
|
+
const at = placeEn(a.position);
|
|
8716
|
+
if (at)
|
|
8717
|
+
return `Approve ${n.length > 1 ? `${n.length} posts: ${n.map((i) => `#${i}`).join(", ")}` : `post #${n[0]}`}. ${n.length > 1 ? "They are" : "It is"} queued ${at}, and go${n.length > 1 ? "" : "es"} out where your network can see ${n.length > 1 ? "them" : "it"}.`;
|
|
8718
|
+
return n.length > 1 ? `Approve ${n.length} posts: ${n.map((i) => `#${i}`).join(", ")}. Each goes out at your next posting times, where your network can see it.` : `Approve post #${n[0]}. It goes out at your next posting time, where your network can see it.`;
|
|
8656
8719
|
},
|
|
8657
8720
|
"zh-Hant": (a) => {
|
|
8658
8721
|
const n = ids(a);
|
|
8659
|
-
|
|
8722
|
+
const at = placeZh(a.position);
|
|
8723
|
+
if (at)
|
|
8724
|
+
return `\u6279\u51C6${n.length > 1 ? ` ${n.length} \u7BC7\u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}` : `\u8CBC\u6587 #${n[0]}`}\u3002${at}\uFF0C\u767C\u51FA\u5F8C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
|
|
8725
|
+
return n.length > 1 ? `\u6279\u51C6 ${n.length} \u7BC7\u8CBC\u6587\uFF1A${n.map((i) => `#${i}`).join("\u3001")}\u3002\u6BCF\u7BC7\u5728\u63A5\u4E0B\u4F86\u7684\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002` : `\u6279\u51C6\u8CBC\u6587 #${n[0]}\u3002\u5B83\u6703\u5728\u4E0B\u4E00\u500B\u767C\u6587\u6642\u9593\u767C\u51FA\uFF0C\u4F60\u7684\u4EBA\u8108\u90FD\u770B\u5F97\u5230\u3002`;
|
|
8660
8726
|
}
|
|
8661
8727
|
},
|
|
8662
|
-
social_skip: { en: (a) => `Skip
|
|
8728
|
+
social_skip: { en: (a) => `Skip post #${a.id}.`, "zh-Hant": (a) => `\u8DF3\u904E\u8CBC\u6587 #${a.id}\u3002` },
|
|
8663
8729
|
social_restore: {
|
|
8664
|
-
en: (a) => `Bring back
|
|
8665
|
-
"zh-Hant": (a) => `\u6062\u5FA9
|
|
8730
|
+
en: (a) => `Bring back post #${a.id}.`,
|
|
8731
|
+
"zh-Hant": (a) => `\u6062\u5FA9\u8CBC\u6587 #${a.id}\u3002`
|
|
8666
8732
|
},
|
|
8667
8733
|
youtube_connect_link: {
|
|
8668
8734
|
en: () => "Make a link to connect your YouTube channel.",
|
|
@@ -8703,28 +8769,52 @@ var confirmCopy = {
|
|
|
8703
8769
|
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8704
8770
|
},
|
|
8705
8771
|
social_image_new: {
|
|
8706
|
-
en: (a) => `Make a new picture for
|
|
8707
|
-
"zh-Hant": (a) => `\u70BA
|
|
8772
|
+
en: (a) => `Make a new picture for post #${a.id}${a.instruction ? `: ${String(a.instruction).slice(0, 300)}` : ""} (1 AI generation, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.`,
|
|
8773
|
+
"zh-Hant": (a) => `\u70BA\u8CBC\u6587 #${a.id} \u751F\u6210\u65B0\u5716\u7247${a.instruction ? `\uFF1A${String(a.instruction).slice(0, 300)}` : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002`
|
|
8708
8774
|
},
|
|
8709
8775
|
social_image_stop: {
|
|
8710
|
-
en: () => "Stop the
|
|
8711
|
-
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684
|
|
8776
|
+
en: () => "Stop the picture being made; the post keeps the one it had.",
|
|
8777
|
+
"zh-Hant": () => "\u505C\u6B62\u6B63\u5728\u751F\u6210\u7684\u5716\u7247\uFF1B\u8CBC\u6587\u4FDD\u7559\u539F\u4F86\u7684\u5716\u7247\u3002"
|
|
8778
|
+
},
|
|
8779
|
+
social_pictures_attach: {
|
|
8780
|
+
en: (a) => {
|
|
8781
|
+
const n = Array.isArray(a.urls) ? a.urls.length : 0;
|
|
8782
|
+
return `${a.append ? "Add" : "Put"} your ${n} picture${n === 1 ? "" : "s"} ${a.append ? "after the pictures on" : "on"} post #${a.id}${a.append ? "" : ", in place of its picture"} (free). On LinkedIn a set goes out as a swipeable PDF carousel.`;
|
|
8783
|
+
},
|
|
8784
|
+
"zh-Hant": (a) => {
|
|
8785
|
+
const n = Array.isArray(a.urls) ? a.urls.length : 0;
|
|
8786
|
+
return `${a.append ? `\u5728\u8CBC\u6587 #${a.id} \u73FE\u6709\u5716\u7247\u5F8C\u52A0\u4E0A` : `\u628A\u8CBC\u6587 #${a.id} \u7684\u5716\u7247\u63DB\u6210`}\u4F60\u7684 ${n} \u5F35\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002\u5728 LinkedIn \u4E0A\uFF0C\u4E00\u7D44\u5716\u7247\u6703\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA\u3002`;
|
|
8787
|
+
}
|
|
8788
|
+
},
|
|
8789
|
+
social_carousel_pdf_attach: {
|
|
8790
|
+
en: (a) => `LinkedIn post #${a.id} goes out as your PDF, a swipeable carousel${a.title ? ` titled ${q(a.title)}` : ""}; its first pages become the post's pictures (free).`,
|
|
8791
|
+
"zh-Hant": (a) => `LinkedIn \u8CBC\u6587 #${a.id} \u6703\u4EE5\u4F60\u7684 PDF \u767C\u51FA\uFF0C\u6210\u70BA\u53EF\u6ED1\u52D5\u7684\u8F2A\u64AD${a.title ? `\uFF0C\u6A19\u984C ${q(a.title)}` : ""}\uFF1BPDF \u7684\u524D\u5E7E\u9801\u4E5F\u6703\u6210\u70BA\u8CBC\u6587\u7684\u5716\u7247\uFF08\u514D\u8CBB\uFF09\u3002`
|
|
8792
|
+
},
|
|
8793
|
+
social_carousel_set: {
|
|
8794
|
+
en: (a) => `LinkedIn post #${a.id} goes out ${a.format === "images" ? "as several pictures" : a.format === "pdf" ? "as a swipeable PDF carousel" : "as it is set"}${a.title ? `, titled ${q(a.title)}` : ""}.`,
|
|
8795
|
+
"zh-Hant": (a) => `LinkedIn \u8CBC\u6587 #${a.id} ${a.format === "images" ? "\u4EE5\u591A\u5F35\u5716\u7247\u767C\u51FA" : a.format === "pdf" ? "\u4EE5\u53EF\u6ED1\u52D5\u7684 PDF \u8F2A\u64AD\u767C\u51FA" : "\u7167\u76EE\u524D\u8A2D\u5B9A\u767C\u51FA"}${a.title ? `\uFF0C\u6A19\u984C ${q(a.title)}` : ""}\u3002`
|
|
8712
8796
|
},
|
|
8713
8797
|
social_move: {
|
|
8714
|
-
en: (a) => `Move
|
|
8715
|
-
"zh-Hant": (a) => `\u628A
|
|
8798
|
+
en: (a) => `Move post #${a.id} ${a.to === "top" ? "to the top of its channel's queue" : `${a.to} in its channel's queue`}.`,
|
|
8799
|
+
"zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} ${a.to === "top" ? "\u79FB\u5230\u8A72\u6E20\u9053\u968A\u5217\u7684\u6700\u524D" : a.to === "up" ? "\u5728\u8A72\u6E20\u9053\u968A\u5217\u4E2D\u4E0A\u79FB" : "\u5728\u8A72\u6E20\u9053\u968A\u5217\u4E2D\u4E0B\u79FB"}\u3002`
|
|
8716
8800
|
},
|
|
8717
8801
|
social_set_paused: {
|
|
8718
|
-
en: (a) =>
|
|
8719
|
-
|
|
8802
|
+
en: (a) => {
|
|
8803
|
+
const on2 = a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} posting` : "posting on every channel";
|
|
8804
|
+
return a.paused ? `Pause ${on2}.` : `Resume ${on2}.`;
|
|
8805
|
+
},
|
|
8806
|
+
"zh-Hant": (a) => {
|
|
8807
|
+
const on2 = a.channel ? `${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\u6240\u6709\u6E20\u9053\u7684";
|
|
8808
|
+
return a.paused ? `\u66AB\u505C${on2}\u767C\u6587\u3002` : `\u6062\u5FA9${on2}\u767C\u6587\u3002`;
|
|
8809
|
+
}
|
|
8720
8810
|
},
|
|
8721
8811
|
social_from_article: {
|
|
8722
|
-
en: (a) => `Write a
|
|
8723
|
-
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)}
|
|
8812
|
+
en: (a) => `Write ${channelsEn(a) ? `a ${channelsEn(a)} post` : "a post for each connected channel"} about ${q(a.title || a.source_id)}; it waits for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then it is queued ${placeEn(a.position)})` : ""}.`,
|
|
8813
|
+
"zh-Hant": (a) => `\u6839\u64DA ${q(a.title || a.source_id)} ${channelsZh(a) ? `\u5BEB\u4E00\u7BC7 ${channelsZh(a)} \u8CBC\u6587` : "\u70BA\u6BCF\u500B\u5DF2\u9023\u63A5\u7684\u6E20\u9053\u5404\u5BEB\u4E00\u7BC7\u8CBC\u6587"}\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
|
|
8724
8814
|
},
|
|
8725
8815
|
social_plan_days: {
|
|
8726
|
-
en: (a) => `Write
|
|
8727
|
-
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684
|
|
8816
|
+
en: (a) => `Write ${channelsEn(a) ? `${channelsEn(a)} posts` : "posts on each connected channel"} for the next ${a.days} day${Number(a.days) === 1 ? "" : "s"} from your best articles; they wait for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then they are queued ${placeEn(a.position)})` : ""}.`,
|
|
8817
|
+
"zh-Hant": (a) => `\u7528\u4F60\u6700\u597D\u7684\u6587\u7AE0\u5BEB\u672A\u4F86 ${a.days} \u5929\u7684${channelsZh(a) ? ` ${channelsZh(a)} ` : "\u5404\u6E20\u9053"}\u8CBC\u6587\uFF1B\u9664\u975E\u4F60\u8A2D\u5B9A\u8CBC\u6587\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
|
|
8728
8818
|
},
|
|
8729
8819
|
social_idea_add: {
|
|
8730
8820
|
en: (a) => `Add the LinkedIn idea ${q(a.title)}.`,
|
|
@@ -8739,8 +8829,8 @@ var confirmCopy = {
|
|
|
8739
8829
|
"zh-Hant": (a) => `\u522A\u9664 LinkedIn idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}\u3002`
|
|
8740
8830
|
},
|
|
8741
8831
|
social_idea_write: {
|
|
8742
|
-
en: (a) => `Write the
|
|
8743
|
-
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB
|
|
8832
|
+
en: (a) => `Write ${channelsEn(a) ? `the ${channelsEn(a)} post` : "a post for each connected channel"} for idea #${a.id}${a.title ? ` ${q(a.title)}` : ""}; your own idea's posts wait for your OK unless your posts go out on their own${placeEn(a.position) ? ` (then it is queued ${placeEn(a.position)})` : ""}.`,
|
|
8833
|
+
"zh-Hant": (a) => `\u70BA idea #${a.id}${a.title ? ` ${q(a.title)}` : ""} \u5BEB${channelsZh(a) ? ` ${channelsZh(a)} ` : "\u5404\u6E20\u9053\u7684"}\u8CBC\u6587\uFF1B\u4F60\u81EA\u5DF1 idea \u7684\u8CBC\u6587\uFF0C\u9664\u975E\u4F60\u8A2D\u5B9A\u81EA\u52D5\u767C\u51FA\uFF0C\u5426\u5247\u6703\u7B49\u4F60\u6279\u51C6${placeZh(a.position) ? `\uFF08\u81EA\u52D5\u767C\u51FA\u6642${placeZh(a.position)}\uFF09` : ""}\u3002`
|
|
8744
8834
|
},
|
|
8745
8835
|
social_idea_save: {
|
|
8746
8836
|
en: (a) => `Save this LinkedIn post for idea #${a.id} (one AI generation): ${q(a.text)}`,
|
|
@@ -8806,8 +8896,8 @@ var confirmCopy = {
|
|
|
8806
8896
|
"zh-Hant": () => "\u7522\u751F\u4E00\u500B\u9023\u63A5 LinkedIn \u7684\u9023\u7D50\u3002"
|
|
8807
8897
|
},
|
|
8808
8898
|
social_approve_all: {
|
|
8809
|
-
en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times, where people can see them.`,
|
|
8810
|
-
"zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\
|
|
8899
|
+
en: (a) => `Approve every ${a.channel ? `${CHANNEL_EN[String(a.channel)] ?? a.channel} ` : ""}post waiting for your OK${a.channel ? "" : ", on every channel"}, including any held for a picture check. They go out one by one at each channel's posting times${placeEn(a.position) ? `, queued ${placeEn(a.position)}` : ""}, where people can see them.`,
|
|
8900
|
+
"zh-Hant": (a) => `\u6279\u51C6\u6240\u6709\u7B49\u4F60\u78BA\u8A8D\u7684${a.channel ? ` ${CHANNEL_ZH[String(a.channel)] ?? a.channel} ` : "\uFF08\u6240\u6709\u6E20\u9053\uFF09"}\u8CBC\u6587\uFF0C\u5305\u62EC\u56E0\u5716\u7247\u6AA2\u67E5\u800C\u66AB\u505C\u7684\u3002\u5B83\u5011\u6703\u5728\u5404\u6E20\u9053\u7684\u767C\u6587\u6642\u9593\u9010\u7BC7\u767C\u51FA${placeZh(a.position) ? `\uFF08${placeZh(a.position)}\uFF09` : ""}\uFF0C\u5927\u5BB6\u90FD\u770B\u5F97\u5230\u3002`
|
|
8811
8901
|
},
|
|
8812
8902
|
series_create: {
|
|
8813
8903
|
en: (a) => `Plan and write a LinkedIn series about ${q(a.brief)}: ${a.count || 3} posts that read as a series. Each waits in Review for your OK. Uses one AI generation a post.`,
|
|
@@ -8962,6 +9052,18 @@ var int = (v, min, max, fallback) => {
|
|
|
8962
9052
|
const n = Math.trunc(Number(v));
|
|
8963
9053
|
return Number.isFinite(n) ? Math.min(max, Math.max(min, n)) : fallback;
|
|
8964
9054
|
};
|
|
9055
|
+
var positionParam = (what, fallback) => ({
|
|
9056
|
+
type: "string",
|
|
9057
|
+
description: `Where ${what} go: "front", "end", "random", or a number of items to go after ("0" = front; more than there are = end). ${fallback} Scheduled or automated runs should use "end" so they don't jump ahead of what is already waiting.`
|
|
9058
|
+
});
|
|
9059
|
+
var positionOf = (value) => {
|
|
9060
|
+
if (value == null || value === "") return void 0;
|
|
9061
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
9062
|
+
const text3 = String(value).trim().toLowerCase();
|
|
9063
|
+
if (text3 === "front" || text3 === "end" || text3 === "random") return text3;
|
|
9064
|
+
if (/^\d+$/.test(text3)) return Number(text3);
|
|
9065
|
+
throw new Error('position must be "front", "end", "random" or a number of items to go after (0 = front).');
|
|
9066
|
+
};
|
|
8965
9067
|
function tool(t) {
|
|
8966
9068
|
const out = { ...t };
|
|
8967
9069
|
if (t.risk !== "R0" && !t.confirm)
|
|
@@ -9656,19 +9758,20 @@ var blogTools = [
|
|
|
9656
9758
|
}),
|
|
9657
9759
|
tool({
|
|
9658
9760
|
name: "idea_plan",
|
|
9659
|
-
description: "Add a waiting idea to the
|
|
9761
|
+
description: "Add a waiting idea to the plan (the plan holds up to 100): at the end, or where `position` says (front, random, after N planned posts). The answer's position counts from 1.",
|
|
9660
9762
|
parameters: {
|
|
9661
9763
|
type: "object",
|
|
9662
9764
|
properties: {
|
|
9663
9765
|
id: { type: "string" },
|
|
9664
|
-
title: { type: "string", description: "As listed (shown on the approval card and in History)" }
|
|
9766
|
+
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
9767
|
+
position: positionParam("the idea", "Leave out for the end of the plan.")
|
|
9665
9768
|
},
|
|
9666
9769
|
required: ["id", "title"]
|
|
9667
9770
|
},
|
|
9668
9771
|
group: "blog",
|
|
9669
9772
|
need: write,
|
|
9670
9773
|
risk: "W1",
|
|
9671
|
-
run: async (ctx, { id }) => ws(ctx).planIdea(id)
|
|
9774
|
+
run: async (ctx, { id, position }) => ws(ctx).planIdea(id, positionOf(position))
|
|
9672
9775
|
}),
|
|
9673
9776
|
tool({
|
|
9674
9777
|
name: "idea_discard",
|
|
@@ -10839,6 +10942,32 @@ var opsTools = [
|
|
|
10839
10942
|
const { reviewer, comment, ...rest } = r;
|
|
10840
10943
|
return { ...rest, ...untrusted({ reviewer, comment }) };
|
|
10841
10944
|
}
|
|
10945
|
+
}),
|
|
10946
|
+
tool({
|
|
10947
|
+
name: "social_post_get",
|
|
10948
|
+
description: "One social post in full: its whole text (social_list shortens it to about 300 characters), its pictures, its link, its channel and status, when it goes out, and why it was held before going out, if it was. Use it to check a post's ending (a web address or phone number on Google, a cut-off sentence) before approving or fixing it.",
|
|
10949
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
10950
|
+
group: "social",
|
|
10951
|
+
need: { resource: "autopilot", level: "read" },
|
|
10952
|
+
risk: "R0",
|
|
10953
|
+
run: async (ctx, { id }) => {
|
|
10954
|
+
const p = await ws(ctx).socialPost(int(id, 1, Number.MAX_SAFE_INTEGER, 0));
|
|
10955
|
+
return {
|
|
10956
|
+
id: p.id,
|
|
10957
|
+
channel: p.provider,
|
|
10958
|
+
status: p.status,
|
|
10959
|
+
eta: p.eta ?? null,
|
|
10960
|
+
text: p.social_copy,
|
|
10961
|
+
characters: String(p.social_copy ?? "").length,
|
|
10962
|
+
picture: p.image_url ?? null,
|
|
10963
|
+
pictures: p.image_urls ?? [],
|
|
10964
|
+
// LinkedIn with a set (or an uploaded PDF): a PDF carousel or several pictures.
|
|
10965
|
+
carousel: p.carousel ?? null,
|
|
10966
|
+
link: p.cta_url ?? p.source_url ?? null,
|
|
10967
|
+
held: p.held ?? null,
|
|
10968
|
+
failure: p.failure_message ?? null
|
|
10969
|
+
};
|
|
10970
|
+
}
|
|
10842
10971
|
})
|
|
10843
10972
|
];
|
|
10844
10973
|
|
|
@@ -10861,7 +10990,7 @@ var publishingTools = [
|
|
|
10861
10990
|
}),
|
|
10862
10991
|
tool({
|
|
10863
10992
|
name: "posts_per_day_set",
|
|
10864
|
-
description: "
|
|
10993
|
+
description: "Blog posts a day for the website (1, 2, 12, 24, 48 or 96, up to the plan's limit). Sites AI CMO pushes to: how many it publishes a day. Plugin / API sites: the most the site collects a day (it still decides how often it checks).",
|
|
10865
10994
|
parameters: {
|
|
10866
10995
|
type: "object",
|
|
10867
10996
|
properties: {
|
|
@@ -11424,6 +11553,7 @@ var channelsParam = {
|
|
|
11424
11553
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
11425
11554
|
description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
|
|
11426
11555
|
};
|
|
11556
|
+
var queuedAt = positionParam("the posts", "Leave out for the front (the next posting times).");
|
|
11427
11557
|
var channelsOf = (value) => {
|
|
11428
11558
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
11429
11559
|
return list2.length ? list2 : void 0;
|
|
@@ -11516,24 +11646,28 @@ var socialTools = [
|
|
|
11516
11646
|
}),
|
|
11517
11647
|
tool({
|
|
11518
11648
|
name: "social_approve",
|
|
11519
|
-
description: "Approve
|
|
11649
|
+
description: "Approve post drafts: each goes to the front of the user's queue and out at the next posting times, or where `position` says (end, random, after N posts). Pass every id to approve in one call.",
|
|
11520
11650
|
parameters: {
|
|
11521
11651
|
type: "object",
|
|
11522
|
-
properties: {
|
|
11652
|
+
properties: {
|
|
11653
|
+
ids: { type: "array", items: { type: "integer" }, description: "The drafts to approve" },
|
|
11654
|
+
position: queuedAt
|
|
11655
|
+
},
|
|
11523
11656
|
required: ["ids"]
|
|
11524
11657
|
},
|
|
11525
11658
|
group: "social",
|
|
11526
11659
|
need: write5,
|
|
11527
11660
|
risk: "W3",
|
|
11528
11661
|
gates: ["VER", "LI"],
|
|
11529
|
-
run: async (ctx, { ids: ids3, id }) => {
|
|
11662
|
+
run: async (ctx, { ids: ids3, id, position }) => {
|
|
11530
11663
|
const list2 = (Array.isArray(ids3) && ids3.length ? ids3 : id != null ? [id] : []).map(Number).slice(0, 20);
|
|
11531
11664
|
if (!list2.length) throw new Error("Which post?");
|
|
11665
|
+
const at = positionOf(position);
|
|
11532
11666
|
const api2 = ws(ctx);
|
|
11533
11667
|
const results = [];
|
|
11534
11668
|
for (const n of list2) {
|
|
11535
11669
|
try {
|
|
11536
|
-
await api2.approveSocial(n);
|
|
11670
|
+
await api2.approveSocial(n, at);
|
|
11537
11671
|
results.push({ id: n, ok: true });
|
|
11538
11672
|
} catch (e) {
|
|
11539
11673
|
results.push({ id: n, ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
@@ -11786,6 +11920,90 @@ var socialTools = [
|
|
|
11786
11920
|
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
11787
11921
|
}
|
|
11788
11922
|
}),
|
|
11923
|
+
tool({
|
|
11924
|
+
name: "social_pictures_attach",
|
|
11925
|
+
description: "Put the user's own pictures on a post, in order: public https links to image files (JPEG, PNG or WebP), for example slides another assistant made. 1-20 pictures; append adds them after the post's current ones. Free (no AI). They replace Maggie's picture; a post from an idea shares them with its other channels. Instagram and Facebook post the first 10, Google Business the first. On LinkedIn a set goes out as a swipeable PDF carousel unless social_carousel_set says images. Preview first.",
|
|
11926
|
+
parameters: {
|
|
11927
|
+
type: "object",
|
|
11928
|
+
properties: {
|
|
11929
|
+
id: { type: "integer", description: "The post (from social_list), not gone out yet" },
|
|
11930
|
+
urls: {
|
|
11931
|
+
type: "array",
|
|
11932
|
+
items: { type: "string" },
|
|
11933
|
+
description: "Public https links to the image files, in the order they should appear"
|
|
11934
|
+
},
|
|
11935
|
+
append: { type: "boolean", description: "Add after the post's current pictures instead of replacing them" }
|
|
11936
|
+
},
|
|
11937
|
+
required: ["id", "urls"]
|
|
11938
|
+
},
|
|
11939
|
+
group: "social",
|
|
11940
|
+
need: write5,
|
|
11941
|
+
risk: "W1",
|
|
11942
|
+
gates: ["VER"],
|
|
11943
|
+
run: async (ctx, { id, urls, append }) => {
|
|
11944
|
+
const list2 = (Array.isArray(urls) ? urls : []).map((u) => String(u).trim()).filter(Boolean);
|
|
11945
|
+
if (!list2.length) throw new Error("Give at least one https link to a picture.");
|
|
11946
|
+
if (list2.length > 20) throw new Error("A post takes up to 20 pictures.");
|
|
11947
|
+
if (list2.some((u) => !u.startsWith("https://"))) throw new Error("Every picture needs a public https link.");
|
|
11948
|
+
const out = await ws(ctx).setSocialPictures(Number(id), { urls: list2, append: append === true });
|
|
11949
|
+
return {
|
|
11950
|
+
id: out.id,
|
|
11951
|
+
pictures: out.image_urls?.length ? out.image_urls : [out.image_url],
|
|
11952
|
+
carousel: out.carousel ?? null
|
|
11953
|
+
};
|
|
11954
|
+
}
|
|
11955
|
+
}),
|
|
11956
|
+
tool({
|
|
11957
|
+
name: "social_carousel_pdf_attach",
|
|
11958
|
+
description: "A LinkedIn post goes out as this PDF: a swipeable document carousel (a public https link to the PDF, up to 300 pages and 20 MB, no password). Its first 20 pages also become the post's pictures (what its other channels post). Free. Change the pictures later and the carousel is made from them instead. Preview first.",
|
|
11959
|
+
parameters: {
|
|
11960
|
+
type: "object",
|
|
11961
|
+
properties: {
|
|
11962
|
+
id: { type: "integer", description: "A LinkedIn post (from social_list), not gone out yet" },
|
|
11963
|
+
url: { type: "string", description: "Public https link to the PDF file" },
|
|
11964
|
+
title: {
|
|
11965
|
+
type: "string",
|
|
11966
|
+
description: "Shown on the carousel (up to 120 characters); the post's topic if left out"
|
|
11967
|
+
}
|
|
11968
|
+
},
|
|
11969
|
+
required: ["id", "url"]
|
|
11970
|
+
},
|
|
11971
|
+
group: "social",
|
|
11972
|
+
need: write5,
|
|
11973
|
+
risk: "W1",
|
|
11974
|
+
gates: ["VER", "LI"],
|
|
11975
|
+
run: async (ctx, { id, url, title }) => {
|
|
11976
|
+
const link = String(url ?? "").trim();
|
|
11977
|
+
if (!link.startsWith("https://")) throw new Error("Give a public https link to the PDF.");
|
|
11978
|
+
const out = await ws(ctx).attachSocialCarouselPdf(Number(id), { url: link, ...title ? { title } : {} });
|
|
11979
|
+
return { id: out.id, pages: out.pages, pictures: out.pictures, carousel: out.carousel ?? null };
|
|
11980
|
+
}
|
|
11981
|
+
}),
|
|
11982
|
+
tool({
|
|
11983
|
+
name: "social_carousel_set",
|
|
11984
|
+
description: "How a LinkedIn post with a set of pictures goes out: format pdf (a swipeable PDF carousel made from the pictures, the default) or images (several pictures); and the carousel's title. Preview first.",
|
|
11985
|
+
parameters: {
|
|
11986
|
+
type: "object",
|
|
11987
|
+
properties: {
|
|
11988
|
+
id: { type: "integer", description: "A LinkedIn post with a set of pictures (from social_list)" },
|
|
11989
|
+
format: { type: "string", enum: ["pdf", "images"] },
|
|
11990
|
+
title: { type: "string", description: "Shown on the carousel (up to 120 characters)" }
|
|
11991
|
+
},
|
|
11992
|
+
required: ["id"]
|
|
11993
|
+
},
|
|
11994
|
+
group: "social",
|
|
11995
|
+
need: write5,
|
|
11996
|
+
risk: "W1",
|
|
11997
|
+
gates: ["VER"],
|
|
11998
|
+
run: async (ctx, { id, format, title }) => {
|
|
11999
|
+
if (format == null && title == null) throw new Error("Say the format (pdf or images) or a title.");
|
|
12000
|
+
const out = await ws(ctx).setSocialCarousel(Number(id), {
|
|
12001
|
+
...format ? { format } : {},
|
|
12002
|
+
...title != null ? { title: String(title) } : {}
|
|
12003
|
+
});
|
|
12004
|
+
return { id: out.id, carousel: out.carousel ?? null };
|
|
12005
|
+
}
|
|
12006
|
+
}),
|
|
11789
12007
|
tool({
|
|
11790
12008
|
name: "social_post_insights",
|
|
11791
12009
|
description: "How a posted Instagram post is doing: views, reach, likes, comments, saves and shares, read from Instagram (kept 15 minutes; refresh: true reads again). Saves and shares per reach are the best signs of a good post. available: false says why (not posted yet, deleted on Instagram, the account needs reconnecting).",
|
|
@@ -11894,13 +12112,14 @@ var socialTools = [
|
|
|
11894
12112
|
}),
|
|
11895
12113
|
tool({
|
|
11896
12114
|
name: "social_from_article",
|
|
11897
|
-
description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it
|
|
12115
|
+
description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it is queued: at the front, or where `position` says). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
11898
12116
|
parameters: {
|
|
11899
12117
|
type: "object",
|
|
11900
12118
|
properties: {
|
|
11901
12119
|
source_id: { type: "string" },
|
|
11902
12120
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
11903
|
-
channels: channelsParam
|
|
12121
|
+
channels: channelsParam,
|
|
12122
|
+
position: queuedAt
|
|
11904
12123
|
},
|
|
11905
12124
|
required: ["source_id", "title"]
|
|
11906
12125
|
},
|
|
@@ -11908,17 +12127,18 @@ var socialTools = [
|
|
|
11908
12127
|
need: write5,
|
|
11909
12128
|
risk: "W2",
|
|
11910
12129
|
gates: ["VER", "LI", "LLM"],
|
|
11911
|
-
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
12130
|
+
run: async (ctx, { source_id, channels, position }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels), positionOf(position))).job)
|
|
11912
12131
|
}),
|
|
11913
12132
|
tool({
|
|
11914
12133
|
name: "social_plan_days",
|
|
11915
|
-
description: "Maggie writes posts for the next 1\u20137 days from the best recent articles, on each channel. They wait for the user's OK as drafts, unless the user's approval mode is auto (then they are queued in order). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
12134
|
+
description: "Maggie writes posts for the next 1\u20137 days from the best recent articles, on each channel. They wait for the user's OK as drafts, unless the user's approval mode is auto (then they are queued in order: at the front, or where `position` says). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
11916
12135
|
parameters: {
|
|
11917
12136
|
type: "object",
|
|
11918
12137
|
properties: {
|
|
11919
12138
|
days: { type: "integer", minimum: 1, maximum: 7 },
|
|
11920
12139
|
timezone: { type: "string", description: "IANA timezone, if known" },
|
|
11921
|
-
channels: channelsParam
|
|
12140
|
+
channels: channelsParam,
|
|
12141
|
+
position: queuedAt
|
|
11922
12142
|
},
|
|
11923
12143
|
required: ["days"]
|
|
11924
12144
|
},
|
|
@@ -11926,7 +12146,14 @@ var socialTools = [
|
|
|
11926
12146
|
need: write5,
|
|
11927
12147
|
risk: "W2",
|
|
11928
12148
|
gates: ["VER", "LI", "LLM"],
|
|
11929
|
-
run: async (ctx, { days, timezone, channels }) => writing(
|
|
12149
|
+
run: async (ctx, { days, timezone, channels, position }) => writing(
|
|
12150
|
+
(await ws(ctx).startSocialPlan(
|
|
12151
|
+
int(days, 1, 7, 3),
|
|
12152
|
+
timezone || "UTC",
|
|
12153
|
+
channelsOf(channels),
|
|
12154
|
+
positionOf(position)
|
|
12155
|
+
)).job
|
|
12156
|
+
)
|
|
11930
12157
|
}),
|
|
11931
12158
|
tool({
|
|
11932
12159
|
name: "social_ideas_list",
|
|
@@ -11999,13 +12226,14 @@ var socialTools = [
|
|
|
11999
12226
|
}),
|
|
12000
12227
|
tool({
|
|
12001
12228
|
name: "social_idea_write",
|
|
12002
|
-
description: "Maggie writes one idea's post for each channel (1 credit per post). The user's own idea's posts wait for their OK (queued straight away only in approval mode auto); a campaign's wait for the campaign to start. Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
12229
|
+
description: "Maggie writes one idea's post for each channel (1 credit per post). The user's own idea's posts wait for their OK (queued straight away only in approval mode auto, at the front or where `position` says); a campaign's wait for the campaign to start. Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
12003
12230
|
parameters: {
|
|
12004
12231
|
type: "object",
|
|
12005
12232
|
properties: {
|
|
12006
12233
|
id: { type: "integer" },
|
|
12007
12234
|
title: { type: "string", description: "As listed (shown on the approval card and in History)" },
|
|
12008
|
-
channels: channelsParam
|
|
12235
|
+
channels: channelsParam,
|
|
12236
|
+
position: queuedAt
|
|
12009
12237
|
},
|
|
12010
12238
|
required: ["id", "title"]
|
|
12011
12239
|
},
|
|
@@ -12013,7 +12241,7 @@ var socialTools = [
|
|
|
12013
12241
|
need: write5,
|
|
12014
12242
|
risk: "W2",
|
|
12015
12243
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
12016
|
-
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
12244
|
+
run: async (ctx, { id, channels, position }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels), positionOf(position))).job)
|
|
12017
12245
|
}),
|
|
12018
12246
|
tool({
|
|
12019
12247
|
name: "social_idea_brief",
|
|
@@ -12139,13 +12367,16 @@ var socialTools = [
|
|
|
12139
12367
|
}),
|
|
12140
12368
|
tool({
|
|
12141
12369
|
name: "social_approve_all",
|
|
12142
|
-
description: "Approve every post waiting for the user's OK (not a series that hasn't been approved), on one channel or all: they go out one by one at each channel's next posting times.",
|
|
12143
|
-
parameters: {
|
|
12370
|
+
description: "Approve every post waiting for the user's OK (not a series that hasn't been approved), on one channel or all: they go out one by one at each channel's next posting times (from the front of the queue, or where `position` says).",
|
|
12371
|
+
parameters: {
|
|
12372
|
+
type: "object",
|
|
12373
|
+
properties: { channel: { type: "string", enum: CHANNEL_KEYS }, position: queuedAt }
|
|
12374
|
+
},
|
|
12144
12375
|
group: "social",
|
|
12145
12376
|
need: write5,
|
|
12146
12377
|
risk: "W3",
|
|
12147
12378
|
gates: ["VER", "LI"],
|
|
12148
|
-
run: async (ctx, { channel }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0])
|
|
12379
|
+
run: async (ctx, { channel, position }) => ws(ctx).approveAllSocial(channelsOf(channel ? [channel] : [])?.[0], positionOf(position))
|
|
12149
12380
|
}),
|
|
12150
12381
|
tool({
|
|
12151
12382
|
name: "series_create",
|
|
@@ -16268,7 +16499,7 @@ var program = new Command();
|
|
|
16268
16499
|
// package.json
|
|
16269
16500
|
var package_default = {
|
|
16270
16501
|
name: "@cmo-so/cli",
|
|
16271
|
-
version: "0.
|
|
16502
|
+
version: "0.9.0",
|
|
16272
16503
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
16273
16504
|
type: "module",
|
|
16274
16505
|
license: "MIT",
|
|
@@ -27244,7 +27475,32 @@ ${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
|
|
|
27244
27475
|
})
|
|
27245
27476
|
)
|
|
27246
27477
|
);
|
|
27247
|
-
social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").
|
|
27478
|
+
social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").option("--position <where>", "front (default), end, random, or a number of posts to go after").action(
|
|
27479
|
+
act(
|
|
27480
|
+
async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2), ...opts.position ? { position: opts.position } : {} })
|
|
27481
|
+
)
|
|
27482
|
+
);
|
|
27483
|
+
social.command("pictures <id> <urls...>").description("your own pictures on a post, in order (public https links, up to 20); a PDF carousel on LinkedIn").option("--append", "add after the post's current pictures").action(
|
|
27484
|
+
act(
|
|
27485
|
+
async (opts, id, urls) => tool2(opts, "social_pictures_attach", { id: ids2([id])[0], urls, append: opts.append === true })
|
|
27486
|
+
)
|
|
27487
|
+
);
|
|
27488
|
+
social.command("carousel <id>").description("a LinkedIn post with a set of pictures: a swipeable PDF carousel or several pictures").option("--format <format>", "pdf (a swipeable carousel, the default) or images").option("--title <text>", "shown on the carousel").option("--pdf <url>", "a public https link to your own PDF: it goes out as it is").action(
|
|
27489
|
+
act(async (opts, id) => {
|
|
27490
|
+
const post = ids2([id])[0];
|
|
27491
|
+
if (opts.pdf)
|
|
27492
|
+
return tool2(opts, "social_carousel_pdf_attach", {
|
|
27493
|
+
id: post,
|
|
27494
|
+
url: opts.pdf,
|
|
27495
|
+
...opts.title ? { title: opts.title } : {}
|
|
27496
|
+
});
|
|
27497
|
+
return tool2(opts, "social_carousel_set", {
|
|
27498
|
+
id: post,
|
|
27499
|
+
...opts.format ? { format: opts.format } : {},
|
|
27500
|
+
...opts.title ? { title: opts.title } : {}
|
|
27501
|
+
});
|
|
27502
|
+
})
|
|
27503
|
+
);
|
|
27248
27504
|
social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
|
|
27249
27505
|
social.command("restore <id>").description("bring a skipped draft back").action(act(async (opts, id) => tool2(opts, "social_restore", { id: ids2([id])[0] })));
|
|
27250
27506
|
social.command("draft <topic...>").description("add a post idea for Maggie to write (every connected channel unless --channel)").option("--summary <text>", "what the post should say").option("--channel <names>", "linkedin, facebook, instagram, google_business (comma-separated)").action(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|