@cmo-so/mcp 0.3.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 +246 -5
- 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",
|
|
@@ -7763,7 +7771,24 @@ function domainApi(api2, domain) {
|
|
|
7763
7771
|
`${base}/social/google_business/location`,
|
|
7764
7772
|
{ connection_id: connectionId }
|
|
7765
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" : ""}`),
|
|
7766
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" }),
|
|
7767
7792
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7768
7793
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7769
7794
|
/** How often one channel posts. */
|
|
@@ -7928,7 +7953,34 @@ function domainApi(api2, domain) {
|
|
|
7928
7953
|
headers: { "Content-Type": "application/json" },
|
|
7929
7954
|
body: JSON.stringify({ cta_type: ctaType })
|
|
7930
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
|
+
}),
|
|
7931
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
|
+
}),
|
|
7932
7984
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7933
7985
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
7934
7986
|
// Reports (docs/18 §6): report:read with the user's own key.
|
|
@@ -8349,6 +8401,22 @@ var confirmCopy = {
|
|
|
8349
8401
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
8350
8402
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
8351
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
|
+
},
|
|
8352
8420
|
social_image_new: {
|
|
8353
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" : ""}.`,
|
|
8354
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`
|
|
@@ -10362,10 +10430,11 @@ var read5 = { resource: "autopilot", level: "read" };
|
|
|
10362
10430
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10363
10431
|
var moves2 = ["up", "down", "top"];
|
|
10364
10432
|
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10433
|
+
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube"];
|
|
10365
10434
|
var channelsParam = {
|
|
10366
10435
|
type: "array",
|
|
10367
10436
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10368
|
-
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."
|
|
10369
10438
|
};
|
|
10370
10439
|
var channelsOf = (value) => {
|
|
10371
10440
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -10388,7 +10457,11 @@ var item = (i) => ({
|
|
|
10388
10457
|
campaign: i.campaign ?? null,
|
|
10389
10458
|
failure: i.failure_message ?? null,
|
|
10390
10459
|
link: i.provider_permalink ?? null,
|
|
10391
|
-
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
|
+
} : {}
|
|
10392
10465
|
});
|
|
10393
10466
|
var socialTools = [
|
|
10394
10467
|
tool({
|
|
@@ -10797,7 +10870,7 @@ var socialTools = [
|
|
|
10797
10870
|
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10798
10871
|
parameters: {
|
|
10799
10872
|
type: "object",
|
|
10800
|
-
properties: { paused: { type: "boolean" }, channel: { type: "string", enum:
|
|
10873
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: ALL_CHANNEL_KEYS } },
|
|
10801
10874
|
required: ["paused"]
|
|
10802
10875
|
},
|
|
10803
10876
|
group: "social",
|
|
@@ -11353,6 +11426,174 @@ var socialTools = [
|
|
|
11353
11426
|
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11354
11427
|
}
|
|
11355
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
|
+
}),
|
|
11356
11597
|
tool({
|
|
11357
11598
|
name: "instagram_connect_link",
|
|
11358
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).",
|
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",
|