@cmo-so/cli 0.2.0 → 0.3.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 +441 -41
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7473,7 +7473,7 @@ function dataUrl(type, bytes) {
|
|
|
7473
7473
|
}
|
|
7474
7474
|
|
|
7475
7475
|
// ../../packages/api-client/src/domains.ts
|
|
7476
|
-
var MEMORY_CHANNELS = ["general", "blog", "linkedin", "facebook"];
|
|
7476
|
+
var MEMORY_CHANNELS = ["general", "blog", "linkedin", "facebook", "instagram", "google_business"];
|
|
7477
7477
|
function addWebsite(api2, url) {
|
|
7478
7478
|
return api2.request("/v1/domains", {
|
|
7479
7479
|
method: "POST",
|
|
@@ -7592,8 +7592,15 @@ function domainApi(api2, domain) {
|
|
|
7592
7592
|
approveSocial: (id) => post(`${base}/social/${id}/approve`),
|
|
7593
7593
|
skipSocial: (id) => post(`${base}/social/${id}/skip`),
|
|
7594
7594
|
/** Post it on LinkedIn now, not at the next posting time (even while posting is paused). */
|
|
7595
|
-
|
|
7595
|
+
/** Post it now. `background`: the reply comes at once (status "publishing") and it goes out behind the
|
|
7596
|
+
* scenes (a notice when it's out); 409 `picture_busy` while its picture is being made. */
|
|
7597
|
+
publishSocialNow: (id, opts = {}) => post(
|
|
7598
|
+
`${base}/social/${id}/publish`,
|
|
7599
|
+
opts.background ? { background: true } : void 0
|
|
7600
|
+
),
|
|
7596
7601
|
/** Take a posted LinkedIn post down (its likes and comments go with it). */
|
|
7602
|
+
/** How a posted Instagram post is doing (read from Instagram, kept 15 minutes; `refresh` reads again). */
|
|
7603
|
+
socialInsights: (id, refresh = false) => api2.request(`${base}/social/${id}/insights${refresh ? "?refresh=true" : ""}`),
|
|
7597
7604
|
deletePostedSocial: (id) => api2.request(`${base}/social/${id}/published`, { method: "DELETE" }),
|
|
7598
7605
|
/** Change a posted LinkedIn post and put it up again: the old one comes down, the new text goes out now. */
|
|
7599
7606
|
republishSocial: (id, socialCopy) => post(`${base}/social/${id}/republish`, socialCopy ? { social_copy: socialCopy } : {}),
|
|
@@ -7629,10 +7636,10 @@ function domainApi(api2, domain) {
|
|
|
7629
7636
|
/** The template library, searchable. */
|
|
7630
7637
|
socialTemplates: (search = "") => api2.request(`${base}/social/templates?search=${encodeURIComponent(search)}`),
|
|
7631
7638
|
socialTemplatePreview: (key) => api2.request(`${base}/social/templates/${encodeURIComponent(key)}/preview`),
|
|
7632
|
-
socialSetupTemplates: (keys3,
|
|
7639
|
+
socialSetupTemplates: (keys3, writing2) => api2.request(`${base}/social/setup/templates`, {
|
|
7633
7640
|
method: "PUT",
|
|
7634
7641
|
headers: { "Content-Type": "application/json" },
|
|
7635
|
-
body: JSON.stringify({ keys: keys3, writing })
|
|
7642
|
+
body: JSON.stringify({ keys: keys3, writing: writing2 })
|
|
7636
7643
|
}),
|
|
7637
7644
|
socialSetupSchedule: (schedule) => api2.request(`${base}/social/setup/schedule`, {
|
|
7638
7645
|
method: "PUT",
|
|
@@ -7674,6 +7681,32 @@ function domainApi(api2, domain) {
|
|
|
7674
7681
|
}
|
|
7675
7682
|
),
|
|
7676
7683
|
disconnectFacebook: (connectionId) => api2.request(`${base}/social/facebook/${connectionId}`, { method: "DELETE" }),
|
|
7684
|
+
/** Instagram connects through the Facebook sign-in (the accounts linked to your Pages): the same link;
|
|
7685
|
+
* back with ?facebook=connected, then `channels` says whether an Instagram account was found. */
|
|
7686
|
+
connectInstagram: (returnTo = "close") => post(`${base}/social/instagram/connect`, { return_to: returnTo }),
|
|
7687
|
+
/** Your Instagram accounts here (each with the Facebook Page it posts through), and the chosen one. */
|
|
7688
|
+
instagramAccounts: () => api2.request(`${base}/social/instagram/accounts`),
|
|
7689
|
+
chooseInstagramAccount: (connectionId) => api2.request(
|
|
7690
|
+
`${base}/social/instagram/account`,
|
|
7691
|
+
{
|
|
7692
|
+
method: "PUT",
|
|
7693
|
+
headers: { "Content-Type": "application/json" },
|
|
7694
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
7695
|
+
}
|
|
7696
|
+
),
|
|
7697
|
+
disconnectInstagram: (connectionId) => api2.request(`${base}/social/instagram/${connectionId}`, { method: "DELETE" }),
|
|
7698
|
+
/** A Google sign-in link for this website's Google Business Profile; back with ?google_business=connected
|
|
7699
|
+
* (or none / error&message=…). Every profile the account can post to is saved; this website's is picked. */
|
|
7700
|
+
connectGoogleBusiness: (returnTo = "close") => post(`${base}/social/google_business/connect`, {
|
|
7701
|
+
return_to: returnTo
|
|
7702
|
+
}),
|
|
7703
|
+
/** Your Google Business profiles here (name, address, website), and the one this website posts to. */
|
|
7704
|
+
googleBusinessLocations: () => api2.request(`${base}/social/google_business/locations`),
|
|
7705
|
+
chooseGoogleBusinessLocation: (connectionId) => put(
|
|
7706
|
+
`${base}/social/google_business/location`,
|
|
7707
|
+
{ connection_id: connectionId }
|
|
7708
|
+
),
|
|
7709
|
+
disconnectGoogleBusiness: (connectionId) => api2.request(`${base}/social/google_business/${connectionId}`, { method: "DELETE" }),
|
|
7677
7710
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7678
7711
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7679
7712
|
/** How often one channel posts. */
|
|
@@ -7683,14 +7716,56 @@ function domainApi(api2, domain) {
|
|
|
7683
7716
|
),
|
|
7684
7717
|
/** Start a new picture for the post in the background (one AI generation); `replace` stops the one
|
|
7685
7718
|
* already running; `instruction` (up to 300 characters) steers it: "gold border, our logo". */
|
|
7719
|
+
// `mode: "edit"` changes the post's current picture as `instruction` says. Another picture
|
|
7720
|
+
// being made here: this one waits its turn (status queued).
|
|
7721
|
+
// `count: 4` makes a set of 4 pictures from one storyboard (one AI generation), with words on each in
|
|
7722
|
+
// `textEffect` (an id from socialTextEffects; omit = Maggie picks).
|
|
7686
7723
|
regenerateSocialImage: (id, opts = {}) => post(`${base}/social/${id}/image/regenerate`, {
|
|
7724
|
+
...opts.style ? { style: opts.style } : {},
|
|
7687
7725
|
...opts.replace ? { replace: true } : {},
|
|
7726
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7727
|
+
...opts.mode === "edit" ? { mode: "edit" } : {},
|
|
7728
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7729
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7730
|
+
}),
|
|
7731
|
+
/** Change a picture Maggie made without a new one (free): the words on each picture, its style, its text
|
|
7732
|
+
* style, the order of a set, or drop pictures from it. `words` follow the new order when `order` is given. */
|
|
7733
|
+
editSocialPicture: (id, changes) => api2.request(`${base}/social/${id}/picture`, {
|
|
7734
|
+
method: "PATCH",
|
|
7735
|
+
headers: { "Content-Type": "application/json" },
|
|
7736
|
+
body: JSON.stringify(changes)
|
|
7737
|
+
}),
|
|
7738
|
+
/** A title and a line on your own photo(s) in your look (Maggie plans the words; no AI picture, free). */
|
|
7739
|
+
wordsOnPhoto: (id, style = "real_photo") => post(`${base}/social/${id}/picture/words`, { style }),
|
|
7740
|
+
/** The brand's picture look (made the first time it's asked for). */
|
|
7741
|
+
socialLook: () => api2.request(`${base}/social/look`),
|
|
7742
|
+
updateSocialLook: (changes) => put(`${base}/social/look`, changes),
|
|
7743
|
+
/** Maggie looks again and suggests a new look (one AI generation). */
|
|
7744
|
+
suggestSocialLook: () => post(`${base}/social/look/suggest`, {}),
|
|
7745
|
+
/** A sample picture of a style in the brand's look: a data: URL. */
|
|
7746
|
+
socialLookSample: (style) => api2.request(`${base}/social/look/sample?style=${encodeURIComponent(style)}`),
|
|
7747
|
+
/** The text effects for the words on a set of pictures, each with a sample picture. */
|
|
7748
|
+
socialTextEffects: () => api2.request(`${base}/social/text-effects`),
|
|
7749
|
+
/** Your own photo (base64 JPEG) as the post's picture: no AI, no charge; only before it goes out.
|
|
7750
|
+
* With `instruction`, Maggie changes the photo as it says, in the background (one AI generation). */
|
|
7751
|
+
uploadSocialPhoto: (id, photo, opts = {}) => post(`${base}/social/${id}/image`, {
|
|
7752
|
+
photo,
|
|
7688
7753
|
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {}
|
|
7689
7754
|
}),
|
|
7690
|
-
/**
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7755
|
+
/** A new picture for every post here without one (or these), one after another in the background. */
|
|
7756
|
+
fillSocialImages: (opts = {}) => post(
|
|
7757
|
+
`${base}/social/images/fill`,
|
|
7758
|
+
{
|
|
7759
|
+
...opts.item_ids ? { item_ids: opts.item_ids } : {},
|
|
7760
|
+
...opts.instruction?.trim() ? { instruction: opts.instruction.trim() } : {},
|
|
7761
|
+
...opts.count && opts.count > 1 ? { count: opts.count } : {},
|
|
7762
|
+
...opts.textEffect ? { text_effect: opts.textEffect } : {}
|
|
7763
|
+
}
|
|
7764
|
+
),
|
|
7765
|
+
/** Stop the picture being made (it's thrown away; the next in line starts). `all` empties the line too. */
|
|
7766
|
+
stopSocialImage: (all = false) => api2.request(`${base}/social/image-job${all ? "?all=true" : ""}`, {
|
|
7767
|
+
method: "DELETE"
|
|
7768
|
+
}),
|
|
7694
7769
|
/** Maggie changes the post as asked, in 3-255 characters (one AI generation). */
|
|
7695
7770
|
rewriteSocial: (id, instruction) => post(`${base}/social/${id}/rewrite`, { instruction }),
|
|
7696
7771
|
/** Maggie plans and writes a series; it waits for one OK. */
|
|
@@ -7703,13 +7778,35 @@ function domainApi(api2, domain) {
|
|
|
7703
7778
|
brief: brief2,
|
|
7704
7779
|
...channels ? { channels } : {}
|
|
7705
7780
|
}),
|
|
7781
|
+
// In the background: the answer is the job at once; watch `write_jobs` in GET /social.
|
|
7782
|
+
startSocialFromPhotos: (photos, brief2 = "", channels) => post(`${base}/social/from-photos`, {
|
|
7783
|
+
photos,
|
|
7784
|
+
brief: brief2,
|
|
7785
|
+
background: true,
|
|
7786
|
+
...channels ? { channels } : {}
|
|
7787
|
+
}),
|
|
7788
|
+
startSocialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7789
|
+
source_post_id: sourcePostId,
|
|
7790
|
+
background: true,
|
|
7791
|
+
...channels ? { channels } : {}
|
|
7792
|
+
}),
|
|
7793
|
+
startSocialPlan: (days, timezone, channels) => post(`${base}/social/plan`, {
|
|
7794
|
+
days,
|
|
7795
|
+
timezone,
|
|
7796
|
+
background: true,
|
|
7797
|
+
...channels ? { channels } : {}
|
|
7798
|
+
}),
|
|
7799
|
+
startWriteSocialIdea: (id, channels) => post(`${base}/social/ideas/${id}/write`, {
|
|
7800
|
+
background: true,
|
|
7801
|
+
...channels ? { channels } : {}
|
|
7802
|
+
}),
|
|
7706
7803
|
/** Disconnect the website's LinkedIn account. */
|
|
7707
7804
|
disconnectLinkedIn: (connectionId) => api2.request(`${base}/social/linkedin/${connectionId}`, { method: "DELETE" }),
|
|
7708
7805
|
/** Published articles a LinkedIn post can be made from. */
|
|
7709
7806
|
socialSources: (search = "") => api2.request(
|
|
7710
7807
|
`${base}/social/sources?search=${encodeURIComponent(search)}`
|
|
7711
7808
|
),
|
|
7712
|
-
/** Maggie writes a
|
|
7809
|
+
/** Maggie writes a post about one article for each channel; `failed` lists a channel that could not be written while the others were. */
|
|
7713
7810
|
socialFromPost: (sourcePostId, channels) => post(`${base}/social/from-post`, {
|
|
7714
7811
|
source_post_id: sourcePostId,
|
|
7715
7812
|
...channels ? { channels } : {}
|
|
@@ -7768,6 +7865,12 @@ function domainApi(api2, domain) {
|
|
|
7768
7865
|
headers: { "Content-Type": "application/json" },
|
|
7769
7866
|
body: JSON.stringify({ publish_mode: mode })
|
|
7770
7867
|
}),
|
|
7868
|
+
/** A Google Business post's button (LEARN_MORE, BOOK, ORDER, SHOP, SIGN_UP, CALL); "" = Learn more. */
|
|
7869
|
+
setSocialButton: (id, ctaType) => api2.request(`${base}/social/${id}`, {
|
|
7870
|
+
method: "PATCH",
|
|
7871
|
+
headers: { "Content-Type": "application/json" },
|
|
7872
|
+
body: JSON.stringify({ cta_type: ctaType })
|
|
7873
|
+
}),
|
|
7771
7874
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7772
7875
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7773
7876
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
@@ -7775,6 +7878,9 @@ function domainApi(api2, domain) {
|
|
|
7775
7878
|
visibility: () => api2.request(`${base}/visibility`),
|
|
7776
7879
|
searchConsole: (period2 = "30d") => api2.request(`${base}/search-console?period=${period2}`),
|
|
7777
7880
|
searchConsoleQueries: (period2 = "30d", filter = "top", limit = 20) => api2.request(`${base}/search-console/queries?period=${period2}&filter=${filter}&limit=${limit}`),
|
|
7881
|
+
/** A Google sign-in link that connects Search Console for this website (open it in a
|
|
7882
|
+
* browser; back at returnTo with ?gsc=connected or ?gsc=error&message=…). 10 minutes. */
|
|
7883
|
+
connectSearchConsole: (returnTo = "close") => post(`${base}/search-console/connect`, { return_to: returnTo }),
|
|
7778
7884
|
/** Fresh numbers from Google now; at most once an hour per website. */
|
|
7779
7885
|
syncSearchConsole: () => post(`${base}/search-console/sync`),
|
|
7780
7886
|
contentPerformance: (sort = "clicks", band, limit = 20) => api2.request(
|
|
@@ -9719,7 +9825,7 @@ var brief = (m) => ({
|
|
|
9719
9825
|
var memoryTools = [
|
|
9720
9826
|
tool({
|
|
9721
9827
|
name: "memory_get",
|
|
9722
|
-
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.",
|
|
9828
|
+
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.",
|
|
9723
9829
|
parameters: none,
|
|
9724
9830
|
group: "memory",
|
|
9725
9831
|
need: own,
|
|
@@ -10017,9 +10123,21 @@ var reportTools = [
|
|
|
10017
10123
|
};
|
|
10018
10124
|
}
|
|
10019
10125
|
}),
|
|
10126
|
+
tool({
|
|
10127
|
+
name: "gsc_connect_link",
|
|
10128
|
+
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.",
|
|
10129
|
+
parameters: none,
|
|
10130
|
+
group: "reports",
|
|
10131
|
+
need: { resource: "autopilot", level: "write" },
|
|
10132
|
+
risk: "W1",
|
|
10133
|
+
run: async (ctx) => {
|
|
10134
|
+
const out = await ws(ctx).connectSearchConsole("close");
|
|
10135
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10136
|
+
}
|
|
10137
|
+
}),
|
|
10020
10138
|
tool({
|
|
10021
10139
|
name: "gsc_performance_get",
|
|
10022
|
-
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.",
|
|
10140
|
+
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.",
|
|
10023
10141
|
parameters: { type: "object", properties: { period: { type: "string", enum: periods } } },
|
|
10024
10142
|
group: "reports",
|
|
10025
10143
|
need: read4,
|
|
@@ -10186,7 +10304,7 @@ var reportTools = [
|
|
|
10186
10304
|
var read5 = { resource: "autopilot", level: "read" };
|
|
10187
10305
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10188
10306
|
var moves2 = ["up", "down", "top"];
|
|
10189
|
-
var CHANNEL_KEYS = ["linkedin", "facebook"];
|
|
10307
|
+
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10190
10308
|
var channelsParam = {
|
|
10191
10309
|
type: "array",
|
|
10192
10310
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
@@ -10196,6 +10314,13 @@ var channelsOf = (value) => {
|
|
|
10196
10314
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
10197
10315
|
return list2.length ? list2 : void 0;
|
|
10198
10316
|
};
|
|
10317
|
+
var writing = (job) => ({
|
|
10318
|
+
job_id: job.id,
|
|
10319
|
+
status: "writing",
|
|
10320
|
+
posts_expected: job.expected,
|
|
10321
|
+
channels: job.channels,
|
|
10322
|
+
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."
|
|
10323
|
+
});
|
|
10199
10324
|
var item = (i) => ({
|
|
10200
10325
|
id: i.id,
|
|
10201
10326
|
channel: i.provider || "linkedin",
|
|
@@ -10314,7 +10439,7 @@ var socialTools = [
|
|
|
10314
10439
|
}),
|
|
10315
10440
|
tool({
|
|
10316
10441
|
name: "social_image_new",
|
|
10317
|
-
description: "Make a new picture for a
|
|
10442
|
+
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.",
|
|
10318
10443
|
parameters: {
|
|
10319
10444
|
type: "object",
|
|
10320
10445
|
properties: {
|
|
@@ -10323,6 +10448,23 @@ var socialTools = [
|
|
|
10323
10448
|
instruction: {
|
|
10324
10449
|
type: "string",
|
|
10325
10450
|
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."
|
|
10451
|
+
},
|
|
10452
|
+
mode: {
|
|
10453
|
+
type: "string",
|
|
10454
|
+
enum: ["new", "edit"],
|
|
10455
|
+
description: "edit: change the post's current picture as the instruction says (it must have one)"
|
|
10456
|
+
},
|
|
10457
|
+
count: {
|
|
10458
|
+
type: "integer",
|
|
10459
|
+
description: "1 (default) or 4: a set of 4 pictures telling one story, a line of words on each"
|
|
10460
|
+
},
|
|
10461
|
+
text_effect: {
|
|
10462
|
+
type: "string",
|
|
10463
|
+
description: "For a set: the words' style id from social_text_effects (omit = Maggie picks)"
|
|
10464
|
+
},
|
|
10465
|
+
style: {
|
|
10466
|
+
type: "string",
|
|
10467
|
+
description: "Picture style id from social_look (omit = Maggie picks from the brand's 3)"
|
|
10326
10468
|
}
|
|
10327
10469
|
},
|
|
10328
10470
|
required: ["id"]
|
|
@@ -10331,36 +10473,253 @@ var socialTools = [
|
|
|
10331
10473
|
need: write5,
|
|
10332
10474
|
risk: "W2",
|
|
10333
10475
|
gates: ["VER", "CR"],
|
|
10334
|
-
run: async (ctx, { id, replace, instruction }) => {
|
|
10476
|
+
run: async (ctx, { id, replace, instruction, mode, count, text_effect, style }) => {
|
|
10335
10477
|
const out = await ws(ctx).regenerateSocialImage(Number(id), {
|
|
10336
10478
|
replace: replace === true,
|
|
10479
|
+
...typeof style === "string" && style.trim() && mode !== "edit" ? { style: style.trim() } : {},
|
|
10480
|
+
...mode === "edit" ? { mode: "edit" } : {},
|
|
10481
|
+
...Number(count) === 4 && mode !== "edit" ? { count: 4 } : {},
|
|
10482
|
+
...typeof text_effect === "string" && text_effect.trim() ? { textEffect: text_effect.trim() } : {},
|
|
10337
10483
|
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10338
10484
|
});
|
|
10339
10485
|
return {
|
|
10340
10486
|
id: out.id,
|
|
10341
10487
|
image_job: out.image_job,
|
|
10342
|
-
note: "Making it in the background; it shows on the post when ready."
|
|
10488
|
+
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."
|
|
10489
|
+
};
|
|
10490
|
+
}
|
|
10491
|
+
}),
|
|
10492
|
+
tool({
|
|
10493
|
+
name: "social_images_fill",
|
|
10494
|
+
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.",
|
|
10495
|
+
parameters: {
|
|
10496
|
+
type: "object",
|
|
10497
|
+
properties: {
|
|
10498
|
+
ids: { type: "array", items: { type: "integer" }, description: "Only these posts (from social_list)" },
|
|
10499
|
+
instruction: { type: "string", description: "What the user wants in every picture, up to 300 characters" }
|
|
10500
|
+
}
|
|
10501
|
+
},
|
|
10502
|
+
group: "social",
|
|
10503
|
+
need: write5,
|
|
10504
|
+
risk: "W2",
|
|
10505
|
+
gates: ["VER", "CR"],
|
|
10506
|
+
run: async (ctx, { ids: ids3, instruction }) => {
|
|
10507
|
+
const out = await ws(ctx).fillSocialImages({
|
|
10508
|
+
...Array.isArray(ids3) && ids3.length ? { item_ids: ids3.map(Number) } : {},
|
|
10509
|
+
...typeof instruction === "string" && instruction.trim() ? { instruction: instruction.trim().slice(0, 300) } : {}
|
|
10510
|
+
});
|
|
10511
|
+
return { queued: out.queued, already: out.skipped, stopped_because: out.message, line: out.image_jobs.length };
|
|
10512
|
+
}
|
|
10513
|
+
}),
|
|
10514
|
+
tool({
|
|
10515
|
+
name: "social_text_effects",
|
|
10516
|
+
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').",
|
|
10517
|
+
parameters: none,
|
|
10518
|
+
group: "social",
|
|
10519
|
+
need: read5,
|
|
10520
|
+
risk: "R0",
|
|
10521
|
+
run: async (ctx) => {
|
|
10522
|
+
const out = await ws(ctx).socialTextEffects();
|
|
10523
|
+
return { effects: out.effects.map(({ id, name, family }) => ({ id, name, family })), auto: out.auto };
|
|
10524
|
+
}
|
|
10525
|
+
}),
|
|
10526
|
+
tool({
|
|
10527
|
+
name: "social_look",
|
|
10528
|
+
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.",
|
|
10529
|
+
parameters: none,
|
|
10530
|
+
group: "social",
|
|
10531
|
+
need: read5,
|
|
10532
|
+
risk: "R0",
|
|
10533
|
+
run: async (ctx) => {
|
|
10534
|
+
const l = await ws(ctx).socialLook();
|
|
10535
|
+
return {
|
|
10536
|
+
styles: l.styles.map((s) => ({ id: s.id, name: s.name, about: s.about })),
|
|
10537
|
+
text_effects: l.text_effects.map((e) => ({ id: e.id, name: e.name, family: e.family })),
|
|
10538
|
+
fonts: l.fonts,
|
|
10539
|
+
colours: l.colours,
|
|
10540
|
+
description: l.brand_suffix,
|
|
10541
|
+
never: l.never,
|
|
10542
|
+
why: l.suggested.reason ?? null,
|
|
10543
|
+
can_choose: {
|
|
10544
|
+
styles: l.catalogue.styles.map((s) => ({ id: s.id, name: s.name, led: s.led, needs_photo: s.needs_photo })),
|
|
10545
|
+
fonts: l.catalogue.fonts,
|
|
10546
|
+
text_effect_families: [...new Set(l.catalogue.text_effects.map((e) => e.family))]
|
|
10547
|
+
}
|
|
10343
10548
|
};
|
|
10344
10549
|
}
|
|
10345
10550
|
}),
|
|
10551
|
+
tool({
|
|
10552
|
+
name: "social_look_update",
|
|
10553
|
+
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.",
|
|
10554
|
+
parameters: {
|
|
10555
|
+
type: "object",
|
|
10556
|
+
properties: {
|
|
10557
|
+
style_ids: { type: "array", items: { type: "string" }, description: "1-3 picture style ids" },
|
|
10558
|
+
text_effect_ids: { type: "array", items: { type: "string" }, description: "1-5 text style ids" },
|
|
10559
|
+
font_display: { type: "string", description: "Font for big words (from social_look can_choose.fonts)" },
|
|
10560
|
+
font_body: { type: "string", description: "Font for lines and lists" },
|
|
10561
|
+
colours: {
|
|
10562
|
+
type: "object",
|
|
10563
|
+
description: "Any of these as #RRGGBB",
|
|
10564
|
+
properties: {
|
|
10565
|
+
ink: { type: "string", description: "Text on paper" },
|
|
10566
|
+
paper: { type: "string", description: "The light ground" },
|
|
10567
|
+
primary: { type: "string", description: "The brand colour" },
|
|
10568
|
+
accent: { type: "string", description: "A second colour" },
|
|
10569
|
+
muted: { type: "string", description: "Small text" }
|
|
10570
|
+
}
|
|
10571
|
+
},
|
|
10572
|
+
description: { type: "string", description: "How the brand's photos look, up to 600 characters" },
|
|
10573
|
+
never: { type: "array", items: { type: "string" }, description: "Things never to show (the whole list)" },
|
|
10574
|
+
reset: { type: "boolean", description: "Back to Maggie's suggested look" }
|
|
10575
|
+
}
|
|
10576
|
+
},
|
|
10577
|
+
group: "social",
|
|
10578
|
+
need: write5,
|
|
10579
|
+
risk: "W1",
|
|
10580
|
+
gates: ["VER"],
|
|
10581
|
+
run: async (ctx, a) => {
|
|
10582
|
+
const fonts = {
|
|
10583
|
+
...a.font_display ? { display: a.font_display } : {},
|
|
10584
|
+
...a.font_body ? { body: a.font_body } : {}
|
|
10585
|
+
};
|
|
10586
|
+
const l = await ws(ctx).updateSocialLook({
|
|
10587
|
+
...a.style_ids ? { style_ids: a.style_ids } : {},
|
|
10588
|
+
...a.text_effect_ids ? { text_effect_ids: a.text_effect_ids } : {},
|
|
10589
|
+
...Object.keys(fonts).length ? { fonts } : {},
|
|
10590
|
+
...a.colours ? { colours: a.colours } : {},
|
|
10591
|
+
...typeof a.description === "string" ? { brand_suffix: a.description } : {},
|
|
10592
|
+
...a.never ? { never: a.never } : {},
|
|
10593
|
+
...a.reset ? { reset: true } : {}
|
|
10594
|
+
});
|
|
10595
|
+
return {
|
|
10596
|
+
styles: l.styles.map((s) => s.id),
|
|
10597
|
+
text_effects: l.text_effects.map((e) => e.id),
|
|
10598
|
+
fonts: l.fonts,
|
|
10599
|
+
colours: l.colours
|
|
10600
|
+
};
|
|
10601
|
+
}
|
|
10602
|
+
}),
|
|
10603
|
+
tool({
|
|
10604
|
+
name: "social_picture_edit",
|
|
10605
|
+
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.",
|
|
10606
|
+
parameters: {
|
|
10607
|
+
type: "object",
|
|
10608
|
+
properties: {
|
|
10609
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10610
|
+
words: {
|
|
10611
|
+
type: "array",
|
|
10612
|
+
items: {
|
|
10613
|
+
type: "object",
|
|
10614
|
+
properties: { headline: { type: "string" }, sub: { type: "string" } },
|
|
10615
|
+
required: ["headline"]
|
|
10616
|
+
},
|
|
10617
|
+
description: "The words on each picture, in order (in the new order when order is given)"
|
|
10618
|
+
},
|
|
10619
|
+
style: { type: "string", description: "A picture style id from social_look" },
|
|
10620
|
+
text_effect: { type: "string", description: "A text style id from social_text_effects" },
|
|
10621
|
+
order: { type: "array", items: { type: "integer" }, description: "The pictures in their new order" },
|
|
10622
|
+
remove: { type: "array", items: { type: "integer" }, description: "Pictures to drop from a set" }
|
|
10623
|
+
},
|
|
10624
|
+
required: ["id"]
|
|
10625
|
+
},
|
|
10626
|
+
group: "social",
|
|
10627
|
+
need: write5,
|
|
10628
|
+
risk: "W1",
|
|
10629
|
+
gates: ["VER"],
|
|
10630
|
+
run: async (ctx, { id, words, style, text_effect, order, remove: remove2 }) => {
|
|
10631
|
+
const out = await ws(ctx).editSocialPicture(Number(id), {
|
|
10632
|
+
...Array.isArray(words) ? { words: words.map((w) => ({ headline: String(w.headline ?? ""), sub: String(w.sub ?? "") })) } : {},
|
|
10633
|
+
...style ? { style } : {},
|
|
10634
|
+
...text_effect ? { text_effect } : {},
|
|
10635
|
+
...Array.isArray(order) ? { order: order.map(Number) } : {},
|
|
10636
|
+
...Array.isArray(remove2) ? { remove: remove2.map(Number) } : {}
|
|
10637
|
+
});
|
|
10638
|
+
return { id: out.id, pictures: out.image_urls?.length ? out.image_urls : [out.image_url], picture: out.picture };
|
|
10639
|
+
}
|
|
10640
|
+
}),
|
|
10641
|
+
tool({
|
|
10642
|
+
name: "social_photo_words",
|
|
10643
|
+
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.",
|
|
10644
|
+
parameters: {
|
|
10645
|
+
type: "object",
|
|
10646
|
+
properties: {
|
|
10647
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
10648
|
+
style: {
|
|
10649
|
+
type: "string",
|
|
10650
|
+
description: "real_photo (default), product_hero, photo_headline, editorial, duotone"
|
|
10651
|
+
}
|
|
10652
|
+
},
|
|
10653
|
+
required: ["id"]
|
|
10654
|
+
},
|
|
10655
|
+
group: "social",
|
|
10656
|
+
need: write5,
|
|
10657
|
+
risk: "W1",
|
|
10658
|
+
gates: ["VER"],
|
|
10659
|
+
run: async (ctx, { id, style }) => {
|
|
10660
|
+
const out = await ws(ctx).wordsOnPhoto(Number(id), style || "real_photo");
|
|
10661
|
+
return { id: out.id, picture: out.picture, pictures: out.image_urls?.length ? out.image_urls : [out.image_url] };
|
|
10662
|
+
}
|
|
10663
|
+
}),
|
|
10664
|
+
tool({
|
|
10665
|
+
name: "social_post_insights",
|
|
10666
|
+
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).",
|
|
10667
|
+
parameters: {
|
|
10668
|
+
type: "object",
|
|
10669
|
+
properties: {
|
|
10670
|
+
id: { type: "integer", description: "A posted Instagram post (from social_list posted)" },
|
|
10671
|
+
refresh: { type: "boolean", description: "Read the numbers again now" }
|
|
10672
|
+
},
|
|
10673
|
+
required: ["id"]
|
|
10674
|
+
},
|
|
10675
|
+
group: "social",
|
|
10676
|
+
need: read5,
|
|
10677
|
+
risk: "R0",
|
|
10678
|
+
run: async (ctx, { id, refresh }) => ws(ctx).socialInsights(Number(id), refresh === true)
|
|
10679
|
+
}),
|
|
10680
|
+
tool({
|
|
10681
|
+
name: "social_write_status",
|
|
10682
|
+
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.",
|
|
10683
|
+
parameters: none,
|
|
10684
|
+
group: "social",
|
|
10685
|
+
need: read5,
|
|
10686
|
+
risk: "R0",
|
|
10687
|
+
run: async (ctx) => ({
|
|
10688
|
+
jobs: ((await ws(ctx).social()).write_jobs ?? []).map((j) => ({
|
|
10689
|
+
id: j.id,
|
|
10690
|
+
kind: j.kind,
|
|
10691
|
+
status: j.status,
|
|
10692
|
+
channels: j.channels,
|
|
10693
|
+
expected: j.expected,
|
|
10694
|
+
written: j.item_ids,
|
|
10695
|
+
queued: j.queued,
|
|
10696
|
+
...j.failed?.length ? { failed: j.failed } : {},
|
|
10697
|
+
...j.error ? { error: j.error } : {},
|
|
10698
|
+
started_at: j.started_at
|
|
10699
|
+
}))
|
|
10700
|
+
})
|
|
10701
|
+
}),
|
|
10346
10702
|
tool({
|
|
10347
10703
|
name: "social_image_status",
|
|
10348
|
-
description: "
|
|
10704
|
+
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).",
|
|
10349
10705
|
parameters: none,
|
|
10350
10706
|
group: "social",
|
|
10351
10707
|
need: read5,
|
|
10352
10708
|
risk: "R0",
|
|
10353
|
-
run: async (ctx) =>
|
|
10709
|
+
run: async (ctx) => {
|
|
10710
|
+
const s = await ws(ctx).social();
|
|
10711
|
+
return { image_job: s.image_job ?? null, line: s.image_jobs ?? [] };
|
|
10712
|
+
}
|
|
10354
10713
|
}),
|
|
10355
10714
|
tool({
|
|
10356
10715
|
name: "social_image_stop",
|
|
10357
|
-
description: "Stop the
|
|
10358
|
-
parameters:
|
|
10716
|
+
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.",
|
|
10717
|
+
parameters: { type: "object", properties: { all: { type: "boolean" } } },
|
|
10359
10718
|
group: "social",
|
|
10360
10719
|
need: write5,
|
|
10361
10720
|
risk: "W1",
|
|
10362
10721
|
gates: ["VER"],
|
|
10363
|
-
run: async (ctx) => ws(ctx).stopSocialImage()
|
|
10722
|
+
run: async (ctx, { all }) => ws(ctx).stopSocialImage(all === true)
|
|
10364
10723
|
}),
|
|
10365
10724
|
tool({
|
|
10366
10725
|
name: "social_move",
|
|
@@ -10404,7 +10763,7 @@ var socialTools = [
|
|
|
10404
10763
|
}),
|
|
10405
10764
|
tool({
|
|
10406
10765
|
name: "social_from_article",
|
|
10407
|
-
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).",
|
|
10766
|
+
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).",
|
|
10408
10767
|
parameters: {
|
|
10409
10768
|
type: "object",
|
|
10410
10769
|
properties: {
|
|
@@ -10418,14 +10777,11 @@ var socialTools = [
|
|
|
10418
10777
|
need: write5,
|
|
10419
10778
|
risk: "W2",
|
|
10420
10779
|
gates: ["VER", "LI", "LLM"],
|
|
10421
|
-
run: async (ctx, { source_id, channels }) =>
|
|
10422
|
-
const out = await ws(ctx).socialFromPost(String(source_id), channelsOf(channels));
|
|
10423
|
-
return { id: out.id, status: out.status, queued: Boolean(out.queued), text: clip(out.social_copy, 300) };
|
|
10424
|
-
}
|
|
10780
|
+
run: async (ctx, { source_id, channels }) => writing((await ws(ctx).startSocialFromPost(String(source_id), channelsOf(channels))).job)
|
|
10425
10781
|
}),
|
|
10426
10782
|
tool({
|
|
10427
10783
|
name: "social_plan_days",
|
|
10428
|
-
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).",
|
|
10784
|
+
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).",
|
|
10429
10785
|
parameters: {
|
|
10430
10786
|
type: "object",
|
|
10431
10787
|
properties: {
|
|
@@ -10439,10 +10795,7 @@ var socialTools = [
|
|
|
10439
10795
|
need: write5,
|
|
10440
10796
|
risk: "W2",
|
|
10441
10797
|
gates: ["VER", "LI", "LLM"],
|
|
10442
|
-
run: async (ctx, { days, timezone, channels }) =>
|
|
10443
|
-
const out = await ws(ctx).socialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels));
|
|
10444
|
-
return { written: out.items.length, queued: Boolean(out.queued), items: out.items.map(item) };
|
|
10445
|
-
}
|
|
10798
|
+
run: async (ctx, { days, timezone, channels }) => writing((await ws(ctx).startSocialPlan(int(days, 1, 7, 3), timezone || "UTC", channelsOf(channels))).job)
|
|
10446
10799
|
}),
|
|
10447
10800
|
tool({
|
|
10448
10801
|
name: "social_ideas_list",
|
|
@@ -10515,7 +10868,7 @@ var socialTools = [
|
|
|
10515
10868
|
}),
|
|
10516
10869
|
tool({
|
|
10517
10870
|
name: "social_idea_write",
|
|
10518
|
-
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.",
|
|
10871
|
+
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).",
|
|
10519
10872
|
parameters: {
|
|
10520
10873
|
type: "object",
|
|
10521
10874
|
properties: {
|
|
@@ -10529,7 +10882,7 @@ var socialTools = [
|
|
|
10529
10882
|
need: write5,
|
|
10530
10883
|
risk: "W2",
|
|
10531
10884
|
gates: ["VER", "LI", "CR", "LLM"],
|
|
10532
|
-
run: async (ctx, { id, channels }) => ws(ctx).
|
|
10885
|
+
run: async (ctx, { id, channels }) => writing((await ws(ctx).startWriteSocialIdea(Number(id), channelsOf(channels))).job)
|
|
10533
10886
|
}),
|
|
10534
10887
|
tool({
|
|
10535
10888
|
name: "social_idea_brief",
|
|
@@ -10826,9 +11179,9 @@ var socialTools = [
|
|
|
10826
11179
|
run: async (ctx, { keys: keys3, ...options }) => {
|
|
10827
11180
|
const api2 = ws(ctx);
|
|
10828
11181
|
const current = (await api2.socialSetup()).writing;
|
|
10829
|
-
const
|
|
10830
|
-
for (const [k, v] of Object.entries(options)) if (v !== void 0)
|
|
10831
|
-
return api2.socialSetupTemplates((keys3 ?? []).slice(0, 10),
|
|
11182
|
+
const writing2 = { ...current };
|
|
11183
|
+
for (const [k, v] of Object.entries(options)) if (v !== void 0) writing2[k] = v;
|
|
11184
|
+
return api2.socialSetupTemplates((keys3 ?? []).slice(0, 10), writing2);
|
|
10832
11185
|
}
|
|
10833
11186
|
}),
|
|
10834
11187
|
tool({
|
|
@@ -10909,6 +11262,52 @@ var socialTools = [
|
|
|
10909
11262
|
const out = await ws(ctx).connectFacebook("close");
|
|
10910
11263
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
10911
11264
|
}
|
|
11265
|
+
}),
|
|
11266
|
+
tool({
|
|
11267
|
+
name: "google_business_connect_link",
|
|
11268
|
+
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.",
|
|
11269
|
+
parameters: none,
|
|
11270
|
+
group: "social",
|
|
11271
|
+
need: write5,
|
|
11272
|
+
risk: "W1",
|
|
11273
|
+
gates: ["VER"],
|
|
11274
|
+
run: async (ctx) => {
|
|
11275
|
+
const out = await ws(ctx).connectGoogleBusiness("close");
|
|
11276
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11277
|
+
}
|
|
11278
|
+
}),
|
|
11279
|
+
tool({
|
|
11280
|
+
name: "social_google_button",
|
|
11281
|
+
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.",
|
|
11282
|
+
parameters: {
|
|
11283
|
+
type: "object",
|
|
11284
|
+
properties: {
|
|
11285
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11286
|
+
button: { type: "string", enum: ["LEARN_MORE", "BOOK", "ORDER", "SHOP", "SIGN_UP", "CALL"] }
|
|
11287
|
+
},
|
|
11288
|
+
required: ["id", "button"]
|
|
11289
|
+
},
|
|
11290
|
+
group: "social",
|
|
11291
|
+
need: write5,
|
|
11292
|
+
risk: "W1",
|
|
11293
|
+
gates: ["VER"],
|
|
11294
|
+
run: async (ctx, { id, button }) => {
|
|
11295
|
+
const out = await ws(ctx).setSocialButton(Number(id), button);
|
|
11296
|
+
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11297
|
+
}
|
|
11298
|
+
}),
|
|
11299
|
+
tool({
|
|
11300
|
+
name: "instagram_connect_link",
|
|
11301
|
+
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).",
|
|
11302
|
+
parameters: none,
|
|
11303
|
+
group: "social",
|
|
11304
|
+
need: write5,
|
|
11305
|
+
risk: "W1",
|
|
11306
|
+
gates: ["VER"],
|
|
11307
|
+
run: async (ctx) => {
|
|
11308
|
+
const out = await ws(ctx).connectInstagram("close");
|
|
11309
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11310
|
+
}
|
|
10912
11311
|
})
|
|
10913
11312
|
];
|
|
10914
11313
|
|
|
@@ -14454,7 +14853,7 @@ var program = new Command();
|
|
|
14454
14853
|
// package.json
|
|
14455
14854
|
var package_default = {
|
|
14456
14855
|
name: "@cmo-so/cli",
|
|
14457
|
-
version: "0.
|
|
14856
|
+
version: "0.3.0",
|
|
14458
14857
|
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.",
|
|
14459
14858
|
type: "module",
|
|
14460
14859
|
license: "MIT",
|
|
@@ -22596,9 +22995,10 @@ posts.command("ls", { isDefault: true }).description("planned, draft or publishe
|
|
|
22596
22995
|
posts.command("show <id>").description("one post in full").action(act(async (opts, id) => tool2(opts, "post_get", { id })));
|
|
22597
22996
|
posts.command("publish <id>").description("publish a draft now (uses a post credit)").action(act(async (opts, id) => tool2(opts, "post_publish_now", { id })));
|
|
22598
22997
|
posts.command("delete <id>").description("delete a draft").action(act(async (opts, id) => tool2(opts, "draft_delete", { id })));
|
|
22599
|
-
var social = program2.command("social").description("your social posts: LinkedIn and your
|
|
22998
|
+
var social = program2.command("social").description("your social posts: LinkedIn, your Facebook Page, Instagram and your Google Business Profile");
|
|
22999
|
+
var CHANNELS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
22600
23000
|
var channelsOpt = (value) => {
|
|
22601
|
-
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).
|
|
23001
|
+
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map((v) => v === "google" || v === "gbp" ? "google_business" : v).filter((v) => CHANNELS.includes(v));
|
|
22602
23002
|
return list2.length ? list2 : void 0;
|
|
22603
23003
|
};
|
|
22604
23004
|
social.command("ls", { isDefault: true }).description("drafts to review and queued posts").action(
|
|
@@ -22606,7 +23006,7 @@ social.command("ls", { isDefault: true }).description("drafts to review and queu
|
|
|
22606
23006
|
async (opts) => tool2(opts, "social_list", {}, (v) => {
|
|
22607
23007
|
const s = v;
|
|
22608
23008
|
const live = (s.channels ?? []).filter((c) => c.connected);
|
|
22609
|
-
const head = !s.connected ? "No social channel is connected (LinkedIn or a
|
|
23009
|
+
const head = !s.connected ? "No social channel is connected (LinkedIn, a Facebook Page, Instagram or a Google Business Profile)." : s.posting_paused ? "Posting is paused on every channel." : live.length ? live.map((c) => `${c.channel}: ${c.paused ? "paused" : "on"}${c.account ? ` (${c.account})` : ""}`).join(" \xB7 ") : "Posting is on.";
|
|
22610
23010
|
return `${head}
|
|
22611
23011
|
${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
|
|
22612
23012
|
})
|
|
@@ -22615,7 +23015,7 @@ ${table(s.queue ?? [], ["id", "channel", "status", "eta", "text"])}`;
|
|
|
22615
23015
|
social.command("approve <ids...>").description("approve drafts: they go out at the next posting times").action(act(async (opts, list2) => tool2(opts, "social_approve", { ids: ids2(list2) })));
|
|
22616
23016
|
social.command("skip <id>").description("skip a draft").action(act(async (opts, id) => tool2(opts, "social_skip", { id: ids2([id])[0] })));
|
|
22617
23017
|
social.command("restore <id>").description("bring a skipped draft back").action(act(async (opts, id) => tool2(opts, "social_restore", { id: ids2([id])[0] })));
|
|
22618
|
-
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
|
|
23018
|
+
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(
|
|
22619
23019
|
act(
|
|
22620
23020
|
async (opts, topic) => tool2(opts, "social_idea_add", {
|
|
22621
23021
|
title: topic.join(" "),
|
|
@@ -22628,7 +23028,7 @@ for (const [cmd, paused] of [
|
|
|
22628
23028
|
["pause", true],
|
|
22629
23029
|
["resume", false]
|
|
22630
23030
|
])
|
|
22631
|
-
social.command(cmd).description(`${cmd} posting (every channel, or --channel linkedin | facebook)`).option("--channel <name>", "only this channel").action(
|
|
23031
|
+
social.command(cmd).description(`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business)`).option("--channel <name>", "only this channel").action(
|
|
22632
23032
|
act(
|
|
22633
23033
|
async (opts) => tool2(opts, "social_set_paused", {
|
|
22634
23034
|
paused,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|