@cmo-so/cli 0.4.0 → 0.6.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 +243 -15
- package/package.json +1 -1
package/dist/cmo.mjs
CHANGED
|
@@ -7480,7 +7480,8 @@ var MEMORY_CHANNELS = [
|
|
|
7480
7480
|
"facebook",
|
|
7481
7481
|
"instagram",
|
|
7482
7482
|
"google_business",
|
|
7483
|
-
"youtube"
|
|
7483
|
+
"youtube",
|
|
7484
|
+
"tiktok"
|
|
7484
7485
|
];
|
|
7485
7486
|
function addWebsite(api2, url) {
|
|
7486
7487
|
return api2.request("/v1/domains", {
|
|
@@ -7732,6 +7733,20 @@ function domainApi(api2, domain) {
|
|
|
7732
7733
|
),
|
|
7733
7734
|
/** Disconnect: the token is deleted and Google's access revoked (YouTube's policy). */
|
|
7734
7735
|
disconnectYouTube: (connectionId) => api2.request(`${base}/social/youtube/${connectionId}`, { method: "DELETE" }),
|
|
7736
|
+
/** A TikTok sign-in link for this website (docs/prd/tiktok.md §7); back with ?tiktok=connected (error&message=…). */
|
|
7737
|
+
connectTikTok: (returnTo = "close") => post(`${base}/social/tiktok/connect`, { return_to: returnTo }),
|
|
7738
|
+
/** What the TikTok account allows right now: show it in the TikTok post editor (TikTok's guidelines). */
|
|
7739
|
+
tiktokCreator: () => api2.request(`${base}/social/tiktok/creator`),
|
|
7740
|
+
/** Disconnect: TikTok's access is revoked and the token deleted. */
|
|
7741
|
+
disconnectTikTok: (connectionId) => api2.request(`${base}/social/tiktok/${connectionId}`, { method: "DELETE" }),
|
|
7742
|
+
/** A TikTok post's settings: who can view it, comments / Duet / Stitch, content disclosure, AI label. */
|
|
7743
|
+
setSocialTikTok: (id, answers) => api2.request(`${base}/social/${id}`, {
|
|
7744
|
+
method: "PATCH",
|
|
7745
|
+
headers: { "Content-Type": "application/json" },
|
|
7746
|
+
body: JSON.stringify({ tiktok: answers })
|
|
7747
|
+
}),
|
|
7748
|
+
/** Ask to join the beta of these channels: an email says what we need (reply or WhatsApp); the team is told. */
|
|
7749
|
+
requestBeta: (req) => post(`${base}/social/beta-request`, req),
|
|
7735
7750
|
/** One channel's own pause switch (pauseSocial / resumeSocial hold every channel). */
|
|
7736
7751
|
setChannelPaused: (channel, paused) => post(`${base}/social/channels/${channel}/${paused ? "pause" : "resume"}`),
|
|
7737
7752
|
/** How often one channel posts. */
|
|
@@ -7914,8 +7929,13 @@ function domainApi(api2, domain) {
|
|
|
7914
7929
|
video: (videoId) => api2.request(`${base}/social/video/${videoId}`),
|
|
7915
7930
|
/** Another part or framing; prepared again in the background. */
|
|
7916
7931
|
editVideo: (videoId, edit) => post(`${base}/social/video/${videoId}/edit`, { edit }),
|
|
7917
|
-
/** A YouTube
|
|
7932
|
+
/** A post on one video channel (YouTube by default, or TikTok) from a ready video, waiting in Review; Maggie
|
|
7933
|
+
* writes the details (YouTube) or the caption (TikTok) unless write is false. */
|
|
7918
7934
|
postFromVideo: (videoId, opts = {}) => post(`${base}/social/video/${videoId}/post`, opts),
|
|
7935
|
+
/** Make a LinkedIn, Facebook or Instagram post a video post: it goes out as this video (its picture is dropped). */
|
|
7936
|
+
attachVideo: (id, videoId) => post(`${base}/social/${id}/video`, { video_id: videoId }),
|
|
7937
|
+
/** Take the video off a post: it's a text post again. */
|
|
7938
|
+
detachVideo: (id) => api2.request(`${base}/social/${id}/video`, { method: "DELETE" }),
|
|
7919
7939
|
youtubeSettings: () => api2.request(`${base}/social/youtube/settings`),
|
|
7920
7940
|
setYouTubeSettings: (s) => put(`${base}/social/youtube/settings`, s),
|
|
7921
7941
|
/** A YouTube post's title, tags, visibility, made-for-kids or altered-content answer. */
|
|
@@ -8163,6 +8183,9 @@ var list = (xs, n = 5) => {
|
|
|
8163
8183
|
};
|
|
8164
8184
|
var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
|
|
8165
8185
|
var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
|
|
8186
|
+
var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
|
|
8187
|
+
(c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
|
|
8188
|
+
);
|
|
8166
8189
|
var confirmCopy = {
|
|
8167
8190
|
// --------------------------------------------------------- account / setup
|
|
8168
8191
|
website_add: {
|
|
@@ -8348,9 +8371,31 @@ var confirmCopy = {
|
|
|
8348
8371
|
en: () => "Make a link to connect your YouTube channel.",
|
|
8349
8372
|
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
|
|
8350
8373
|
},
|
|
8374
|
+
social_video_attach: {
|
|
8375
|
+
en: (a) => `Make post #${a.id} a video post with ${a.url} (its picture is removed).`,
|
|
8376
|
+
"zh-Hant": (a) => `\u628A\u8CBC\u6587 #${a.id} \u6539\u6210\u5F71\u7247\u8CBC\u6587\uFF0C\u7528 ${a.url}\uFF08\u5716\u7247\u6703\u79FB\u9664\uFF09\u3002`
|
|
8377
|
+
},
|
|
8378
|
+
social_video_detach: {
|
|
8379
|
+
en: (a) => `Take the video off post #${a.id} (it's a text post again).`,
|
|
8380
|
+
"zh-Hant": (a) => `\u79FB\u9664\u8CBC\u6587 #${a.id} \u7684\u5F71\u7247\uFF08\u8B8A\u56DE\u6587\u5B57\u8CBC\u6587\uFF09\u3002`
|
|
8381
|
+
},
|
|
8382
|
+
tiktok_connect_link: {
|
|
8383
|
+
en: () => "Make a link to connect your TikTok account.",
|
|
8384
|
+
"zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 TikTok \u5E33\u865F\u7684\u9023\u7D50\u3002"
|
|
8385
|
+
},
|
|
8351
8386
|
social_video_add: {
|
|
8352
|
-
en: (a) =>
|
|
8353
|
-
|
|
8387
|
+
en: (a) => {
|
|
8388
|
+
const names = videoChannels(a).join(" and ");
|
|
8389
|
+
return `Make a ${names} post from ${a.url}${a.write === false ? "" : ` (Maggie writes it: one AI generation per channel)`}; it waits for your OK.`;
|
|
8390
|
+
},
|
|
8391
|
+
"zh-Hant": (a) => {
|
|
8392
|
+
const names = videoChannels(a).join(" \u548C ");
|
|
8393
|
+
return `\u7528 ${a.url} \u505A ${names} \u5F71\u7247\u8CBC\u6587${a.write === false ? "" : "\uFF08Maggie \u4F86\u5BEB\uFF1A\u6BCF\u500B\u6E20\u9053\u7528\u4E00\u6B21 AI \u751F\u6210\uFF09"}\uFF0C\u7B49\u4F60\u78BA\u8A8D\u5F8C\u624D\u767C\u3002`;
|
|
8394
|
+
}
|
|
8395
|
+
},
|
|
8396
|
+
social_tiktok_set: {
|
|
8397
|
+
en: (a) => `Set TikTok post #${a.id}: ${keys(a).filter((k) => k !== "id").map((k) => `${k} ${String(a[k])}`).join(", ") || "its settings"}.`,
|
|
8398
|
+
"zh-Hant": (a) => `\u8A2D\u5B9A TikTok \u8CBC\u6587 #${a.id}\uFF1A${keys(a).filter((k) => k !== "id").map((k) => `${k} ${String(a[k])}`).join("\u3001") || "\u8A2D\u5B9A"}\u3002`
|
|
8354
8399
|
},
|
|
8355
8400
|
social_youtube_set: {
|
|
8356
8401
|
en: (a) => `Change YouTube post #${a.id}: ${keys(a).filter((k) => k !== "id").join(", ") || "its details"}.`,
|
|
@@ -10373,11 +10418,13 @@ var read5 = { resource: "autopilot", level: "read" };
|
|
|
10373
10418
|
var write5 = { resource: "autopilot", level: "write" };
|
|
10374
10419
|
var moves2 = ["up", "down", "top"];
|
|
10375
10420
|
var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
|
|
10376
|
-
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube"];
|
|
10421
|
+
var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
|
|
10422
|
+
var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
|
|
10423
|
+
var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
10377
10424
|
var channelsParam = {
|
|
10378
10425
|
type: "array",
|
|
10379
10426
|
items: { type: "string", enum: CHANNEL_KEYS },
|
|
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
|
|
10427
|
+
description: "Which channels to write for: linkedin, facebook, instagram, google_business. Leave out to write for every connected channel (one post each, one credit each); give one only when the user names it. YouTube and TikTok take a video: use social_video_add."
|
|
10381
10428
|
};
|
|
10382
10429
|
var channelsOf = (value) => {
|
|
10383
10430
|
const list2 = Array.isArray(value) ? value.map((v) => String(v).toLowerCase()).filter((v) => CHANNEL_KEYS.includes(v)) : [];
|
|
@@ -10404,6 +10451,13 @@ var item = (i) => ({
|
|
|
10404
10451
|
...i.provider === "youtube" ? {
|
|
10405
10452
|
youtube: i.youtube ?? null,
|
|
10406
10453
|
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10454
|
+
} : {},
|
|
10455
|
+
...i.video_asset_id && i.provider !== "youtube" && i.provider !== "tiktok" ? {
|
|
10456
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : { id: i.video_asset_id }
|
|
10457
|
+
} : {},
|
|
10458
|
+
...i.provider === "tiktok" ? {
|
|
10459
|
+
tiktok: i.tiktok ?? null,
|
|
10460
|
+
video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
|
|
10407
10461
|
} : {}
|
|
10408
10462
|
});
|
|
10409
10463
|
var socialTools = [
|
|
@@ -11382,13 +11436,110 @@ var socialTools = [
|
|
|
11382
11436
|
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11383
11437
|
}
|
|
11384
11438
|
}),
|
|
11439
|
+
tool({
|
|
11440
|
+
name: "social_video_attach",
|
|
11441
|
+
description: "Make a LinkedIn, Facebook or Instagram post that hasn't gone out a video post: it goes out as this video (a DIRECT link to a video file, 5-30 seconds; never a platform page) with its text, and its picture is removed (a video post has no picture). Not for Google Business (posts can't carry video). Preview first.",
|
|
11442
|
+
parameters: {
|
|
11443
|
+
type: "object",
|
|
11444
|
+
properties: {
|
|
11445
|
+
id: { type: "integer", description: "The post (from social_list)" },
|
|
11446
|
+
url: { type: "string", description: "Direct https link to the video file" }
|
|
11447
|
+
},
|
|
11448
|
+
required: ["id", "url"]
|
|
11449
|
+
},
|
|
11450
|
+
group: "social",
|
|
11451
|
+
need: write5,
|
|
11452
|
+
risk: "W1",
|
|
11453
|
+
gates: ["VER"],
|
|
11454
|
+
run: async (ctx, a) => {
|
|
11455
|
+
const d = ws(ctx);
|
|
11456
|
+
let v = await d.videoFromLink(String(a.url));
|
|
11457
|
+
const until = Date.now() + 15e4;
|
|
11458
|
+
while ((v.status === "checking" || v.status === "uploading") && Date.now() < until) {
|
|
11459
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
11460
|
+
v = await d.video(v.id);
|
|
11461
|
+
}
|
|
11462
|
+
if (v.status !== "ready")
|
|
11463
|
+
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11464
|
+
return { ok: true, post: item(await d.attachVideo(Number(a.id), v.id)) };
|
|
11465
|
+
}
|
|
11466
|
+
}),
|
|
11467
|
+
tool({
|
|
11468
|
+
name: "social_video_detach",
|
|
11469
|
+
description: "Take the video off a LinkedIn, Facebook or Instagram post that hasn't gone out: it's a text post again (a picture can be added after). Preview first.",
|
|
11470
|
+
parameters: { type: "object", properties: { id: { type: "integer" } }, required: ["id"] },
|
|
11471
|
+
group: "social",
|
|
11472
|
+
need: write5,
|
|
11473
|
+
risk: "W1",
|
|
11474
|
+
gates: ["VER"],
|
|
11475
|
+
run: async (ctx, a) => item(await ws(ctx).detachVideo(Number(a.id)))
|
|
11476
|
+
}),
|
|
11477
|
+
tool({
|
|
11478
|
+
name: "tiktok_connect_link",
|
|
11479
|
+
description: "A link for the user to connect their TikTok account (or reconnect): they sign in to TikTok and allow Maggie to see their profile and post videos. Give them the link to open; never ask for a password. TikTok is in beta (testers only; until TikTok reviews our app, videos post as 'Only me' and the account must be private). TikTok posts are short videos (5-30 seconds) that the user approves one by one.",
|
|
11480
|
+
parameters: none,
|
|
11481
|
+
group: "social",
|
|
11482
|
+
need: write5,
|
|
11483
|
+
risk: "W1",
|
|
11484
|
+
gates: ["VER"],
|
|
11485
|
+
run: async (ctx) => {
|
|
11486
|
+
const out = await ws(ctx).connectTikTok("close");
|
|
11487
|
+
return { url: out.url, expires_in_seconds: out.expires_in_seconds };
|
|
11488
|
+
}
|
|
11489
|
+
}),
|
|
11490
|
+
tool({
|
|
11491
|
+
name: "tiktok_creator",
|
|
11492
|
+
description: "What the user's TikTok account allows right now (TikTok asks to check before every post): its name and @username, who a video can be shown to (privacy_options), whether comments / Duet / Stitch are turned off on the account, the longest video it can post, and whether it can post now (problem). Read it before asking the user the TikTok settings of a post.",
|
|
11493
|
+
parameters: none,
|
|
11494
|
+
group: "social",
|
|
11495
|
+
need: read5,
|
|
11496
|
+
risk: "R0",
|
|
11497
|
+
run: async (ctx) => ws(ctx).tiktokCreator()
|
|
11498
|
+
}),
|
|
11499
|
+
tool({
|
|
11500
|
+
name: "social_tiktok_set",
|
|
11501
|
+
description: "Set a TikTok post's settings, ONLY as the user chose them (TikTok's rules: never pick for them, nothing is preset): privacy (one of the account's privacy_options from tiktok_creator: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS, FOLLOWER_OF_CREATOR, SELF_ONLY), allow_comment / allow_duet / allow_stitch (off unless the user turns them on; not possible when the account turned them off), disclose (the video promotes a brand, product or service) with your_brand (their own: labelled 'Promotional content') and / or branded (a third party: labelled 'Paid partnership'; can't be SELF_ONLY), is_aigc (AI-generated content label). The caption is the post's text (social_update_copy). Before the user approves, tell them: 'By posting, you agree to TikTok's Music Usage Confirmation' (with branded content: 'TikTok's Branded Content Policy and Music Usage Confirmation'). Preview first.",
|
|
11502
|
+
parameters: {
|
|
11503
|
+
type: "object",
|
|
11504
|
+
properties: {
|
|
11505
|
+
id: { type: "integer", description: "A TikTok post (from social_list)" },
|
|
11506
|
+
privacy: { type: "string", enum: TIKTOK_PRIVACY },
|
|
11507
|
+
allow_comment: { type: "boolean" },
|
|
11508
|
+
allow_duet: { type: "boolean" },
|
|
11509
|
+
allow_stitch: { type: "boolean" },
|
|
11510
|
+
disclose: { type: "boolean" },
|
|
11511
|
+
your_brand: { type: "boolean" },
|
|
11512
|
+
branded: { type: "boolean" },
|
|
11513
|
+
is_aigc: { type: "boolean" }
|
|
11514
|
+
},
|
|
11515
|
+
required: ["id"]
|
|
11516
|
+
},
|
|
11517
|
+
group: "social",
|
|
11518
|
+
need: write5,
|
|
11519
|
+
risk: "W1",
|
|
11520
|
+
gates: ["VER"],
|
|
11521
|
+
run: async (ctx, { id, privacy, ...rest }) => {
|
|
11522
|
+
const answers = Object.fromEntries(
|
|
11523
|
+
Object.entries(rest).filter(([, v]) => v !== void 0)
|
|
11524
|
+
);
|
|
11525
|
+
if (privacy) answers.privacy = privacy;
|
|
11526
|
+
const out = await ws(ctx).setSocialTikTok(Number(id), answers);
|
|
11527
|
+
return item(out);
|
|
11528
|
+
}
|
|
11529
|
+
}),
|
|
11385
11530
|
tool({
|
|
11386
11531
|
name: "social_video_add",
|
|
11387
|
-
description: "Make
|
|
11532
|
+
description: "Make video posts (channels: linkedin, facebook, instagram, youtube, tiktok; default youtube; one post each, never google_business: Google posts can't carry video) 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 YouTube's title, description and tags, TikTok's caption, or the LinkedIn / Facebook / Instagram text, from the video and the note (one AI generation per channel) unless write is false. Each post waits in Review: video posts are always approved by the user. YouTube asks whether it's made for kids; TikTok asks who can view it and the other TikTok settings (social_tiktok_set, as the user chooses). is_aigc: TikTok's AI-generated label, only when the user says the video was made with AI. Preview first.",
|
|
11388
11533
|
parameters: {
|
|
11389
11534
|
type: "object",
|
|
11390
11535
|
properties: {
|
|
11391
11536
|
url: { type: "string", description: "Direct https link to the video file" },
|
|
11537
|
+
channels: {
|
|
11538
|
+
type: "array",
|
|
11539
|
+
items: { type: "string", enum: [...VIDEO_KEYS] },
|
|
11540
|
+
description: "Where the video goes (one post each); default youtube"
|
|
11541
|
+
},
|
|
11542
|
+
is_aigc: { type: "boolean", description: "TikTok: the video is AI-generated (the user said so)" },
|
|
11392
11543
|
note: { type: "string", description: "What the video is about, in the user's words" },
|
|
11393
11544
|
write: { type: "boolean", description: "Let Maggie write the details (default true)" },
|
|
11394
11545
|
start: { type: "number", description: "Start of the part to use, seconds" },
|
|
@@ -11416,11 +11567,28 @@ var socialTools = [
|
|
|
11416
11567
|
}
|
|
11417
11568
|
if (v.status !== "ready")
|
|
11418
11569
|
return { ok: false, video: { status: v.status, error: v.error, seconds: v.source_duration } };
|
|
11419
|
-
const
|
|
11570
|
+
const wanted = (a.channels ?? ["youtube"]).filter(
|
|
11571
|
+
(c) => VIDEO_KEYS.includes(c)
|
|
11572
|
+
);
|
|
11573
|
+
const posts2 = [];
|
|
11574
|
+
for (const channel of wanted.length ? wanted : ["youtube"]) {
|
|
11575
|
+
posts2.push(
|
|
11576
|
+
await d.postFromVideo(v.id, {
|
|
11577
|
+
channel,
|
|
11578
|
+
note: a.note ?? "",
|
|
11579
|
+
write: a.write !== false,
|
|
11580
|
+
...channel === "tiktok" && a.is_aigc != null ? { is_aigc: a.is_aigc } : {}
|
|
11581
|
+
})
|
|
11582
|
+
);
|
|
11583
|
+
}
|
|
11420
11584
|
return {
|
|
11421
11585
|
ok: true,
|
|
11422
|
-
post: item(
|
|
11423
|
-
next:
|
|
11586
|
+
...posts2.length === 1 ? { post: item(posts2[0]) } : { posts: posts2.map(item) },
|
|
11587
|
+
next: [
|
|
11588
|
+
posts2.some((p) => p.provider === "youtube") ? "YouTube: show the title and description; ask whether it's made for kids and which visibility, then social_youtube_set." : "",
|
|
11589
|
+
posts2.some((p) => p.provider === "tiktok") ? "TikTok: show the caption; read tiktok_creator and ask who can view it, whether to allow comments / Duet / Stitch and whether it promotes a brand, then social_tiktok_set exactly as they say; tell them the Music Usage Confirmation line." : "",
|
|
11590
|
+
"The user approves each post."
|
|
11591
|
+
].filter(Boolean).join(" ")
|
|
11424
11592
|
};
|
|
11425
11593
|
}
|
|
11426
11594
|
}),
|
|
@@ -15094,7 +15262,7 @@ var program = new Command();
|
|
|
15094
15262
|
// package.json
|
|
15095
15263
|
var package_default = {
|
|
15096
15264
|
name: "@cmo-so/cli",
|
|
15097
|
-
version: "0.
|
|
15265
|
+
version: "0.6.0",
|
|
15098
15266
|
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.",
|
|
15099
15267
|
type: "module",
|
|
15100
15268
|
license: "MIT",
|
|
@@ -23236,10 +23404,14 @@ posts.command("ls", { isDefault: true }).description("planned, draft or publishe
|
|
|
23236
23404
|
posts.command("show <id>").description("one post in full").action(act(async (opts, id) => tool2(opts, "post_get", { id })));
|
|
23237
23405
|
posts.command("publish <id>").description("publish a draft now (uses a post credit)").action(act(async (opts, id) => tool2(opts, "post_publish_now", { id })));
|
|
23238
23406
|
posts.command("delete <id>").description("delete a draft").action(act(async (opts, id) => tool2(opts, "draft_delete", { id })));
|
|
23239
|
-
var social = program2.command("social").description(
|
|
23240
|
-
|
|
23407
|
+
var social = program2.command("social").description(
|
|
23408
|
+
"your social posts: LinkedIn, your Facebook Page, Instagram, your Google Business Profile, YouTube and TikTok"
|
|
23409
|
+
);
|
|
23410
|
+
var CHANNELS = ["linkedin", "facebook", "instagram", "google_business", "youtube", "tiktok"];
|
|
23241
23411
|
var channelsOpt = (value) => {
|
|
23242
|
-
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map(
|
|
23412
|
+
const list2 = (value ?? "").split(",").map((v) => v.trim().toLowerCase()).map(
|
|
23413
|
+
(v) => v === "google" || v === "gbp" ? "google_business" : v === "yt" || v === "shorts" ? "youtube" : v === "tt" ? "tiktok" : v
|
|
23414
|
+
).filter((v) => CHANNELS.includes(v));
|
|
23243
23415
|
return list2.length ? list2 : void 0;
|
|
23244
23416
|
};
|
|
23245
23417
|
social.command("ls", { isDefault: true }).description("drafts to review and queued posts").action(
|
|
@@ -23270,7 +23442,7 @@ for (const [cmd, paused] of [
|
|
|
23270
23442
|
["resume", false]
|
|
23271
23443
|
])
|
|
23272
23444
|
social.command(cmd).description(
|
|
23273
|
-
`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business | youtube)`
|
|
23445
|
+
`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business | youtube | tiktok)`
|
|
23274
23446
|
).option("--channel <name>", "only this channel").action(
|
|
23275
23447
|
act(
|
|
23276
23448
|
async (opts) => tool2(opts, "social_set_paused", {
|
|
@@ -23308,6 +23480,62 @@ youtube.command("set <id>").description("a YouTube post's title, tags, visibilit
|
|
|
23308
23480
|
)
|
|
23309
23481
|
);
|
|
23310
23482
|
youtube.command("settings").description("your YouTube defaults (visibility, made for kids)").action(act(async (opts) => tool2(opts, "youtube_settings")));
|
|
23483
|
+
var video = social.command("video").description("video posts: LinkedIn, Facebook, Instagram, YouTube, TikTok");
|
|
23484
|
+
video.command("add <url>").description("posts from a direct link to a video file (5-30 s), one per channel; Maggie writes the words").option("--channel <names>", "linkedin, facebook, instagram, youtube, tiktok (comma-separated)", "youtube").option("--note <text>", "what the video is about").option("--no-write", "write the words yourself").action(
|
|
23485
|
+
act(
|
|
23486
|
+
async (opts, url) => tool2(opts, "social_video_add", {
|
|
23487
|
+
url,
|
|
23488
|
+
channels: (channelsOpt(opts.channel) ?? ["youtube"]).filter((c) => c !== "google_business"),
|
|
23489
|
+
...opts.note ? { note: opts.note } : {},
|
|
23490
|
+
write: opts.write !== false
|
|
23491
|
+
})
|
|
23492
|
+
)
|
|
23493
|
+
);
|
|
23494
|
+
video.command("attach <id> <url>").description("make a LinkedIn / Facebook / Instagram post a video post (its picture is removed)").action(act(async (opts, id, url) => tool2(opts, "social_video_attach", { id: ids2([id])[0], url })));
|
|
23495
|
+
video.command("detach <id>").description("take the video off a post (it's a text post again)").action(act(async (opts, id) => tool2(opts, "social_video_detach", { id: ids2([id])[0] })));
|
|
23496
|
+
var tiktok = social.command("tiktok").description("your TikTok account (beta): short videos you approve one by one");
|
|
23497
|
+
tiktok.command("connect").description("a link to connect your TikTok account").action(act(async (opts) => tool2(opts, "tiktok_connect_link")));
|
|
23498
|
+
tiktok.command("account").description("what your TikTok account allows now: who can view, comments / Duet / Stitch, longest video").action(act(async (opts) => tool2(opts, "tiktok_creator")));
|
|
23499
|
+
tiktok.command("add <url>").description("a TikTok post from a direct link to a video file (5-30 s): Maggie writes the caption").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("--ai", "the video is AI-generated (TikTok labels it)").option("--no-write", "write the caption yourself").action(
|
|
23500
|
+
act(
|
|
23501
|
+
async (opts, url) => tool2(opts, "social_video_add", {
|
|
23502
|
+
url,
|
|
23503
|
+
channels: ["tiktok"],
|
|
23504
|
+
...opts.note ? { note: opts.note } : {},
|
|
23505
|
+
...opts.start ? { start: Number(opts.start) } : {},
|
|
23506
|
+
...opts.end ? { end: Number(opts.end) } : {},
|
|
23507
|
+
...opts.vertical ? { vertical: opts.vertical } : {},
|
|
23508
|
+
...opts.ai ? { is_aigc: true } : {},
|
|
23509
|
+
write: opts.write !== false
|
|
23510
|
+
})
|
|
23511
|
+
)
|
|
23512
|
+
);
|
|
23513
|
+
tiktok.command("set <id>").description("a TikTok post's settings, as you choose them (nothing is preset)").option("--privacy <p>", "everyone, friends, followers or only-me (what your account offers)").option("--comment <yes|no>").option("--duet <yes|no>").option("--stitch <yes|no>").option("--promotes <what>", "none, your-brand, branded or both (content disclosure)").option("--ai <yes|no>", "AI-generated content label").action(
|
|
23514
|
+
act(
|
|
23515
|
+
async (opts, id) => {
|
|
23516
|
+
const privacy = {
|
|
23517
|
+
everyone: "PUBLIC_TO_EVERYONE",
|
|
23518
|
+
friends: "MUTUAL_FOLLOW_FRIENDS",
|
|
23519
|
+
followers: "FOLLOWER_OF_CREATOR",
|
|
23520
|
+
"only-me": "SELF_ONLY"
|
|
23521
|
+
}[String(opts.privacy ?? "").toLowerCase()];
|
|
23522
|
+
const p = opts.promotes?.toLowerCase();
|
|
23523
|
+
return tool2(opts, "social_tiktok_set", {
|
|
23524
|
+
id: ids2([id])[0],
|
|
23525
|
+
...privacy ? { privacy } : {},
|
|
23526
|
+
...opts.comment ? { allow_comment: opts.comment === "yes" } : {},
|
|
23527
|
+
...opts.duet ? { allow_duet: opts.duet === "yes" } : {},
|
|
23528
|
+
...opts.stitch ? { allow_stitch: opts.stitch === "yes" } : {},
|
|
23529
|
+
...opts.ai ? { is_aigc: opts.ai === "yes" } : {},
|
|
23530
|
+
...p ? {
|
|
23531
|
+
disclose: p !== "none",
|
|
23532
|
+
your_brand: p === "your-brand" || p === "both",
|
|
23533
|
+
branded: p === "branded" || p === "both"
|
|
23534
|
+
} : {}
|
|
23535
|
+
});
|
|
23536
|
+
}
|
|
23537
|
+
)
|
|
23538
|
+
);
|
|
23311
23539
|
var autopilot = program2.command("autopilot").description("the blog autopilot");
|
|
23312
23540
|
autopilot.command("status", { isDefault: true }).description("on or paused, posts per day, where it publishes").action(act(async (opts) => tool2(opts, "publish_settings_get")));
|
|
23313
23541
|
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.6.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",
|