@cmo-so/cli 0.5.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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +71 -5
  2. package/package.json +1 -1
package/dist/cmo.mjs CHANGED
@@ -7932,6 +7932,10 @@ function domainApi(api2, domain) {
7932
7932
  /** A post on one video channel (YouTube by default, or TikTok) from a ready video, waiting in Review; Maggie
7933
7933
  * writes the details (YouTube) or the caption (TikTok) unless write is false. */
7934
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" }),
7935
7939
  youtubeSettings: () => api2.request(`${base}/social/youtube/settings`),
7936
7940
  setYouTubeSettings: (s) => put(`${base}/social/youtube/settings`, s),
7937
7941
  /** A YouTube post's title, tags, visibility, made-for-kids or altered-content answer. */
@@ -8180,7 +8184,7 @@ var list = (xs, n = 5) => {
8180
8184
  var ids = (a) => (Array.isArray(a.ids) && a.ids.length ? a.ids : a.id != null ? [a.id] : []).map(Number);
8181
8185
  var keys = (a) => Object.keys(a ?? {}).filter((k) => a[k] !== void 0);
8182
8186
  var videoChannels = (a) => (Array.isArray(a.channels) && a.channels.length ? a.channels : ["youtube"]).map(
8183
- (c) => c === "tiktok" ? "TikTok" : "YouTube"
8187
+ (c) => ({ tiktok: "TikTok", linkedin: "LinkedIn", facebook: "Facebook", instagram: "Instagram" })[String(c)] ?? "YouTube"
8184
8188
  );
8185
8189
  var confirmCopy = {
8186
8190
  // --------------------------------------------------------- account / setup
@@ -8367,6 +8371,14 @@ var confirmCopy = {
8367
8371
  en: () => "Make a link to connect your YouTube channel.",
8368
8372
  "zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 YouTube \u983B\u9053\u7684\u9023\u7D50\u3002"
8369
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
+ },
8370
8382
  tiktok_connect_link: {
8371
8383
  en: () => "Make a link to connect your TikTok account.",
8372
8384
  "zh-Hant": () => "\u7522\u751F\u9023\u63A5\u4F60 TikTok \u5E33\u865F\u7684\u9023\u7D50\u3002"
@@ -10407,7 +10419,7 @@ var write5 = { resource: "autopilot", level: "write" };
10407
10419
  var moves2 = ["up", "down", "top"];
10408
10420
  var CHANNEL_KEYS = ["linkedin", "facebook", "instagram", "google_business"];
10409
10421
  var ALL_CHANNEL_KEYS = [...CHANNEL_KEYS, "youtube", "tiktok"];
10410
- var VIDEO_KEYS = ["youtube", "tiktok"];
10422
+ var VIDEO_KEYS = ["linkedin", "facebook", "instagram", "youtube", "tiktok"];
10411
10423
  var TIKTOK_PRIVACY = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
10412
10424
  var channelsParam = {
10413
10425
  type: "array",
@@ -10440,6 +10452,9 @@ var item = (i) => ({
10440
10452
  youtube: i.youtube ?? null,
10441
10453
  video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
10442
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
+ } : {},
10443
10458
  ...i.provider === "tiktok" ? {
10444
10459
  tiktok: i.tiktok ?? null,
10445
10460
  video: i.video ? { status: i.video.status, seconds: i.video.duration, shape: i.video.shape, error: i.video.error } : null
@@ -11421,6 +11436,44 @@ var socialTools = [
11421
11436
  return { url: out.url, expires_in_seconds: out.expires_in_seconds };
11422
11437
  }
11423
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
+ }),
11424
11477
  tool({
11425
11478
  name: "tiktok_connect_link",
11426
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.",
@@ -11476,7 +11529,7 @@ var socialTools = [
11476
11529
  }),
11477
11530
  tool({
11478
11531
  name: "social_video_add",
11479
- description: "Make a YouTube and / or TikTok post (channels; default youtube) 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, or TikTok's caption, 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.",
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.",
11480
11533
  parameters: {
11481
11534
  type: "object",
11482
11535
  properties: {
@@ -11484,7 +11537,7 @@ var socialTools = [
11484
11537
  channels: {
11485
11538
  type: "array",
11486
11539
  items: { type: "string", enum: [...VIDEO_KEYS] },
11487
- description: "youtube, tiktok or both (one post each); default youtube"
11540
+ description: "Where the video goes (one post each); default youtube"
11488
11541
  },
11489
11542
  is_aigc: { type: "boolean", description: "TikTok: the video is AI-generated (the user said so)" },
11490
11543
  note: { type: "string", description: "What the video is about, in the user's words" },
@@ -15209,7 +15262,7 @@ var program = new Command();
15209
15262
  // package.json
15210
15263
  var package_default = {
15211
15264
  name: "@cmo-so/cli",
15212
- version: "0.5.0",
15265
+ version: "0.6.0",
15213
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.",
15214
15267
  type: "module",
15215
15268
  license: "MIT",
@@ -23427,6 +23480,19 @@ youtube.command("set <id>").description("a YouTube post's title, tags, visibilit
23427
23480
  )
23428
23481
  );
23429
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] })));
23430
23496
  var tiktok = social.command("tiktok").description("your TikTok account (beta): short videos you approve one by one");
23431
23497
  tiktok.command("connect").description("a link to connect your TikTok account").action(act(async (opts) => tool2(opts, "tiktok_connect_link")));
23432
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")));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmo-so/cli",
3
- "version": "0.5.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",