@cmo-so/cli 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.mjs +281 -9
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7473,7 +7473,15 @@ function dataUrl(type, bytes) {
|
|
|
7473
7473
|
}
|
|
7474
7474
|
|
|
7475
7475
|
// ../../packages/api-client/src/domains.ts
|
|
7476
|
-
var MEMORY_CHANNELS = [
|
|
7476
|
+
var MEMORY_CHANNELS = [
|
|
7477
|
+
"general",
|
|
7478
|
+
"blog",
|
|
7479
|
+
"linkedin",
|
|
7480
|
+
"facebook",
|
|
7481
|
+
"instagram",
|
|
7482
|
+
"google_business",
|
|
7483
|
+
"youtube"
|
|
7484
|
+
];
|
|
7477
7485
|
function addWebsite(api2, url) {
|
|
7478
7486
|
return api2.request("/v1/domains", {
|
|
7479
7487
|
method: "POST",
|
|
@@ -7706,7 +7714,24 @@ function domainApi(api2, domain) {
|
|
|
7706
7714
|
`${base}/social/google_business/location`,
|
|
7707
7715
|
{ connection_id: connectionId }
|
|
7708
7716
|
),
|
|
7717
|
+
/** How the Google Business Profile is doing: the last 28 days and the 28 before (Google lags 2-3 days). */
|
|
7718
|
+
googleBusinessPerformance: (refresh = false) => api2.request(`${base}/social/google_business/performance${refresh ? "?refresh=true" : ""}`),
|
|
7709
7719
|
disconnectGoogleBusiness: (connectionId) => api2.request(`${base}/social/google_business/${connectionId}`, { method: "DELETE" }),
|
|
7720
|
+
/** A Google sign-in link for this website's YouTube channel (docs/prd/youtube.md §7); back with ?youtube=connected
|
|
7721
|
+
* (or none when the Google account has no channel, error&message=…). */
|
|
7722
|
+
connectYouTube: (returnTo = "close") => post(`${base}/social/youtube/connect`, { return_to: returnTo }),
|
|
7723
|
+
/** Your YouTube channels here (name, @handle, picture, subscribers), and the one this website posts to. */
|
|
7724
|
+
youtubeChannels: () => api2.request(
|
|
7725
|
+
`${base}/social/youtube/channels`
|
|
7726
|
+
),
|
|
7727
|
+
chooseYouTubeChannel: (connectionId) => put(
|
|
7728
|
+
`${base}/social/youtube/channel`,
|
|
7729
|
+
{
|
|
7730
|
+
connection_id: connectionId
|
|
7731
|
+
}
|
|
7732
|
+
),
|
|
7733
|
+
/** Disconnect: the token is deleted and Google's access revoked (YouTube's policy). */
|
|
7734
|
+
disconnectYouTube: (connectionId) => api2.request(`${base}/social/youtube/${connectionId}`, { method: "DELETE" }),
|
|
7710
7735
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7711
7736
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7712
7737
|
/** How often one channel posts. */
|
|
@@ -7871,7 +7896,34 @@ function domainApi(api2, domain) {
|
|
|
7871
7896
|
headers: { "Content-Type": "application/json" },
|
|
7872
7897
|
body: JSON.stringify({ cta_type: ctaType })
|
|
7873
7898
|
}),
|
|
7899
|
+
/** Make a Google Business post an Event or Offer (or back to an update with { type: "STANDARD" }). */
|
|
7900
|
+
setSocialGooglePost: (id, googlePost) => api2.request(`${base}/social/${id}`, {
|
|
7901
|
+
method: "PATCH",
|
|
7902
|
+
headers: { "Content-Type": "application/json" },
|
|
7903
|
+
body: JSON.stringify({ google_post: googlePost })
|
|
7904
|
+
}),
|
|
7874
7905
|
resumeSocial: () => post(`${base}/social/resume`),
|
|
7906
|
+
/** Start a video upload: PUT each part_size slice of the file to its url, keep the ETags, then completeVideoUpload. */
|
|
7907
|
+
startVideoUpload: (file) => post(
|
|
7908
|
+
`${base}/social/video/upload`,
|
|
7909
|
+
file
|
|
7910
|
+
),
|
|
7911
|
+
completeVideoUpload: (videoId, parts, edit) => post(`${base}/social/video/${videoId}/complete`, { parts, edit }),
|
|
7912
|
+
/** A direct link to a video file (not a YouTube / TikTok page). */
|
|
7913
|
+
videoFromLink: (url, edit) => post(`${base}/social/video/link`, { url, edit }),
|
|
7914
|
+
video: (videoId) => api2.request(`${base}/social/video/${videoId}`),
|
|
7915
|
+
/** Another part or framing; prepared again in the background. */
|
|
7916
|
+
editVideo: (videoId, edit) => post(`${base}/social/video/${videoId}/edit`, { edit }),
|
|
7917
|
+
/** A YouTube post from a ready video, waiting in Review; Maggie writes the details unless write is false. */
|
|
7918
|
+
postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
|
|
7919
|
+
youtubeSettings: () => api2.request(`${base}/social/youtube/settings`),
|
|
7920
|
+
setYouTubeSettings: (s) => put(`${base}/social/youtube/settings`, s),
|
|
7921
|
+
/** A YouTube post's title, tags, visibility, made-for-kids or altered-content answer. */
|
|
7922
|
+
setSocialYouTube: (id, answers) => api2.request(`${base}/social/${id}`, {
|
|
7923
|
+
method: "PATCH",
|
|
7924
|
+
headers: { "Content-Type": "application/json" },
|
|
7925
|
+
body: JSON.stringify({ youtube: answers })
|
|
7926
|
+
}),
|
|
7875
7927
|
pauseAutopilot: () => post(`${base}/autopilot/pause`),
|
|
7876
7928
|
resumeAutopilot: () => post(`${base}/autopilot/resume`),
|
|
7877
7929
|
// Reports (docs/18 §6): report:read with the user's own key.
|
|
@@ -8292,6 +8344,22 @@ var confirmCopy = {
|
|
|
8292
8344
|
en: (a) => `Bring back LinkedIn post #${a.id}.`,
|
|
8293
8345
|
"zh-Hant": (a) => `\u6062\u5FA9 LinkedIn \u8CBC\u6587 #${a.id}\u3002`
|
|
8294
8346
|
},
|
|
8347
|
+
youtube_connect_link: {
|
|
8348
|
+
en: () => "Make a link to connect your YouTube channel.",
|
|
8349
|
+
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8350
|
+
},
|
|
8351
|
+
social_video_add: {
|
|
8352
|
+
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.`,
|
|
8353
|
+
"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`
|
|
8354
|
+
},
|
|
8355
|
+
social_youtube_set: {
|
|
8356
|
+
en: (a) => `Change YouTube post #${a.id}: ${keys(a).filter((k) => k !== "id").join(", ") || "its details"}.`,
|
|
8357
|
+
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u8CBC\u6587 #${a.id}\uFF1A${keys(a).filter((k) => k !== "id").join("\u3001") || "\u7D30\u7BC0"}\u3002`
|
|
8358
|
+
},
|
|
8359
|
+
youtube_settings_set: {
|
|
8360
|
+
en: (a) => `Change your YouTube defaults: ${keys(a).join(", ") || "nothing"}.`,
|
|
8361
|
+
"zh-Hant": (a) => `\u4FEE\u6539 YouTube \u9810\u8A2D\uFF1A${keys(a).join("\u3001") || "\u7121"}\u3002`
|
|
8362
|
+
},
|
|
8295
8363
|
social_image_new: {
|
|
8296
8364
|
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" : ""}.`,
|
|
8297
8365
|
"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`
|
|
@@ -10305,10 +10373,11 @@ var read5 = { resource: "autopilot", level: "read" };
|
|
|
10305
10373
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10306
10374
|
var moves2 = ["up", "down", "top"];
|
|
10307
10375
|
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10376
|
+
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube"];
|
|
10308
10377
|
var channelsParam = {
|
|
10309
10378
|
type: "array",
|
|
10310
10379
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
10311
|
-
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."
|
|
10380
|
+
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."
|
|
10312
10381
|
};
|
|
10313
10382
|
var channelsOf = (value) => {
|
|
10314
10383
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -10331,7 +10400,11 @@ var item = (i) => ({
|
|
|
10331
10400
|
campaign: i.campaign ?? null,
|
|
10332
10401
|
failure: i.failure_message ?? null,
|
|
10333
10402
|
link: i.provider_permalink ?? null,
|
|
10334
|
-
text: clip(i.social_copy, 300)
|
|
10403
|
+
text: clip(i.social_copy, 300),
|
|
10404
|
+
...i.provider === "youtube" ? {
|
|
10405
|
+
youtube: i.youtube ?? null,
|
|
10406
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10407
|
+
} : {}
|
|
10335
10408
|
});
|
|
10336
10409
|
var socialTools = [
|
|
10337
10410
|
tool({
|
|
@@ -10740,7 +10813,7 @@ var socialTools = [
|
|
|
10740
10813
|
description: "Pause or resume the user's social posting: one channel (linkedin or facebook), or every channel when no channel is given.",
|
|
10741
10814
|
parameters: {
|
|
10742
10815
|
type: "object",
|
|
10743
|
-
properties: { paused: { type: "boolean" }, channel: { type: "string", enum:
|
|
10816
|
+
properties: { paused: { type: "boolean" }, channel: { type: "string", enum: ALL_CHANNEL_KEYS } },
|
|
10744
10817
|
required: ["paused"]
|
|
10745
10818
|
},
|
|
10746
10819
|
group: "social",
|
|
@@ -11296,6 +11369,174 @@ var socialTools = [
|
|
|
11296
11369
|
return { id: out.id, button: out.cta_type ?? "LEARN_MORE", link: out.cta_url ?? null };
|
|
11297
11370
|
}
|
|
11298
11371
|
}),
|
|
11372
|
+
tool({
|
|
11373
|
+
name: "youtube_connect_link",
|
|
11374
|
+
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.",
|
|
11375
|
+
parameters: none,
|
|
11376
|
+
group: "social",
|
|
11377
|
+
need: write5,
|
|
11378
|
+
risk: "W1",
|
|
11379
|
+
gates: ["VER"],
|
|
11380
|
+
run: async (ctx) => {
|
|
11381
|
+
const out = await ws(ctx).connectYouTube("close");
|
|
11382
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11383
|
+
}
|
|
11384
|
+
}),
|
|
11385
|
+
tool({
|
|
11386
|
+
name: "social_video_add",
|
|
11387
|
+
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.",
|
|
11388
|
+
parameters: {
|
|
11389
|
+
type: "object",
|
|
11390
|
+
properties: {
|
|
11391
|
+
url: { type: "string", description: "Direct https link to the video file" },
|
|
11392
|
+
note: { type: "string", description: "What the video is about, in the user's words" },
|
|
11393
|
+
write: { type: "boolean", description: "Let Maggie write the details (default true)" },
|
|
11394
|
+
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
11395
|
+
end: { type: "number", description: "End of the part to use, seconds" },
|
|
11396
|
+
vertical: { type: "string", enum: ["left", "middle", "right"] }
|
|
11397
|
+
},
|
|
11398
|
+
required: ["url"]
|
|
11399
|
+
},
|
|
11400
|
+
group: "social",
|
|
11401
|
+
need: write5,
|
|
11402
|
+
risk: "W2",
|
|
11403
|
+
gates: ["VER", "LLM"],
|
|
11404
|
+
run: async (ctx, a) => {
|
|
11405
|
+
const d = ws(ctx);
|
|
11406
|
+
const edit = {
|
|
11407
|
+
...a.start != null ? { start: Number(a.start) } : {},
|
|
11408
|
+
...a.end != null ? { end: Number(a.end) } : {},
|
|
11409
|
+
...a.vertical ? { aspect: "9:16", crop_x: a.vertical === "left" ? 0 : a.vertical === "right" ? 1 : 0.5 } : {}
|
|
11410
|
+
};
|
|
11411
|
+
let v = await d.videoFromLink(String(a.url), Object.keys(edit).length ? edit : void 0);
|
|
11412
|
+
const until = Date.now() + 15e4;
|
|
11413
|
+
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
11414
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
11415
|
+
v = await d.video(v.id);
|
|
11416
|
+
}
|
|
11417
|
+
if (v.status !== "ready")
|
|
11418
|
+
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11419
|
+
const post = await d.postFromVideo(v.id, { note: a.note ?? "", write: a.write !== false });
|
|
11420
|
+
return {
|
|
11421
|
+
ok: true,
|
|
11422
|
+
post: item(post),
|
|
11423
|
+
next: "Show the title and description; ask whether it's made for kids and which visibility, then social_youtube_set; the user approves it."
|
|
11424
|
+
};
|
|
11425
|
+
}
|
|
11426
|
+
}),
|
|
11427
|
+
tool({
|
|
11428
|
+
name: "social_youtube_set",
|
|
11429
|
+
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.",
|
|
11430
|
+
parameters: {
|
|
11431
|
+
type: "object",
|
|
11432
|
+
properties: {
|
|
11433
|
+
id: { type: "integer", description: "A YouTube post (from social_list)" },
|
|
11434
|
+
title: { type: "string" },
|
|
11435
|
+
tags: { type: "array", items: { type: "string" } },
|
|
11436
|
+
privacy: { type: "string", enum: ["public", "unlisted", "private"] },
|
|
11437
|
+
made_for_kids: { type: "boolean" },
|
|
11438
|
+
synthetic: { type: "boolean" }
|
|
11439
|
+
},
|
|
11440
|
+
required: ["id"]
|
|
11441
|
+
},
|
|
11442
|
+
group: "social",
|
|
11443
|
+
need: write5,
|
|
11444
|
+
risk: "W1",
|
|
11445
|
+
gates: ["VER"],
|
|
11446
|
+
run: async (ctx, { id, ...rest }) => {
|
|
11447
|
+
const answers = Object.fromEntries(
|
|
11448
|
+
Object.entries(rest).filter(([, v]) => v !== void 0)
|
|
11449
|
+
);
|
|
11450
|
+
const out = await ws(ctx).setSocialYouTube(Number(id), answers);
|
|
11451
|
+
return item(out);
|
|
11452
|
+
}
|
|
11453
|
+
}),
|
|
11454
|
+
tool({
|
|
11455
|
+
name: "youtube_settings",
|
|
11456
|
+
description: "The user's YouTube defaults for new posts on this website: visibility, made_for_kids (null = asked each time), category and altered content.",
|
|
11457
|
+
parameters: none,
|
|
11458
|
+
group: "social",
|
|
11459
|
+
need: read5,
|
|
11460
|
+
risk: "R0",
|
|
11461
|
+
run: async (ctx) => ws(ctx).youtubeSettings()
|
|
11462
|
+
}),
|
|
11463
|
+
tool({
|
|
11464
|
+
name: "youtube_settings_set",
|
|
11465
|
+
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.",
|
|
11466
|
+
parameters: {
|
|
11467
|
+
type: "object",
|
|
11468
|
+
properties: {
|
|
11469
|
+
privacy: { type: "string", enum: ["public", "unlisted", "private"] },
|
|
11470
|
+
made_for_kids: {
|
|
11471
|
+
type: "string",
|
|
11472
|
+
enum: ["yes", "no", "ask"],
|
|
11473
|
+
description: "yes / no for every new video, or ask (answer per video)"
|
|
11474
|
+
},
|
|
11475
|
+
synthetic: { type: "boolean" }
|
|
11476
|
+
}
|
|
11477
|
+
},
|
|
11478
|
+
group: "social",
|
|
11479
|
+
need: write5,
|
|
11480
|
+
risk: "W1",
|
|
11481
|
+
gates: ["VER"],
|
|
11482
|
+
run: async (ctx, a) => ws(ctx).setYouTubeSettings({
|
|
11483
|
+
...a.privacy ? { privacy: a.privacy } : {},
|
|
11484
|
+
...a.made_for_kids ? { made_for_kids: a.made_for_kids === "ask" ? null : a.made_for_kids === "yes" } : {},
|
|
11485
|
+
...a.synthetic != null ? { synthetic: a.synthetic } : {}
|
|
11486
|
+
})
|
|
11487
|
+
}),
|
|
11488
|
+
tool({
|
|
11489
|
+
name: "google_business_performance",
|
|
11490
|
+
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.",
|
|
11491
|
+
parameters: {
|
|
11492
|
+
type: "object",
|
|
11493
|
+
properties: { refresh: { type: "boolean", description: "Read Google again now (else up to 6 hours old)" } }
|
|
11494
|
+
},
|
|
11495
|
+
group: "social",
|
|
11496
|
+
need: read5,
|
|
11497
|
+
risk: "R0",
|
|
11498
|
+
run: async (ctx, { refresh }) => ws(ctx).googleBusinessPerformance(refresh === true)
|
|
11499
|
+
}),
|
|
11500
|
+
tool({
|
|
11501
|
+
name: "social_google_post_type",
|
|
11502
|
+
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.",
|
|
11503
|
+
parameters: {
|
|
11504
|
+
type: "object",
|
|
11505
|
+
properties: {
|
|
11506
|
+
id: { type: "integer", description: "A Google Business post (from social_list)" },
|
|
11507
|
+
type: { type: "string", enum: ["STANDARD", "EVENT", "OFFER"] },
|
|
11508
|
+
title: { type: "string", description: "Event or offer title, up to 58 characters" },
|
|
11509
|
+
start: { type: "string", description: "YYYY-MM-DD" },
|
|
11510
|
+
end: { type: "string", description: "YYYY-MM-DD (defaults to start)" },
|
|
11511
|
+
start_time: { type: "string", description: "HH:MM (optional)" },
|
|
11512
|
+
end_time: { type: "string", description: "HH:MM (optional)" },
|
|
11513
|
+
coupon: { type: "string", description: "Offer code (optional)" },
|
|
11514
|
+
terms: { type: "string", description: "Offer terms (optional)" },
|
|
11515
|
+
redeem_url: { type: "string", description: "Where to redeem the offer online (optional)" }
|
|
11516
|
+
},
|
|
11517
|
+
required: ["id", "type"]
|
|
11518
|
+
},
|
|
11519
|
+
group: "social",
|
|
11520
|
+
need: write5,
|
|
11521
|
+
risk: "W1",
|
|
11522
|
+
gates: ["VER"],
|
|
11523
|
+
run: async (ctx, a) => {
|
|
11524
|
+
const type = String(a.type).toUpperCase();
|
|
11525
|
+
const body = type === "STANDARD" ? { type: "STANDARD" } : {
|
|
11526
|
+
type,
|
|
11527
|
+
title: String(a.title ?? ""),
|
|
11528
|
+
start: String(a.start ?? ""),
|
|
11529
|
+
...a.end ? { end: a.end } : {},
|
|
11530
|
+
...a.start_time ? { start_time: a.start_time } : {},
|
|
11531
|
+
...a.end_time ? { end_time: a.end_time } : {},
|
|
11532
|
+
...a.coupon ? { coupon: a.coupon } : {},
|
|
11533
|
+
...a.terms ? { terms: a.terms } : {},
|
|
11534
|
+
...a.redeem_url ? { redeem_url: a.redeem_url } : {}
|
|
11535
|
+
};
|
|
11536
|
+
const out = await ws(ctx).setSocialGooglePost(Number(a.id), body);
|
|
11537
|
+
return { id: out.id, google_post: out.google_post ?? null };
|
|
11538
|
+
}
|
|
11539
|
+
}),
|
|
11299
11540
|
tool({
|
|
11300
11541
|
name: "instagram_connect_link",
|
|
11301
11542
|
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).",
|
|
@@ -14853,7 +15094,7 @@ var program = new Command();
|
|
|
14853
15094
|
// package.json
|
|
14854
15095
|
var package_default = {
|
|
14855
15096
|
name: "@cmo-so/cli",
|
|
14856
|
-
version: "0.
|
|
15097
|
+
version: "0.4.0",
|
|
14857
15098
|
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.",
|
|
14858
15099
|
type: "module",
|
|
14859
15100
|
license: "MIT",
|
|
@@ -22995,10 +23236,10 @@ posts.command("ls", { isDefault: true }).description("planned, draft or publishe
|
|
|
22995
23236
|
posts.command("show <id>").description("one post in full").action(act(async (opts, id) => tool2(opts, "post_get", { id })));
|
|
22996
23237
|
posts.command("publish <id>").description("publish a draft now (uses a post credit)").action(act(async (opts, id) => tool2(opts, "post_publish_now", { id })));
|
|
22997
23238
|
posts.command("delete <id>").description("delete a draft").action(act(async (opts, id) => tool2(opts, "draft_delete", { id })));
|
|
22998
|
-
var social = program2.command("social").description("your social posts: LinkedIn, your Facebook Page, Instagram
|
|
22999
|
-
var CHANNELS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
23239
|
+
var social = program2.command("social").description("your social posts: LinkedIn, your Facebook Page, Instagram, your Google Business Profile and YouTube");
|
|
23240
|
+
var CHANNELS = ["linkedin", "facebook", "instagram", "google_business", "youtube"];
|
|
23000
23241
|
var channelsOpt = (value) => {
|
|
23001
|
-
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map((v) => v === "google" || v === "gbp" ? "google_business" : v).filter((v) => CHANNELS.includes(v));
|
|
23242
|
+
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map((v) => v === "google" || v === "gbp" ? "google_business" : v === "yt" || v === "shorts" ? "youtube" : v).filter((v) => CHANNELS.includes(v));
|
|
23002
23243
|
return list2.length ? list2 : void 0;
|
|
23003
23244
|
};
|
|
23004
23245
|
social.command("ls", { isDefault: true }).description("drafts to review and queued posts").action(
|
|
@@ -23028,7 +23269,9 @@ for (const [cmd, paused] of [
|
|
|
23028
23269
|
["pause", true],
|
|
23029
23270
|
["resume", false]
|
|
23030
23271
|
])
|
|
23031
|
-
social.command(cmd).description(
|
|
23272
|
+
social.command(cmd).description(
|
|
23273
|
+
`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business | youtube)`
|
|
23274
|
+
).option("--channel <name>", "only this channel").action(
|
|
23032
23275
|
act(
|
|
23033
23276
|
async (opts) => tool2(opts, "social_set_paused", {
|
|
23034
23277
|
paused,
|
|
@@ -23036,6 +23279,35 @@ for (const [cmd, paused] of [
|
|
|
23036
23279
|
})
|
|
23037
23280
|
)
|
|
23038
23281
|
);
|
|
23282
|
+
var youtube = social.command("youtube").description("your YouTube channel: short videos you approve one by one");
|
|
23283
|
+
youtube.command("connect").description("a link to connect your YouTube channel").action(act(async (opts) => tool2(opts, "youtube_connect_link")));
|
|
23284
|
+
youtube.command("add <url>").description("a YouTube post from a direct link to a video file (5-30 s): Maggie writes the title and description").option("--note <text>", "what the video is about").option("--start <seconds>", "use the part from here").option("--end <seconds>", "use the part up to here").option("--vertical <where>", "make a wide video vertical: left, middle or right").option("--no-write", "write the title and description yourself").action(
|
|
23285
|
+
act(
|
|
23286
|
+
async (opts, url) => tool2(opts, "social_video_add", {
|
|
23287
|
+
url,
|
|
23288
|
+
...opts.note ? { note: opts.note } : {},
|
|
23289
|
+
...opts.start ? { start: Number(opts.start) } : {},
|
|
23290
|
+
...opts.end ? { end: Number(opts.end) } : {},
|
|
23291
|
+
...opts.vertical ? { vertical: opts.vertical } : {},
|
|
23292
|
+
write: opts.write !== false
|
|
23293
|
+
})
|
|
23294
|
+
)
|
|
23295
|
+
);
|
|
23296
|
+
youtube.command("set <id>").description("a YouTube post's title, tags, visibility, made-for-kids or altered-content answer").option("--title <text>").option("--tags <list>", "comma-separated").option("--privacy <p>", "public, unlisted or private").option("--kids <yes|no>", "made for kids").option("--synthetic <yes|no>", "realistic AI-made or altered content").action(
|
|
23297
|
+
act(
|
|
23298
|
+
async (opts, id) => tool2(opts, "social_youtube_set", {
|
|
23299
|
+
id: ids2([id])[0],
|
|
23300
|
+
...opts.title ? { title: opts.title } : {},
|
|
23301
|
+
...opts.tags ? {
|
|
23302
|
+
tags: opts.tags.split(",").map((t) => t.trim()).filter(Boolean)
|
|
23303
|
+
} : {},
|
|
23304
|
+
...opts.privacy ? { privacy: opts.privacy } : {},
|
|
23305
|
+
...opts.kids ? { made_for_kids: opts.kids === "yes" } : {},
|
|
23306
|
+
...opts.synthetic ? { synthetic: opts.synthetic === "yes" } : {}
|
|
23307
|
+
})
|
|
23308
|
+
)
|
|
23309
|
+
);
|
|
23310
|
+
youtube.command("settings").description("your YouTube defaults (visibility, made for kids)").action(act(async (opts) => tool2(opts, "youtube_settings")));
|
|
23039
23311
|
var autopilot = program2.command("autopilot").description("the blog autopilot");
|
|
23040
23312
|
autopilot.command("status", { isDefault: true }).description("on or paused, posts per day, where it publishes").action(act(async (opts) => tool2(opts, "publish_settings_get")));
|
|
23041
23313
|
for (const [cmd, paused] of [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cmo-so/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|