@cmo-so/mcp 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cmo-mcp.mjs +679 -39
- 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.4.0",
|
|
7203
7203
|
description: "CMO.so on the command line: your websites, ideas, blog posts, LinkedIn queue and results, plus a local MCP server for Claude Desktop, Cursor and other MCP clients.",
|
|
7204
7204
|
type: "module",
|
|
7205
7205
|
license: "MIT",
|
|
@@ -7530,7 +7530,15 @@ function dataUrl(type, bytes) {
|
|
|
7530
7530
|
}
|
|
7531
7531
|
|
|
7532
7532
|
// ../../packages/api-client/src/domains.ts
|
|
7533
|
-
var MEMORY_CHANNELS = [
|
|
7533
|
+
var MEMORY_CHANNELS = [
|
|
7534
|
+
"general",
|
|
7535
|
+
"blog",
|
|
7536
|
+
"linkedin",
|
|
7537
|
+
"facebook",
|
|
7538
|
+
"instagram",
|
|
7539
|
+
"google_business",
|
|
7540
|
+
"youtube"
|
|
7541
|
+
];
|
|
7534
7542
|
function addWebsite(api2, url) {
|
|
7535
7543
|
return api2.request("/v1/domains", {
|
|
7536
7544
|
method: "POST",
|
|
@@ -7649,8 +7657,15 @@ function domainApi(api2, domain) {
|
|
|
7649
7657
|
approveSocial: (id) => post(`${base}/social/${id}/approve`),
|
|
7650
7658
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7651
7659
|
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7652
|
-
|
|
7660
|
+
/** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
|
|
7661
|
+
* scenes (a notice when it's out); 409 `picture_busy` while its picture is being made. */
|
|
7662
|
+
publishSocialNow: (id, opts = {}) => post(
|
|
7663
|
+
`${base}/social/${id}/publish`,
|
|
7664
|
+
opts.background ? { background: true } : void 0
|
|
7665
|
+
),
|
|
7653
7666
|
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7667
|
+
/** How a posted Instagram post is doing (read from Instagram, kept 15 minutes; `refresh` reads again). */
|
|
7668
|
+
socialInsights: (id, refresh = false) => api2.request(`${base}/social/${id}/insights${refresh ? "?refresh=true" : ""}`),
|
|
7654
7669
|
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7655
7670
|
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7656
7671
|
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
@@ -7686,10 +7701,10 @@ function domainApi(api2, domain) {
|
|
|
7686
7701
|
/** The template library, searchable. */
|
|
7687
7702
|
socialTemplates: (search = "") => api2.request(`${base}/social/templates?search=${encodeURIComponent(search)}`),
|
|
7688
7703
|
socialTemplatePreview: (key) => api2.request(`${base}/social/templates/${encodeURIComponent(key)}/preview`),
|
|
7689
|
-
socialSetupTemplates: (keys2,
|
|
7704
|
+
socialSetupTemplates: (keys2, writing2) => api2.request(`${base}/social/setup/templates`, {
|
|
7690
7705
|
method: "PUT",
|
|
7691
7706
|
headers: { "Content-Type": "application/json" },
|
|
7692
|
-
body: JSON.stringify({ keys: keys2, writing })
|
|
7707
|
+
body: JSON.stringify({ keys: keys2, writing: writing2 })
|
|
7693
7708
|
}),
|
|
7694
7709
|
socialSetupSchedule: (schedule) => api2.request(`${base}/social/setup/schedule`, {
|
|
7695
7710
|
method: "PUT",
|
|
@@ -7731,6 +7746,49 @@ function domainApi(api2, domain) {
|
|
|
7731
7746
|
}
|
|
7732
7747
|
),
|
|
7733
7748
|
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7749
|
+
/** Instagram connects through the Facebook sign-in (the accounts linked to your Pages): the same link;
|
|
7750
|
+
* back with ?facebook=connected, then `channels` says whether an Instagram account was found. */
|
|
7751
|
+
connectInstagram: (returnTo = "close") => post(`${base}/social/instagram/connect`, { return_to: returnTo }),
|
|
7752
|
+
/** Your Instagram accounts here (each with the Facebook Page it posts through), and the chosen one. */
|
|
7753
|
+
instagramAccounts: () => api2.request(`${base}/social/instagram/accounts`),
|
|
7754
|
+
chooseInstagramAccount: (connectionId) => api2.request(
|
|
7755
|
+
`${base}/social/instagram/account`,
|
|
7756
|
+
{
|
|
7757
|
+
method: "PUT",
|
|
7758
|
+
headers: { "Content-Type": "application/json" },
|
|
7759
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7760
|
+
}
|
|
7761
|
+
),
|
|
7762
|
+
disconnectInstagram: (connectionId) => api2.request(`${base}/social/instagram/${connectionId}`, { method: "DELETE" }),
|
|
7763
|
+
/** A Google sign-in link for this website's Google Business Profile; back with ?google_business=connected
|
|
7764
|
+
* (or none / error&message=…). Every profile the account can post to is saved; this website's is picked. */
|
|
7765
|
+
connectGoogleBusiness: (returnTo = "close") => post(`${base}/social/google_business/connect`, {
|
|
7766
|
+
return_to: returnTo
|
|
7767
|
+
}),
|
|
7768
|
+
/** Your Google Business profiles here (name, address, website), and the one this website posts to. */
|
|
7769
|
+
googleBusinessLocations: () => api2.request(`${base}/social/google_business/locations`),
|
|
7770
|
+
chooseGoogleBusinessLocation: (connectionId) => put(
|
|
7771
|
+
`${base}/social/google_business/location`,
|
|
7772
|
+
{ connection_id: connectionId }
|
|
7773
|
+
),
|
|
7774
|
+
/** How the Google Business Profile is doing: the last 28 days and the 28 before (Google lags 2-3 days). */
|
|
7775
|
+
googleBusinessPerformance: (refresh = false) => api2.request(`${base}/social/google_business/performance${refresh ? "?refresh=true" : ""}`),
|
|
7776
|
+
disconnectGoogleBusiness: (connectionId) => api2.request(`${base}/social/google_business/${connectionId}`, { method: "DELETE" }),
|
|
7777
|
+
/** A Google sign-in link for this website's YouTube channel (docs/prd/youtube.md §7); back with ?youtube=connected
|
|
7778
|
+
* (or none when the Google account has no channel, error&message=…). */
|
|
7779
|
+
connectYouTube: (returnTo = "close") => post(`${base}/social/youtube/connect`, { return_to: returnTo }),
|
|
7780
|
+
/** Your YouTube channels here (name, @handle, picture, subscribers), and the one this website posts to. */
|
|
7781
|
+
youtubeChannels: () => api2.request(
|
|
7782
|
+
`${base}/social/youtube/channels`
|
|
7783
|
+
),
|
|
7784
|
+
chooseYouTubeChannel: (connectionId) => put(
|
|
7785
|
+
`${base}/social/youtube/channel`,
|
|
7786
|
+
{
|
|
7787
|
+
connection_id: connectionId
|
|
7788
|
+
}
|
|
7789
|
+
),
|
|
7790
|
+
/** Disconnect: the token is deleted and Google's access revoked (YouTube's policy). */
|
|
7791
|
+
disconnectYouTube: (connectionId) => api2.request(`${base}/social/youtube/${connectionId}`, { method: "DELETE" }),
|
|
7734
7792
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7735
7793
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7736
7794
|
/** How often one channel posts. */
|
|
@@ -7740,14 +7798,56 @@ function domainApi(api2, domain) {
|
|
|
7740
7798
|
),
|
|
7741
7799
|
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7742
7800
|
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7801
|
+
// `mode: "edit"` changes the post's current picture as `instruction` says. Another picture
|
|
7802
|
+
// being made here: this one waits its turn (status queued).
|
|
7803
|
+
// `count: 4` makes a set of 4 pictures from one storyboard (one AI generation), with words on each in
|
|
7804
|
+
// `textEffect` (an id from socialTextEffects; omit = Maggie picks).
|
|
7743
7805
|
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7806
|
+
...opts.style ? { style: opts.style } : {},
|
|
7744
7807
|
...opts.replace ? { replace: true } : {},
|
|
7808
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7809
|
+
...opts.mode === "edit" ? { mode: "edit" } : {},
|
|
7810
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7811
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7812
|
+
}),
|
|
7813
|
+
/** Change a picture Maggie made without a new one (free): the words on each picture, its style, its text
|
|
7814
|
+
* style, the order of a set, or drop pictures from it. `words` follow the new order when `order` is given. */
|
|
7815
|
+
editSocialPicture: (id, changes) => api2.request(`${base}/social/${id}/picture`, {
|
|
7816
|
+
method: "PATCH",
|
|
7817
|
+
headers: { "Content-Type": "application/json" },
|
|
7818
|
+
body: JSON.stringify(changes)
|
|
7819
|
+
}),
|
|
7820
|
+
/** A title and a line on your own photo(s) in your look (Maggie plans the words; no AI picture, free). */
|
|
7821
|
+
wordsOnPhoto: (id, style = "real_photo") => post(`${base}/social/${id}/picture/words`, { style }),
|
|
7822
|
+
/** The brand's picture look (made the first time it's asked for). */
|
|
7823
|
+
socialLook: () => api2.request(`${base}/social/look`),
|
|
7824
|
+
updateSocialLook: (changes) => put(`${base}/social/look`, changes),
|
|
7825
|
+
/** Maggie looks again and suggests a new look (one AI generation). */
|
|
7826
|
+
suggestSocialLook: () => post(`${base}/social/look/suggest`, {}),
|
|
7827
|
+
/** A sample picture of a style in the brand's look: a data: URL. */
|
|
7828
|
+
socialLookSample: (style) => api2.request(`${base}/social/look/sample?style=${encodeURIComponent(style)}`),
|
|
7829
|
+
/** The text effects for the words on a set of pictures, each with a sample picture. */
|
|
7830
|
+
socialTextEffects: () => api2.request(`${base}/social/text-effects`),
|
|
7831
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out.
|
|
7832
|
+
* With `instruction`, Maggie changes the photo as it says, in the background (one AI generation). */
|
|
7833
|
+
uploadSocialPhoto: (id, photo, opts = {}) => post(`${base}/social/${id}/image`, {
|
|
7834
|
+
photo,
|
|
7745
7835
|
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7746
7836
|
}),
|
|
7747
|
-
/**
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7837
|
+
/** A new picture for every post here without one (or these), one after another in the background. */
|
|
7838
|
+
fillSocialImages: (opts = {}) => post(
|
|
7839
|
+
`${base}/social/images/fill`,
|
|
7840
|
+
{
|
|
7841
|
+
...opts.item_ids ? { item_ids: opts.item_ids } : {},
|
|
7842
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7843
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7844
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7845
|
+
}
|
|
7846
|
+
),
|
|
7847
|
+
/** Stop the picture being made (it's thrown away; the next in line starts). `all` empties the line too. */
|
|
7848
|
+
stopSocialImage: (all = false) => api2.request(`${base}/social/image-job${all ? "?all=true" : ""}`, {
|
|
7849
|
+
method: "DELETE"
|
|
7850
|
+
}),
|
|
7751
7851
|
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7752
7852
|
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7753
7853
|
/** Maggie plans and writes a series; it waits for one OK. */
|
|
@@ -7760,13 +7860,35 @@ function domainApi(api2, domain) {
|
|
|
7760
7860
|
brief: brief2,
|
|
7761
7861
|
...channels ? { channels } : {}
|
|
7762
7862
|
}),
|
|
7863
|
+
// In the background: the answer is the job at once; watch `write_jobs` in GET /social.
|
|
7864
|
+
startSocialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7865
|
+
photos,
|
|
7866
|
+
brief: brief2,
|
|
7867
|
+
background: true,
|
|
7868
|
+
...channels ? { channels } : {}
|
|
7869
|
+
}),
|
|
7870
|
+
startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7871
|
+
source_post_id: sourcePostId,
|
|
7872
|
+
background: true,
|
|
7873
|
+
...channels ? { channels } : {}
|
|
7874
|
+
}),
|
|
7875
|
+
startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7876
|
+
days,
|
|
7877
|
+
timezone,
|
|
7878
|
+
background: true,
|
|
7879
|
+
...channels ? { channels } : {}
|
|
7880
|
+
}),
|
|
7881
|
+
startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
|
|
7882
|
+
background: true,
|
|
7883
|
+
...channels ? { channels } : {}
|
|
7884
|
+
}),
|
|
7763
7885
|
/** Disconnect the website's LinkedIn account. */
|
|
7764
7886
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7765
7887
|
/** Published articles a LinkedIn post can be made from. */
|
|
7766
7888
|
socialSources: (search = "") => api2.request(
|
|
7767
7889
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7768
7890
|
),
|
|
7769
|
-
/** Maggie writes a
|
|
7891
|
+
/** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
|
|
7770
7892
|
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7771
7893
|
source_post_id: sourcePostId,
|
|
7772
7894
|
...channels ? { channels } : {}
|
|
@@ -7825,13 +7947,49 @@ function domainApi(api2, domain) {
|
|
|
7825
7947
|
headers: { "Content-Type": "application/json" },
|
|
7826
7948
|
body: JSON.stringify({ publish_mode: mode })
|
|
7827
7949
|
}),
|
|
7950
|
+
/** A Google Business post's button (LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL); "" = Learn more. */
|
|
7951
|
+
setSocialButton: (id, ctaType) => api2.request(`${base}/social/${id}`, {
|
|
7952
|
+
method: "PATCH",
|
|
7953
|
+
headers: { "Content-Type": "application/json" },
|
|
7954
|
+
body: JSON.stringify({ cta_type: ctaType })
|
|
7955
|
+
}),
|
|
7956
|
+
/** Make a Google Business post an Event or Offer (or back to an update with { type: "STANDARD" }). */
|
|
7957
|
+
setSocialGooglePost: (id, googlePost) => api2.request(`${base}/social/${id}`, {
|
|
7958
|
+
method: "PATCH",
|
|
7959
|
+
headers: { "Content-Type": "application/json" },
|
|
7960
|
+
body: JSON.stringify({ google_post: googlePost })
|
|
7961
|
+
}),
|
|
7828
7962
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7963
|
+
/** Start a video upload: PUT each part_size slice of the file to its url, keep the ETags, then completeVideoUpload. */
|
|
7964
|
+
startVideoUpload: (file) => post(
|
|
7965
|
+
`${base}/social/video/upload`,
|
|
7966
|
+
file
|
|
7967
|
+
),
|
|
7968
|
+
completeVideoUpload: (videoId, parts, edit) => post(`${base}/social/video/${videoId}/complete`, { parts, edit }),
|
|
7969
|
+
/** A direct link to a video file (not a YouTube / TikTok page). */
|
|
7970
|
+
videoFromLink: (url, edit) => post(`${base}/social/video/link`, { url, edit }),
|
|
7971
|
+
video: (videoId) => api2.request(`${base}/social/video/${videoId}`),
|
|
7972
|
+
/** Another part or framing; prepared again in the background. */
|
|
7973
|
+
editVideo: (videoId, edit) => post(`${base}/social/video/${videoId}/edit`, { edit }),
|
|
7974
|
+
/** A YouTube post from a ready video, waiting in Review; Maggie writes the details unless write is false. */
|
|
7975
|
+
postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
|
|
7976
|
+
youtubeSettings: () => api2.request(`${base}/social/youtube/settings`),
|
|
7977
|
+
setYouTubeSettings: (s) => put(`${base}/social/youtube/settings`, s),
|
|
7978
|
+
/** A YouTube post's title, tags, visibility, made-for-kids or altered-content answer. */
|
|
7979
|
+
setSocialYouTube: (id, answers) => api2.request(`${base}/social/${id}`, {
|
|
7980
|
+
method: "PATCH",
|
|
7981
|
+
headers: { "Content-Type": "application/json" },
|
|
7982
|
+
body: JSON.stringify({ youtube: answers })
|
|
7983
|
+
}),
|
|
7829
7984
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7830
7985
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
7831
7986
|
// Reports (docs/18 §6): report:read with the user's own key.
|
|
7832
7987
|
visibility: () => api2.request(`${base}/visibility`),
|
|
7833
7988
|
searchConsole: (period2 = "30d") => api2.request(`${base}/search-console?period=${period2}`),
|
|
7834
7989
|
searchConsoleQueries: (period2 = "30d", filter = "top", limit = 20) => api2.request(`${base}/search-console/queries?period=${period2}&filter=${filter}&limit=${limit}`),
|
|
7990
|
+
/** A Google sign-in link that connects Search Console for this website (open it in a
|
|
7991
|
+
* browser; back at returnTo with ?gsc=connected or ?gsc=error&message=…). 10 minutes. */
|
|
7992
|
+
connectSearchConsole: (returnTo = "close") => post(`${base}/search-console/connect`, { return_to: returnTo }),
|
|
7835
7993
|
/** Fresh numbers from Google now; at most once an hour per website. */
|
|
7836
7994
|
syncSearchConsole: () => post(`${base}/search-console/sync`),
|
|
7837
7995
|
contentPerformance: (sort = "clicks", band, limit = 20) => api2.request(
|
|
@@ -8243,6 +8401,22 @@ var confirmCopy = {
|
|
|
8243
8401
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
8244
8402
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
8245
8403
|
},
|
|
8404
|
+
youtube_connect_link: {
|
|
8405
|
+
en: () => "Make a link to connect your YouTube channel.",
|
|
8406
|
+
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8407
|
+
},
|
|
8408
|
+
social_video_add: {
|
|
8409
|
+
en: (a) => `Make a YouTube post from ${a.url}${a.write === false ? "" : " (Maggie writes the title and description: one AI generation)"}; it waits for your OK.`,
|
|
8410
|
+
"zh-Hant": (a) => `\u7528 ${a.url} \u505A\u4E00\u5247 YouTube \u5F71\u7247\u8CBC\u6587${a.write === false ? "" : "\uFF08Maggie \u5BEB\u6A19\u984C\u548C\u8AAA\u660E\uFF1A\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09"}\uFF0C\u7B49\u4F60\u78BA\u8A8D\u5F8C\u624D\u767C\u3002`
|
|
8411
|
+
},
|
|
8412
|
+
social_youtube_set: {
|
|
8413
|
+
en: (a) => `Change YouTube post #${a.id}: ${keys(a).filter((k) => k !== "id").join(", ") || "its details"}.`,
|
|
8414
|
+
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u8CBC\u6587 #${a.id}\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001") || "\u7D30\u7BC0"}\u3002`
|
|
8415
|
+
},
|
|
8416
|
+
youtube_settings_set: {
|
|
8417
|
+
en: (a) => `Change your YouTube defaults: ${keys(a).join(", ") || "nothing"}.`,
|
|
8418
|
+
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8419
|
+
},
|
|
8246
8420
|
social_image_new: {
|
|
8247
8421
|
en: (a) => `Make a new picture for LinkedIn post #${a.id}${a.instruction ? `: ${String(a.instruction).slice(0, 300)}` : ""} (1 AI generation, in the background)${a.replace ? ", stopping the one being made for another post" : ""}.`,
|
|
8248
8422
|
"zh-Hant": (a) => `\u70BA LinkedIn \u8CBC\u6587 #${a.id} \u751F\u6210\u65B0\u5716\u7247${a.instruction ? `\uFF1A${String(a.instruction).slice(0, 300)}` : ""}\uFF08\u7528 1 \u6B21 AI \u751F\u6210\uFF0C\u5728\u80CC\u666F\u9032\u884C\uFF09${a.replace ? "\uFF0C\u4E26\u505C\u6B62\u6B63\u5728\u70BA\u53E6\u4E00\u7BC7\u751F\u6210\u7684\u5716\u7247" : ""}\u3002`
|
|
@@ -9776,7 +9950,7 @@ var brief = (m) => ({
|
|
|
9776
9950
|
var memoryTools = [
|
|
9777
9951
|
tool({
|
|
9778
9952
|
name: "memory_get",
|
|
9779
|
-
description: "The user's Maggie Memory on this website: their saved taste per channel (general, blog, linkedin, facebook) as prefer / always / never items, each with an id. Already in your instructions when it is on; read it to answer what you remember or to find an id to forget.",
|
|
9953
|
+
description: "The user's Maggie Memory on this website: their saved taste per channel (general, blog, linkedin, facebook, instagram, google_business) as prefer / always / never items, each with an id. Already in your instructions when it is on; read it to answer what you remember or to find an id to forget.",
|
|
9780
9954
|
parameters: none,
|
|
9781
9955
|
group: "memory",
|
|
9782
9956
|
need: own,
|
|
@@ -10074,9 +10248,21 @@ var reportTools = [
|
|
|
10074
10248
|
};
|
|
10075
10249
|
}
|
|
10076
10250
|
}),
|
|
10251
|
+
tool({
|
|
10252
|
+
name: "gsc_connect_link",
|
|
10253
|
+
description: "A Google sign-in link for the user to connect Google Search Console for this website (or reconnect). Give them the link to open; never ask for their Google password. They must sign in with a Google account that can see this website in Search Console; if it can't, the page says which account and how to add it.",
|
|
10254
|
+
parameters: none,
|
|
10255
|
+
group: "reports",
|
|
10256
|
+
need: { resource: "autopilot", level: "write" },
|
|
10257
|
+
risk: "W1",
|
|
10258
|
+
run: async (ctx) => {
|
|
10259
|
+
const out = await ws(ctx).connectSearchConsole("close");
|
|
10260
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10261
|
+
}
|
|
10262
|
+
}),
|
|
10077
10263
|
tool({
|
|
10078
10264
|
name: "gsc_performance_get",
|
|
10079
|
-
description: "Google Search Console for the period (30d, 90d or 12m): clicks, impressions, CTR and average position, with the change against the period before, the daily trend and the top pages.",
|
|
10265
|
+
description: "Google Search Console for the period (30d, 90d or 12m): clicks, impressions, CTR and average position, with the change against the period before (clicks and impressions as a ratio: -0.25 = 25% fewer, null when there was nothing before; ctr in points as a fraction; position in places, lower is better), the daily trend and the top pages.",
|
|
10080
10266
|
parameters: { type: "object", properties: { period: { type: "string", enum: periods } } },
|
|
10081
10267
|
group: "reports",
|
|
10082
10268
|
need: read4,
|
|
@@ -10243,16 +10429,24 @@ var reportTools = [
|
|
|
10243
10429
|
var read5 = { resource: "autopilot", level: "read" };
|
|
10244
10430
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10245
10431
|
var moves2 = ["up", "down", "top"];
|
|
10246
|
-
var CHANNEL_KEYS = ["linkedin", "facebook"];
|
|
10432
|
+
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10433
|
+
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube"];
|
|
10247
10434
|
var channelsParam = {
|
|
10248
10435
|
type: "array",
|
|
10249
10436
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10250
|
-
description: "Which channels to write for: linkedin, facebook. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it."
|
|
10437
|
+
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 takes a video: use social_video_add."
|
|
10251
10438
|
};
|
|
10252
10439
|
var channelsOf = (value) => {
|
|
10253
10440
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10254
10441
|
return list2.length ? list2 : void 0;
|
|
10255
10442
|
};
|
|
10443
|
+
var writing = (job) => ({
|
|
10444
|
+
job_id: job.id,
|
|
10445
|
+
status: "writing",
|
|
10446
|
+
posts_expected: job.expected,
|
|
10447
|
+
channels: job.channels,
|
|
10448
|
+
note: "Maggie is writing in the background (a minute or two). Tell the user they'll be told when it's ready; social_write_status says when it's done."
|
|
10449
|
+
});
|
|
10256
10450
|
var item = (i) => ({
|
|
10257
10451
|
id: i.id,
|
|
10258
10452
|
channel: i.provider || "linkedin",
|
|
@@ -10263,7 +10457,11 @@ var item = (i) => ({
|
|
|
10263
10457
|
campaign: i.campaign ?? null,
|
|
10264
10458
|
failure: i.failure_message ?? null,
|
|
10265
10459
|
link: i.provider_permalink ?? null,
|
|
10266
|
-
text: clip(i.social_copy, 300)
|
|
10460
|
+
text: clip(i.social_copy, 300),
|
|
10461
|
+
...i.provider === "youtube" ? {
|
|
10462
|
+
youtube: i.youtube ?? null,
|
|
10463
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10464
|
+
} : {}
|
|
10267
10465
|
});
|
|
10268
10466
|
var socialTools = [
|
|
10269
10467
|
tool({
|
|
@@ -10371,7 +10569,7 @@ var socialTools = [
|
|
|
10371
10569
|
}),
|
|
10372
10570
|
tool({
|
|
10373
10571
|
name: "social_image_new",
|
|
10374
|
-
description: "Make a new picture for a
|
|
10572
|
+
description: "Make a new picture for a social post that hasn't gone out, or change its current one (mode: edit, as `instruction` says). Uses 1 AI generation. It is made in the background, on the user's own ChatGPT when their Maggie Bridge has images on, else by CMO.so; it takes up to a couple of minutes and appears on the post by itself (check with social_image_status). One picture is made at a time per website: while another is being made this one waits in line (status queued). It fails with image_busy only when this post already has one being made or in line; pass replace: true only when the user agrees to stop the one being made and make this one now. A post written from an article gets its own picture (from its text and the article's title) and then goes out as that picture. Every new picture carries a short title and line set on it; count: 4 makes a set of 4 pictures that tell one story (still 1 AI generation, drawn as one storyboard and cut); LinkedIn and Facebook post a set together as a collage. text_effect picks the style of those words (an id from social_text_effects); leave it out to let Maggie pick. style picks the picture style (an id from social_look: photo_headline, editorial, duotone, illustrated, bold_type, infographic); leave it out and Maggie takes turns with the brand's 3. bold_type and infographic make no AI picture (big words or a list in the brand's look) and cost nothing.",
|
|
10375
10573
|
parameters: {
|
|
10376
10574
|
type: "object",
|
|
10377
10575
|
properties: {
|
|
@@ -10380,6 +10578,23 @@ var socialTools = [
|
|
|
10380
10578
|
instruction: {
|
|
10381
10579
|
type: "string",
|
|
10382
10580
|
description: "What the user wants in the picture, in their words (up to 300 characters), e.g. 'gold border, our logo, Future 100 \xB7 2025'. Leave out when they gave no direction."
|
|
10581
|
+
},
|
|
10582
|
+
mode: {
|
|
10583
|
+
type: "string",
|
|
10584
|
+
enum: ["new", "edit"],
|
|
10585
|
+
description: "edit: change the post's current picture as the instruction says (it must have one)"
|
|
10586
|
+
},
|
|
10587
|
+
count: {
|
|
10588
|
+
type: "integer",
|
|
10589
|
+
description: "1 (default) or 4: a set of 4 pictures telling one story, a line of words on each"
|
|
10590
|
+
},
|
|
10591
|
+
text_effect: {
|
|
10592
|
+
type: "string",
|
|
10593
|
+
description: "For a set: the words' style id from social_text_effects (omit = Maggie picks)"
|
|
10594
|
+
},
|
|
10595
|
+
style: {
|
|
10596
|
+
type: "string",
|
|
10597
|
+
description: "Picture style id from social_look (omit = Maggie picks from the brand's 3)"
|
|
10383
10598
|
}
|
|
10384
10599
|
},
|
|
10385
10600
|
required: ["id"]
|
|
@@ -10388,36 +10603,253 @@ var socialTools = [
|
|
|
10388
10603
|
need: write5,
|
|
10389
10604
|
risk: "W2",
|
|
10390
10605
|
gates: ["VER", "CR"],
|
|
10391
|
-
run: async (ctx, { id, replace, instruction }) => {
|
|
10606
|
+
run: async (ctx, { id, replace, instruction, mode, count, text_effect, style }) => {
|
|
10392
10607
|
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10393
10608
|
replace: replace === true,
|
|
10609
|
+
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
10610
|
+
...mode === "edit" ? { mode: "edit" } : {},
|
|
10611
|
+
...Number(count) === 4 && mode !== "edit" ? { count: 4 } : {},
|
|
10612
|
+
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
10394
10613
|
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10395
10614
|
});
|
|
10396
10615
|
return {
|
|
10397
10616
|
id: out.id,
|
|
10398
10617
|
image_job: out.image_job,
|
|
10399
|
-
note: "Making it in the background; it shows on the post when ready."
|
|
10618
|
+
note: out.image_job?.status === "queued" ? "In line after the picture being made now; it shows on the post when ready." : "Making it in the background; it shows on the post when ready."
|
|
10619
|
+
};
|
|
10620
|
+
}
|
|
10621
|
+
}),
|
|
10622
|
+
tool({
|
|
10623
|
+
name: "social_images_fill",
|
|
10624
|
+
description: "A new picture for every social post here that hasn't gone out and has none (or only these ids), made one after another in the background: 1 AI generation each. A LinkedIn and a Facebook post of the same idea share one picture. Says how many are in line, how many already were, and why it stopped early (the line is full, or the day's AI generations ran out). Preview the count first.",
|
|
10625
|
+
parameters: {
|
|
10626
|
+
type: "object",
|
|
10627
|
+
properties: {
|
|
10628
|
+
ids: { type: "array", items: { type: "integer" }, description: "Only these posts (from social_list)" },
|
|
10629
|
+
instruction: { type: "string", description: "What the user wants in every picture, up to 300 characters" }
|
|
10630
|
+
}
|
|
10631
|
+
},
|
|
10632
|
+
group: "social",
|
|
10633
|
+
need: write5,
|
|
10634
|
+
risk: "W2",
|
|
10635
|
+
gates: ["VER", "CR"],
|
|
10636
|
+
run: async (ctx, { ids: ids2, instruction }) => {
|
|
10637
|
+
const out = await ws(ctx).fillSocialImages({
|
|
10638
|
+
...Array.isArray(ids2) && ids2.length ? { item_ids: ids2.map(Number) } : {},
|
|
10639
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10640
|
+
});
|
|
10641
|
+
return { queued: out.queued, already: out.skipped, stopped_because: out.message, line: out.image_jobs.length };
|
|
10642
|
+
}
|
|
10643
|
+
}),
|
|
10644
|
+
tool({
|
|
10645
|
+
name: "social_text_effects",
|
|
10646
|
+
description: "The styles for the words on a set of pictures (social_image_new with count 4): id, name, family and a sample picture URL. Use when the user wants to choose how the words look ('bold sticker', 'neon', 'gold').",
|
|
10647
|
+
parameters: none,
|
|
10648
|
+
group: "social",
|
|
10649
|
+
need: read5,
|
|
10650
|
+
risk: "R0",
|
|
10651
|
+
run: async (ctx) => {
|
|
10652
|
+
const out = await ws(ctx).socialTextEffects();
|
|
10653
|
+
return { effects: out.effects.map(({ id, name, family }) => ({ id, name, family })), auto: out.auto };
|
|
10654
|
+
}
|
|
10655
|
+
}),
|
|
10656
|
+
tool({
|
|
10657
|
+
name: "social_look",
|
|
10658
|
+
description: "The brand's picture look on this website (made the first time it's asked for): the 3 picture styles Maggie takes turns with, the 5 text styles she picks from per picture, two fonts, the colours (ink, paper, primary, accent, muted), a short description of how its photos look, and things never to show; plus every style, font and text style that can be chosen. Read it before changing the look or when the user asks how their pictures look.",
|
|
10659
|
+
parameters: none,
|
|
10660
|
+
group: "social",
|
|
10661
|
+
need: read5,
|
|
10662
|
+
risk: "R0",
|
|
10663
|
+
run: async (ctx) => {
|
|
10664
|
+
const l = await ws(ctx).socialLook();
|
|
10665
|
+
return {
|
|
10666
|
+
styles: l.styles.map((s) => ({ id: s.id, name: s.name, about: s.about })),
|
|
10667
|
+
text_effects: l.text_effects.map((e) => ({ id: e.id, name: e.name, family: e.family })),
|
|
10668
|
+
fonts: l.fonts,
|
|
10669
|
+
colours: l.colours,
|
|
10670
|
+
description: l.brand_suffix,
|
|
10671
|
+
never: l.never,
|
|
10672
|
+
why: l.suggested.reason ?? null,
|
|
10673
|
+
can_choose: {
|
|
10674
|
+
styles: l.catalogue.styles.map((s) => ({ id: s.id, name: s.name, led: s.led, needs_photo: s.needs_photo })),
|
|
10675
|
+
fonts: l.catalogue.fonts,
|
|
10676
|
+
text_effect_families: [...new Set(l.catalogue.text_effects.map((e) => e.family))]
|
|
10677
|
+
}
|
|
10400
10678
|
};
|
|
10401
10679
|
}
|
|
10402
10680
|
}),
|
|
10681
|
+
tool({
|
|
10682
|
+
name: "social_look_update",
|
|
10683
|
+
description: "Change the brand's picture look: the 3 styles Maggie takes turns with (style_ids, 1-3), the text styles (text_effect_ids, 1-5, ids from social_text_effects), fonts, colours (hex), the description of how photos look, things never to show, or reset: true (back to Maggie's suggestion). New pictures use it; pictures already made keep theirs (social_picture_edit changes one). Preview the change first.",
|
|
10684
|
+
parameters: {
|
|
10685
|
+
type: "object",
|
|
10686
|
+
properties: {
|
|
10687
|
+
style_ids: { type: "array", items: { type: "string" }, description: "1-3 picture style ids" },
|
|
10688
|
+
text_effect_ids: { type: "array", items: { type: "string" }, description: "1-5 text style ids" },
|
|
10689
|
+
font_display: { type: "string", description: "Font for big words (from social_look can_choose.fonts)" },
|
|
10690
|
+
font_body: { type: "string", description: "Font for lines and lists" },
|
|
10691
|
+
colours: {
|
|
10692
|
+
type: "object",
|
|
10693
|
+
description: "Any of these as #RRGGBB",
|
|
10694
|
+
properties: {
|
|
10695
|
+
ink: { type: "string", description: "Text on paper" },
|
|
10696
|
+
paper: { type: "string", description: "The light ground" },
|
|
10697
|
+
primary: { type: "string", description: "The brand colour" },
|
|
10698
|
+
accent: { type: "string", description: "A second colour" },
|
|
10699
|
+
muted: { type: "string", description: "Small text" }
|
|
10700
|
+
}
|
|
10701
|
+
},
|
|
10702
|
+
description: { type: "string", description: "How the brand's photos look, up to 600 characters" },
|
|
10703
|
+
never: { type: "array", items: { type: "string" }, description: "Things never to show (the whole list)" },
|
|
10704
|
+
reset: { type: "boolean", description: "Back to Maggie's suggested look" }
|
|
10705
|
+
}
|
|
10706
|
+
},
|
|
10707
|
+
group: "social",
|
|
10708
|
+
need: write5,
|
|
10709
|
+
risk: "W1",
|
|
10710
|
+
gates: ["VER"],
|
|
10711
|
+
run: async (ctx, a) => {
|
|
10712
|
+
const fonts = {
|
|
10713
|
+
...a.font_display ? { display: a.font_display } : {},
|
|
10714
|
+
...a.font_body ? { body: a.font_body } : {}
|
|
10715
|
+
};
|
|
10716
|
+
const l = await ws(ctx).updateSocialLook({
|
|
10717
|
+
...a.style_ids ? { style_ids: a.style_ids } : {},
|
|
10718
|
+
...a.text_effect_ids ? { text_effect_ids: a.text_effect_ids } : {},
|
|
10719
|
+
...Object.keys(fonts).length ? { fonts } : {},
|
|
10720
|
+
...a.colours ? { colours: a.colours } : {},
|
|
10721
|
+
...typeof a.description === "string" ? { brand_suffix: a.description } : {},
|
|
10722
|
+
...a.never ? { never: a.never } : {},
|
|
10723
|
+
...a.reset ? { reset: true } : {}
|
|
10724
|
+
});
|
|
10725
|
+
return {
|
|
10726
|
+
styles: l.styles.map((s) => s.id),
|
|
10727
|
+
text_effects: l.text_effects.map((e) => e.id),
|
|
10728
|
+
fonts: l.fonts,
|
|
10729
|
+
colours: l.colours
|
|
10730
|
+
};
|
|
10731
|
+
}
|
|
10732
|
+
}),
|
|
10733
|
+
tool({
|
|
10734
|
+
name: "social_picture_edit",
|
|
10735
|
+
description: "Change a picture Maggie made for a post that hasn't gone out, without making a new one (free, a few seconds): the words on each picture (title up to 60 characters, line up to 90), its picture style, its text style, or for a set the order (indexes, 0 = first) or pictures to drop. Only posts whose picture says editable in social_list; a photo style needs the picture's photos (has_photos). Changing the words of one picture: send every picture's words in order. Preview first.",
|
|
10736
|
+
parameters: {
|
|
10737
|
+
type: "object",
|
|
10738
|
+
properties: {
|
|
10739
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10740
|
+
words: {
|
|
10741
|
+
type: "array",
|
|
10742
|
+
items: {
|
|
10743
|
+
type: "object",
|
|
10744
|
+
properties: { headline: { type: "string" }, sub: { type: "string" } },
|
|
10745
|
+
required: ["headline"]
|
|
10746
|
+
},
|
|
10747
|
+
description: "The words on each picture, in order (in the new order when order is given)"
|
|
10748
|
+
},
|
|
10749
|
+
style: { type: "string", description: "A picture style id from social_look" },
|
|
10750
|
+
text_effect: { type: "string", description: "A text style id from social_text_effects" },
|
|
10751
|
+
order: { type: "array", items: { type: "integer" }, description: "The pictures in their new order" },
|
|
10752
|
+
remove: { type: "array", items: { type: "integer" }, description: "Pictures to drop from a set" }
|
|
10753
|
+
},
|
|
10754
|
+
required: ["id"]
|
|
10755
|
+
},
|
|
10756
|
+
group: "social",
|
|
10757
|
+
need: write5,
|
|
10758
|
+
risk: "W1",
|
|
10759
|
+
gates: ["VER"],
|
|
10760
|
+
run: async (ctx, { id, words, style, text_effect, order, remove: remove2 }) => {
|
|
10761
|
+
const out = await ws(ctx).editSocialPicture(Number(id), {
|
|
10762
|
+
...Array.isArray(words) ? { words: words.map((w) => ({ headline: String(w.headline ?? ""), sub: String(w.sub ?? "") })) } : {},
|
|
10763
|
+
...style ? { style } : {},
|
|
10764
|
+
...text_effect ? { text_effect } : {},
|
|
10765
|
+
...Array.isArray(order) ? { order: order.map(Number) } : {},
|
|
10766
|
+
...Array.isArray(remove2) ? { remove: remove2.map(Number) } : {}
|
|
10767
|
+
});
|
|
10768
|
+
return { id: out.id, pictures: out.image_urls?.length ? out.image_urls : [out.image_url], picture: out.picture };
|
|
10769
|
+
}
|
|
10770
|
+
}),
|
|
10771
|
+
tool({
|
|
10772
|
+
name: "social_photo_words",
|
|
10773
|
+
description: "Put a title and a line on the user's own photo(s) on a post, in the brand's look: Maggie writes the words from the post (no AI picture, free). Only for a post whose picture is the user's own photo (own_photo in social_list). style: real_photo (default, the photo framed with the words below), product_hero (the words on a card), or another photo style. Then social_picture_edit changes the words. Preview first.",
|
|
10774
|
+
parameters: {
|
|
10775
|
+
type: "object",
|
|
10776
|
+
properties: {
|
|
10777
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10778
|
+
style: {
|
|
10779
|
+
type: "string",
|
|
10780
|
+
description: "real_photo (default), product_hero, photo_headline, editorial, duotone"
|
|
10781
|
+
}
|
|
10782
|
+
},
|
|
10783
|
+
required: ["id"]
|
|
10784
|
+
},
|
|
10785
|
+
group: "social",
|
|
10786
|
+
need: write5,
|
|
10787
|
+
risk: "W1",
|
|
10788
|
+
gates: ["VER"],
|
|
10789
|
+
run: async (ctx, { id, style }) => {
|
|
10790
|
+
const out = await ws(ctx).wordsOnPhoto(Number(id), style || "real_photo");
|
|
10791
|
+
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
10792
|
+
}
|
|
10793
|
+
}),
|
|
10794
|
+
tool({
|
|
10795
|
+
name: "social_post_insights",
|
|
10796
|
+
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).",
|
|
10797
|
+
parameters: {
|
|
10798
|
+
type: "object",
|
|
10799
|
+
properties: {
|
|
10800
|
+
id: { type: "integer", description: "A posted Instagram post (from social_list posted)" },
|
|
10801
|
+
refresh: { type: "boolean", description: "Read the numbers again now" }
|
|
10802
|
+
},
|
|
10803
|
+
required: ["id"]
|
|
10804
|
+
},
|
|
10805
|
+
group: "social",
|
|
10806
|
+
need: read5,
|
|
10807
|
+
risk: "R0",
|
|
10808
|
+
run: async (ctx, { id, refresh }) => ws(ctx).socialInsights(Number(id), refresh === true)
|
|
10809
|
+
}),
|
|
10810
|
+
tool({
|
|
10811
|
+
name: "social_write_status",
|
|
10812
|
+
description: "Posts Maggie is writing in the background for this user on this website (from social_from_article, social_plan_days, social_idea_write or the app), and the ones finished in the last 10 minutes: running, done (the new post ids) or failed (why), and any channel that couldn't be written. Look once when the user asks, not in a loop.",
|
|
10813
|
+
parameters: none,
|
|
10814
|
+
group: "social",
|
|
10815
|
+
need: read5,
|
|
10816
|
+
risk: "R0",
|
|
10817
|
+
run: async (ctx) => ({
|
|
10818
|
+
jobs: ((await ws(ctx).social()).write_jobs ?? []).map((j) => ({
|
|
10819
|
+
id: j.id,
|
|
10820
|
+
kind: j.kind,
|
|
10821
|
+
status: j.status,
|
|
10822
|
+
channels: j.channels,
|
|
10823
|
+
expected: j.expected,
|
|
10824
|
+
written: j.item_ids,
|
|
10825
|
+
queued: j.queued,
|
|
10826
|
+
...j.failed?.length ? { failed: j.failed } : {},
|
|
10827
|
+
...j.error ? { error: j.error } : {},
|
|
10828
|
+
started_at: j.started_at
|
|
10829
|
+
}))
|
|
10830
|
+
})
|
|
10831
|
+
}),
|
|
10403
10832
|
tool({
|
|
10404
10833
|
name: "social_image_status",
|
|
10405
|
-
description: "
|
|
10834
|
+
description: "The picture being made on this website and those waiting in line (which post, new or edit), or how the last one ended (done, failed with why, stopped) and who drew it (chatgpt or gemini).",
|
|
10406
10835
|
parameters: none,
|
|
10407
10836
|
group: "social",
|
|
10408
10837
|
need: read5,
|
|
10409
10838
|
risk: "R0",
|
|
10410
|
-
run: async (ctx) =>
|
|
10839
|
+
run: async (ctx) => {
|
|
10840
|
+
const s = await ws(ctx).social();
|
|
10841
|
+
return { image_job: s.image_job ?? null, line: s.image_jobs ?? [] };
|
|
10842
|
+
}
|
|
10411
10843
|
}),
|
|
10412
10844
|
tool({
|
|
10413
10845
|
name: "social_image_stop",
|
|
10414
|
-
description: "Stop the
|
|
10415
|
-
parameters:
|
|
10846
|
+
description: "Stop the picture being made on this website (the post keeps the picture it had; the next in line starts). all: true also empties the line.",
|
|
10847
|
+
parameters: { type: "object", properties: { all: { type: "boolean" } } },
|
|
10416
10848
|
group: "social",
|
|
10417
10849
|
need: write5,
|
|
10418
10850
|
risk: "W1",
|
|
10419
10851
|
gates: ["VER"],
|
|
10420
|
-
run: async (ctx) => ws(ctx).stopSocialImage()
|
|
10852
|
+
run: async (ctx, { all }) => ws(ctx).stopSocialImage(all === true)
|
|
10421
10853
|
}),
|
|
10422
10854
|
tool({
|
|
10423
10855
|
name: "social_move",
|
|
@@ -10438,7 +10870,7 @@ var socialTools = [
|
|
|
10438
10870
|
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10439
10871
|
parameters: {
|
|
10440
10872
|
type: "object",
|
|
10441
|
-
properties: { paused: { type: "boolean" }, channel: { type: "string", enum:
|
|
10873
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: ALL_CHANNEL_KEYS } },
|
|
10442
10874
|
required: ["paused"]
|
|
10443
10875
|
},
|
|
10444
10876
|
group: "social",
|
|
@@ -10461,7 +10893,7 @@ var socialTools = [
|
|
|
10461
10893
|
}),
|
|
10462
10894
|
tool({
|
|
10463
10895
|
name: "social_from_article",
|
|
10464
|
-
description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it goes to the front of the queue).",
|
|
10896
|
+
description: "Maggie writes a post about one published article (id from social_sources_list) for each channel. It waits for the user's OK as a draft, unless their approval mode is auto (then it goes to the front of the queue). Written in the background (a minute or two): this returns at once with a job. Tell the user Maggie is writing and they will be told when it's ready; before saying what was written, check social_write_status (then social_list for the posts).",
|
|
10465
10897
|
parameters: {
|
|
10466
10898
|
type: "object",
|
|
10467
10899
|
properties: {
|
|
@@ -10475,14 +10907,11 @@ var socialTools = [
|
|
|
10475
10907
|
need: write5,
|
|
10476
10908
|
risk: "W2",
|
|
10477
10909
|
gates: ["VER", "LI", "LLM"],
|
|
10478
|
-
run: async (ctx, { source_id, channels }) =>
|
|
10479
|
-
const out = await ws(ctx).socialFromPost(String(source_id), channelsOf(channels));
|
|
10480
|
-
return { id: out.id, status: out.status, queued: Boolean(out.queued), text: clip(out.social_copy, 300) };
|
|
10481
|
-
}
|
|
10910
|
+
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
10482
10911
|
}),
|
|
10483
10912
|
tool({
|
|
10484
10913
|
name: "social_plan_days",
|
|
10485
|
-
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).",
|
|
10914
|
+
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).",
|
|
10486
10915
|
parameters: {
|
|
10487
10916
|
type: "object",
|
|
10488
10917
|
properties: {
|
|
@@ -10496,10 +10925,7 @@ var socialTools = [
|
|
|
10496
10925
|
need: write5,
|
|
10497
10926
|
risk: "W2",
|
|
10498
10927
|
gates: ["VER", "LI", "LLM"],
|
|
10499
|
-
run: async (ctx, { days, timezone, channels }) =>
|
|
10500
|
-
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels));
|
|
10501
|
-
return { written: out.items.length, queued: Boolean(out.queued), items: out.items.map(item) };
|
|
10502
|
-
}
|
|
10928
|
+
run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
|
|
10503
10929
|
}),
|
|
10504
10930
|
tool({
|
|
10505
10931
|
name: "social_ideas_list",
|
|
@@ -10572,7 +10998,7 @@ var socialTools = [
|
|
|
10572
10998
|
}),
|
|
10573
10999
|
tool({
|
|
10574
11000
|
name: "social_idea_write",
|
|
10575
|
-
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.",
|
|
11001
|
+
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).",
|
|
10576
11002
|
parameters: {
|
|
10577
11003
|
type: "object",
|
|
10578
11004
|
properties: {
|
|
@@ -10586,7 +11012,7 @@ var socialTools = [
|
|
|
10586
11012
|
need: write5,
|
|
10587
11013
|
risk: "W2",
|
|
10588
11014
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
10589
|
-
run: async (ctx, { id, channels }) => ws(ctx).
|
|
11015
|
+
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
10590
11016
|
}),
|
|
10591
11017
|
tool({
|
|
10592
11018
|
name: "social_idea_brief",
|
|
@@ -10883,9 +11309,9 @@ var socialTools = [
|
|
|
10883
11309
|
run: async (ctx, { keys: keys2, ...options }) => {
|
|
10884
11310
|
const api2 = ws(ctx);
|
|
10885
11311
|
const current = (await api2.socialSetup()).writing;
|
|
10886
|
-
const
|
|
10887
|
-
for (const [k, v] of Object.entries(options)) if (v !== void 0)
|
|
10888
|
-
return api2.socialSetupTemplates((keys2 ?? []).slice(0, 10),
|
|
11312
|
+
const writing2 = { ...current };
|
|
11313
|
+
for (const [k, v] of Object.entries(options)) if (v !== void 0) writing2[k] = v;
|
|
11314
|
+
return api2.socialSetupTemplates((keys2 ?? []).slice(0, 10), writing2);
|
|
10889
11315
|
}
|
|
10890
11316
|
}),
|
|
10891
11317
|
tool({
|
|
@@ -10966,6 +11392,220 @@ var socialTools = [
|
|
|
10966
11392
|
const out = await ws(ctx).connectFacebook("close");
|
|
10967
11393
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10968
11394
|
}
|
|
11395
|
+
}),
|
|
11396
|
+
tool({
|
|
11397
|
+
name: "google_business_connect_link",
|
|
11398
|
+
description: "A link for the user to connect their Google Business Profile (or reconnect): they sign in with the Google account that manages the profile; every profile it can post to is saved and the one whose website is this site is picked (else they pick it in the app). Only verified profiles can post. Give them the link to open; never ask for a password. Google posts are short local updates with one photo and a button; Google checks each one before it shows.",
|
|
11399
|
+
parameters: none,
|
|
11400
|
+
group: "social",
|
|
11401
|
+
need: write5,
|
|
11402
|
+
risk: "W1",
|
|
11403
|
+
gates: ["VER"],
|
|
11404
|
+
run: async (ctx) => {
|
|
11405
|
+
const out = await ws(ctx).connectGoogleBusiness("close");
|
|
11406
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11407
|
+
}
|
|
11408
|
+
}),
|
|
11409
|
+
tool({
|
|
11410
|
+
name: "social_google_button",
|
|
11411
|
+
description: "Set the button on a Google Business post that hasn't gone out: LEARN_MORE (default, opens the post's link), BOOK (a booking link), ORDER, SHOP, SIGN_UP, or CALL (calls the profile's phone number; needs no link). Pick the one that matches what the post asks people to do. Preview first.",
|
|
11412
|
+
parameters: {
|
|
11413
|
+
type: "object",
|
|
11414
|
+
properties: {
|
|
11415
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11416
|
+
button: { type: "string", enum: ["LEARN_MORE", "BOOK", "ORDER", "SHOP", "SIGN_UP", "CALL"] }
|
|
11417
|
+
},
|
|
11418
|
+
required: ["id", "button"]
|
|
11419
|
+
},
|
|
11420
|
+
group: "social",
|
|
11421
|
+
need: write5,
|
|
11422
|
+
risk: "W1",
|
|
11423
|
+
gates: ["VER"],
|
|
11424
|
+
run: async (ctx, { id, button }) => {
|
|
11425
|
+
const out = await ws(ctx).setSocialButton(Number(id), button);
|
|
11426
|
+
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11427
|
+
}
|
|
11428
|
+
}),
|
|
11429
|
+
tool({
|
|
11430
|
+
name: "youtube_connect_link",
|
|
11431
|
+
description: "A link for the user to connect their YouTube channel (or reconnect): they sign in with the Google account that owns the channel and allow Maggie to upload videos and see the channel. Give them the link to open; never ask for a password. YouTube posts are short videos (5-30 seconds now) that the user approves one by one.",
|
|
11432
|
+
parameters: none,
|
|
11433
|
+
group: "social",
|
|
11434
|
+
need: write5,
|
|
11435
|
+
risk: "W1",
|
|
11436
|
+
gates: ["VER"],
|
|
11437
|
+
run: async (ctx) => {
|
|
11438
|
+
const out = await ws(ctx).connectYouTube("close");
|
|
11439
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11440
|
+
}
|
|
11441
|
+
}),
|
|
11442
|
+
tool({
|
|
11443
|
+
name: "social_video_add",
|
|
11444
|
+
description: "Make a YouTube post from a video: a DIRECT link to a video file (an .mp4 on their website, a public Dropbox link), never a YouTube / TikTok / Instagram page (we never download those; tell the user to upload the video in the app instead). The video must be 5-30 seconds: give start/end (seconds) to use a part of a longer one. vertical: make a wide video vertical (a Short) cropped at left, middle or right. Maggie writes the title, description and tags from the video and the note (one AI generation) unless write is false. The post waits in Review: YouTube posts are always approved by the user, who must also answer whether it's made for kids. Preview first.",
|
|
11445
|
+
parameters: {
|
|
11446
|
+
type: "object",
|
|
11447
|
+
properties: {
|
|
11448
|
+
url: { type: "string", description: "Direct https link to the video file" },
|
|
11449
|
+
note: { type: "string", description: "What the video is about, in the user's words" },
|
|
11450
|
+
write: { type: "boolean", description: "Let Maggie write the details (default true)" },
|
|
11451
|
+
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
11452
|
+
end: { type: "number", description: "End of the part to use, seconds" },
|
|
11453
|
+
vertical: { type: "string", enum: ["left", "middle", "right"] }
|
|
11454
|
+
},
|
|
11455
|
+
required: ["url"]
|
|
11456
|
+
},
|
|
11457
|
+
group: "social",
|
|
11458
|
+
need: write5,
|
|
11459
|
+
risk: "W2",
|
|
11460
|
+
gates: ["VER", "LLM"],
|
|
11461
|
+
run: async (ctx, a) => {
|
|
11462
|
+
const d = ws(ctx);
|
|
11463
|
+
const edit = {
|
|
11464
|
+
...a.start != null ? { start: Number(a.start) } : {},
|
|
11465
|
+
...a.end != null ? { end: Number(a.end) } : {},
|
|
11466
|
+
...a.vertical ? { aspect: "9:16", crop_x: a.vertical === "left" ? 0 : a.vertical === "right" ? 1 : 0.5 } : {}
|
|
11467
|
+
};
|
|
11468
|
+
let v = await d.videoFromLink(String(a.url), Object.keys(edit).length ? edit : void 0);
|
|
11469
|
+
const until = Date.now() + 15e4;
|
|
11470
|
+
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
11471
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
11472
|
+
v = await d.video(v.id);
|
|
11473
|
+
}
|
|
11474
|
+
if (v.status !== "ready")
|
|
11475
|
+
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11476
|
+
const post = await d.postFromVideo(v.id, { note: a.note ?? "", write: a.write !== false });
|
|
11477
|
+
return {
|
|
11478
|
+
ok: true,
|
|
11479
|
+
post: item(post),
|
|
11480
|
+
next: "Show the title and description; ask whether it's made for kids and which visibility, then social_youtube_set; the user approves it."
|
|
11481
|
+
};
|
|
11482
|
+
}
|
|
11483
|
+
}),
|
|
11484
|
+
tool({
|
|
11485
|
+
name: "social_youtube_set",
|
|
11486
|
+
description: "Change a YouTube post that hasn't gone out: title (at most 100 characters, aim 40-60), tags, visibility (public, unlisted, private), made_for_kids (YouTube asks for every video: ask the user, never guess) or synthetic (realistic AI-made or altered content). The description is the post's text (social_update_copy). Preview first.",
|
|
11487
|
+
parameters: {
|
|
11488
|
+
type: "object",
|
|
11489
|
+
properties: {
|
|
11490
|
+
id: { type: "integer", description: "A YouTube post (from social_list)" },
|
|
11491
|
+
title: { type: "string" },
|
|
11492
|
+
tags: { type: "array", items: { type: "string" } },
|
|
11493
|
+
privacy: { type: "string", enum: ["public", "unlisted", "private"] },
|
|
11494
|
+
made_for_kids: { type: "boolean" },
|
|
11495
|
+
synthetic: { type: "boolean" }
|
|
11496
|
+
},
|
|
11497
|
+
required: ["id"]
|
|
11498
|
+
},
|
|
11499
|
+
group: "social",
|
|
11500
|
+
need: write5,
|
|
11501
|
+
risk: "W1",
|
|
11502
|
+
gates: ["VER"],
|
|
11503
|
+
run: async (ctx, { id, ...rest }) => {
|
|
11504
|
+
const answers = Object.fromEntries(
|
|
11505
|
+
Object.entries(rest).filter(([, v]) => v !== void 0)
|
|
11506
|
+
);
|
|
11507
|
+
const out = await ws(ctx).setSocialYouTube(Number(id), answers);
|
|
11508
|
+
return item(out);
|
|
11509
|
+
}
|
|
11510
|
+
}),
|
|
11511
|
+
tool({
|
|
11512
|
+
name: "youtube_settings",
|
|
11513
|
+
description: "The user's YouTube defaults for new posts on this website: visibility, made_for_kids (null = asked each time), category and altered content.",
|
|
11514
|
+
parameters: none,
|
|
11515
|
+
group: "social",
|
|
11516
|
+
need: read5,
|
|
11517
|
+
risk: "R0",
|
|
11518
|
+
run: async (ctx) => ws(ctx).youtubeSettings()
|
|
11519
|
+
}),
|
|
11520
|
+
tool({
|
|
11521
|
+
name: "youtube_settings_set",
|
|
11522
|
+
description: "Change the YouTube defaults for new posts: privacy (public, unlisted, private), made_for_kids (yes / no for every video, or ask per video) and synthetic. Only what the user says. Preview first.",
|
|
11523
|
+
parameters: {
|
|
11524
|
+
type: "object",
|
|
11525
|
+
properties: {
|
|
11526
|
+
privacy: { type: "string", enum: ["public", "unlisted", "private"] },
|
|
11527
|
+
made_for_kids: {
|
|
11528
|
+
type: "string",
|
|
11529
|
+
enum: ["yes", "no", "ask"],
|
|
11530
|
+
description: "yes / no for every new video, or ask (answer per video)"
|
|
11531
|
+
},
|
|
11532
|
+
synthetic: { type: "boolean" }
|
|
11533
|
+
}
|
|
11534
|
+
},
|
|
11535
|
+
group: "social",
|
|
11536
|
+
need: write5,
|
|
11537
|
+
risk: "W1",
|
|
11538
|
+
gates: ["VER"],
|
|
11539
|
+
run: async (ctx, a) => ws(ctx).setYouTubeSettings({
|
|
11540
|
+
...a.privacy ? { privacy: a.privacy } : {},
|
|
11541
|
+
...a.made_for_kids ? { made_for_kids: a.made_for_kids === "ask" ? null : a.made_for_kids === "yes" } : {},
|
|
11542
|
+
...a.synthetic != null ? { synthetic: a.synthetic } : {}
|
|
11543
|
+
})
|
|
11544
|
+
}),
|
|
11545
|
+
tool({
|
|
11546
|
+
name: "google_business_performance",
|
|
11547
|
+
description: "How the website's Google Business Profile is doing: views on Google Search and Maps, website clicks, calls and direction requests for the last 28 days next to the 28 before (Google's numbers lag 2-3 days). Use it when the user asks how their Google listing or local presence is doing, or to show what Google posts are worth. Compare with the previous 28 days; don't invent reasons for changes.",
|
|
11548
|
+
parameters: {
|
|
11549
|
+
type: "object",
|
|
11550
|
+
properties: { refresh: { type: "boolean", description: "Read Google again now (else up to 6 hours old)" } }
|
|
11551
|
+
},
|
|
11552
|
+
group: "social",
|
|
11553
|
+
need: read5,
|
|
11554
|
+
risk: "R0",
|
|
11555
|
+
run: async (ctx, { refresh }) => ws(ctx).googleBusinessPerformance(refresh === true)
|
|
11556
|
+
}),
|
|
11557
|
+
tool({
|
|
11558
|
+
name: "social_google_post_type",
|
|
11559
|
+
description: "Make a Google Business post that hasn't gone out an EVENT (an open day, a workshop: title up to 58 characters, start and end dates YYYY-MM-DD, optional times HH:MM) or an OFFER (title, dates, optional coupon code, terms and redeem link; Google adds a View offer button), or back to STANDARD (an update). Offers are refused for regulated categories (finance, health, alcohol, gambling) and hotels: use an update or an event. Only real dates and offers the user gave. Preview first.",
|
|
11560
|
+
parameters: {
|
|
11561
|
+
type: "object",
|
|
11562
|
+
properties: {
|
|
11563
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11564
|
+
type: { type: "string", enum: ["STANDARD", "EVENT", "OFFER"] },
|
|
11565
|
+
title: { type: "string", description: "Event or offer title, up to 58 characters" },
|
|
11566
|
+
start: { type: "string", description: "YYYY-MM-DD" },
|
|
11567
|
+
end: { type: "string", description: "YYYY-MM-DD (defaults to start)" },
|
|
11568
|
+
start_time: { type: "string", description: "HH:MM (optional)" },
|
|
11569
|
+
end_time: { type: "string", description: "HH:MM (optional)" },
|
|
11570
|
+
coupon: { type: "string", description: "Offer code (optional)" },
|
|
11571
|
+
terms: { type: "string", description: "Offer terms (optional)" },
|
|
11572
|
+
redeem_url: { type: "string", description: "Where to redeem the offer online (optional)" }
|
|
11573
|
+
},
|
|
11574
|
+
required: ["id", "type"]
|
|
11575
|
+
},
|
|
11576
|
+
group: "social",
|
|
11577
|
+
need: write5,
|
|
11578
|
+
risk: "W1",
|
|
11579
|
+
gates: ["VER"],
|
|
11580
|
+
run: async (ctx, a) => {
|
|
11581
|
+
const type = String(a.type).toUpperCase();
|
|
11582
|
+
const body = type === "STANDARD" ? { type: "STANDARD" } : {
|
|
11583
|
+
type,
|
|
11584
|
+
title: String(a.title ?? ""),
|
|
11585
|
+
start: String(a.start ?? ""),
|
|
11586
|
+
...a.end ? { end: a.end } : {},
|
|
11587
|
+
...a.start_time ? { start_time: a.start_time } : {},
|
|
11588
|
+
...a.end_time ? { end_time: a.end_time } : {},
|
|
11589
|
+
...a.coupon ? { coupon: a.coupon } : {},
|
|
11590
|
+
...a.terms ? { terms: a.terms } : {},
|
|
11591
|
+
...a.redeem_url ? { redeem_url: a.redeem_url } : {}
|
|
11592
|
+
};
|
|
11593
|
+
const out = await ws(ctx).setSocialGooglePost(Number(a.id), body);
|
|
11594
|
+
return { id: out.id, google_post: out.google_post ?? null };
|
|
11595
|
+
}
|
|
11596
|
+
}),
|
|
11597
|
+
tool({
|
|
11598
|
+
name: "instagram_connect_link",
|
|
11599
|
+
description: "A link for the user to connect Instagram (or reconnect). Instagram connects through the Facebook sign-in: only an Instagram professional account (Business or Creator) linked to one of their Facebook Pages can be posted to, so tell them that first. Give them the link to open; never ask for a password. Every Instagram post goes out with its picture (no links in it).",
|
|
11600
|
+
parameters: none,
|
|
11601
|
+
group: "social",
|
|
11602
|
+
need: write5,
|
|
11603
|
+
risk: "W1",
|
|
11604
|
+
gates: ["VER"],
|
|
11605
|
+
run: async (ctx) => {
|
|
11606
|
+
const out = await ws(ctx).connectInstagram("close");
|
|
11607
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11608
|
+
}
|
|
10969
11609
|
})
|
|
10970
11610
|
];
|
|
10971
11611
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CMO.so's MCP server for Claude Desktop, Cursor, VS Code and other MCP clients: manage your websites, blog posts, LinkedIn queue and results with Maggie.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|