@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.
Files changed (2) hide show
  1. package/dist/cmo.mjs +281 -9
  2. 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 = ["general", "blog", "linkedin", "facebook", "instagram", "google_business"];
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: CHANNEL_KEYS } },
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.3.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 and your Google Business Profile");
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(`${cmd} posting (every channel, or --channel linkedin | facebook | instagram | google_business)`).option("--channel <name>", "only this channel").action(
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.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",