@rolino/cli 0.1.0 → 0.3.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/index.cjs CHANGED
@@ -41,6 +41,7 @@ module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/cli.ts
43
43
  var import_node_crypto2 = require("crypto");
44
+ var import_node_fs2 = require("fs");
44
45
  var import_promises2 = require("fs/promises");
45
46
  var import_node_path2 = require("path");
46
47
  var import_promises3 = require("readline/promises");
@@ -49,7 +50,7 @@ var import_commander = require("commander");
49
50
  // package.json
50
51
  var package_default = {
51
52
  name: "@rolino/cli",
52
- version: "0.1.0",
53
+ version: "0.3.0",
53
54
  description: "Agent-friendly command-line interface for Rolino",
54
55
  type: "module",
55
56
  license: "MIT",
@@ -106,9 +107,9 @@ var package_default = {
106
107
  dev: "tsx src/bin.ts"
107
108
  },
108
109
  dependencies: {
109
- "@rolino/contracts": "0.1.0",
110
- "@rolino/local-auth": "0.1.0",
111
- "@rolino/sdk": "0.1.0",
110
+ "@rolino/contracts": "0.3.0",
111
+ "@rolino/local-auth": "0.3.0",
112
+ "@rolino/sdk": "0.3.0",
112
113
  commander: "^15.0.0",
113
114
  open: "^11.0.0"
114
115
  },
@@ -492,7 +493,13 @@ function formatPost(post) {
492
493
  ] : [],
493
494
  ...post.publishedAt ? [`Published: ${post.publishedAt}`] : [],
494
495
  `Media: ${post.media.length}`,
495
- `Destinations: ${post.destinations.map((destination) => `${destination.provider}:${queued && destination.status === "SCHEDULED" ? "QUEUED" : destination.status}`).join(", ") || "none"}`
496
+ `Destinations: ${post.destinations.map((destination) => `${destination.provider}:${queued && destination.status === "SCHEDULED" ? "QUEUED" : destination.status}`).join(", ") || "none"}`,
497
+ ...post.providerSettings?.TIKTOK ? [
498
+ `TikTok: ${post.providerSettings.TIKTOK.postMode}, ${post.providerSettings.TIKTOK.privacyLevel ?? "visibility not set"}, settings ${post.providerSettings.TIKTOK.consentedAt ? "reviewed" : "not reviewed"}`
499
+ ] : [],
500
+ ...post.providerSettings?.YOUTUBE ? [
501
+ `YouTube: ${post.providerSettings.YOUTUBE.privacyStatus}, ${post.providerSettings.YOUTUBE.title}`
502
+ ] : []
496
503
  ].join("\n");
497
504
  }
498
505
  function formatPostSchedulePending(pending) {
@@ -506,6 +513,28 @@ function formatIntegrationHealth(data) {
506
513
  const rows = data.items.map((item) => `${item.provider} ${item.connected ? "connected" : "disconnected"} ${item.health.status} ${item.health.isStale ? "stale" : "current"} ${item.health.message}`);
507
514
  return ["PROVIDER CONNECTION HEALTH FRESHNESS DETAIL", ...rows].join("\n");
508
515
  }
516
+ function formatProviderDeliveryOptions(options) {
517
+ switch (options.provider) {
518
+ case "TIKTOK":
519
+ return [
520
+ `TikTok delivery options for ${options.account.displayName ?? options.account.username ?? "connected account"}`,
521
+ `Checked: ${options.checkedAt}`,
522
+ `Health: ${options.health.status} (${options.health.code}) ${options.health.message}`,
523
+ `Modes: ${options.allowedPostModes.join(", ") || "none"}`,
524
+ `Visibility: ${options.visibilityOptions.join(", ") || "none"}`,
525
+ `Interactions: comments ${options.interactions.comment.available ? "available" : "disabled"}, duet ${options.interactions.duet.available ? "available" : "disabled"}, stitch ${options.interactions.stitch.available ? "available" : "disabled"}`,
526
+ `Maximum video duration: ${options.maxVideoDurationMs === null ? "unknown" : `${options.maxVideoDurationMs} ms`}`
527
+ ].join("\n");
528
+ case "LINKEDIN":
529
+ return [
530
+ `LinkedIn delivery options for ${options.account.displayName ?? "connected member"}`,
531
+ `Checked: ${options.checkedAt}`,
532
+ `Health: ${options.health.status} (${options.health.code}) ${options.health.message}`,
533
+ `Post types: ${options.supportedPostTypes.join(", ")}`,
534
+ `Images: up to ${options.image.maxItems}; ${options.image.mimeTypes.join(", ")}; under ${options.image.maxPixelsExclusive.toLocaleString()} pixels`
535
+ ].join("\n");
536
+ }
537
+ }
509
538
  function formatPostReadiness(readiness) {
510
539
  const attention = readiness.checks.filter((check) => check.status !== "pass");
511
540
  const summary = readiness.hasBlockingChecks ? "Post is not ready to publish." : readiness.requiresLiveRefresh.length ? "Local checks passed; live provider health must be refreshed." : "Post is ready to publish.";
@@ -850,7 +879,32 @@ function collectPublishingProvider(value, previous) {
850
879
  return [...previous ?? [], publishingProvider(value)];
851
880
  }
852
881
  function collectString(value, previous) {
853
- return [...previous, value];
882
+ return [...previous ?? [], value];
883
+ }
884
+ function deliveryOptionsProvider(value) {
885
+ const normalized = value.toUpperCase();
886
+ const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
887
+ if (parsed.success) return parsed.data;
888
+ throw new import_commander.InvalidArgumentError(
889
+ `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
890
+ );
891
+ }
892
+ function tiktokPostMode(value) {
893
+ const normalized = value.trim().toLowerCase();
894
+ if (normalized === "direct" || normalized === "direct_post") {
895
+ return "DIRECT_POST";
896
+ }
897
+ if (normalized === "inbox" || normalized === "media_upload") {
898
+ return "MEDIA_UPLOAD";
899
+ }
900
+ throw new import_commander.InvalidArgumentError("TikTok mode must be direct or inbox.");
901
+ }
902
+ function nonnegativeInteger(value) {
903
+ const parsed = Number(value);
904
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
905
+ throw new import_commander.InvalidArgumentError("Expected a nonnegative integer.");
906
+ }
907
+ return parsed;
854
908
  }
855
909
  function youtubePrivacy(value) {
856
910
  const normalized = value.toUpperCase();
@@ -868,7 +922,7 @@ function yesOrNo(value) {
868
922
  throw new import_commander.InvalidArgumentError("Expected yes or no.");
869
923
  }
870
924
  function youtubeSettings(options) {
871
- const hasYouTubeOptions = options.youtubeTitle !== void 0 || options.youtubeCategoryId !== void 0 || options.youtubePrivacy !== void 0 || options.youtubeMadeForKids !== void 0 || options.youtubeSyntheticMedia === true || options.youtubeNotifySubscribers === false || options.youtubeTag.length > 0;
925
+ const hasYouTubeOptions = options.youtubeTitle !== void 0 || options.youtubeCategoryId !== void 0 || options.youtubePrivacy !== void 0 || options.youtubeMadeForKids !== void 0 || options.youtubeSyntheticMedia === true || options.youtubeNotifySubscribers === false || (options.youtubeTag?.length ?? 0) > 0;
872
926
  if (!hasYouTubeOptions) return null;
873
927
  return import_contracts2.YouTubePostSettingsSchema.parse({
874
928
  title: options.youtubeTitle,
@@ -877,7 +931,7 @@ function youtubeSettings(options) {
877
931
  madeForKids: options.youtubeMadeForKids,
878
932
  containsSyntheticMedia: options.youtubeSyntheticMedia ?? false,
879
933
  notifySubscribers: options.youtubeNotifySubscribers ?? true,
880
- tags: options.youtubeTag
934
+ tags: options.youtubeTag ?? []
881
935
  });
882
936
  }
883
937
  function positiveInteger(value) {
@@ -1023,6 +1077,23 @@ function errorForOutput(error) {
1023
1077
  }
1024
1078
  return { code: "CLI_ERROR", message: "The command failed unexpectedly." };
1025
1079
  }
1080
+ function tiktokSettings(options) {
1081
+ const hasTikTokOptions = options.tiktokMode !== void 0 || options.tiktokVisibility !== void 0 || options.tiktokComments !== void 0 || options.tiktokDuet !== void 0 || options.tiktokStitch !== void 0 || options.tiktokCommercialContent !== void 0 || options.tiktokPromotesOwnBrand !== void 0 || options.tiktokPromotesThirdParty !== void 0 || options.tiktokAiGenerated !== void 0 || options.tiktokCoverTimestampMs !== void 0 || options.tiktokSettingsReviewed === true;
1082
+ if (!hasTikTokOptions) return void 0;
1083
+ return import_contracts2.TikTokDraftSettingsSchema.parse({
1084
+ postMode: options.tiktokMode,
1085
+ privacyLevel: options.tiktokVisibility,
1086
+ allowComment: options.tiktokComments,
1087
+ allowDuet: options.tiktokDuet,
1088
+ allowStitch: options.tiktokStitch,
1089
+ commercialContentEnabled: options.tiktokCommercialContent,
1090
+ promotesOwnBrand: options.tiktokPromotesOwnBrand,
1091
+ promotesThirdParty: options.tiktokPromotesThirdParty,
1092
+ isAiGenerated: options.tiktokAiGenerated,
1093
+ videoCoverTimestampMs: options.tiktokCoverTimestampMs,
1094
+ reviewed: options.tiktokSettingsReviewed === true
1095
+ });
1096
+ }
1026
1097
  function recoverySuggestions(code, command) {
1027
1098
  switch (code) {
1028
1099
  case "AUTH_REQUIRED":
@@ -1226,8 +1297,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1226
1297
  const contentType = contentTypes[(0, import_node_path2.extname)(absolutePath).toLowerCase()];
1227
1298
  if (!contentType) throw new TypeError("Use a JPEG, PNG, WebP, MP4, or MOV file.");
1228
1299
  await requireWriteConsent({ yes: local.yes, global, runtime, subject: "media", preview: [`Upload ${(0, import_node_path2.basename)(absolutePath)} to Rolino?`, `Project: ${local.project}`, `Size: ${details.size} bytes`, "This adds a reusable media asset but does not create, schedule, or publish a post."].join("\n") });
1229
- const bytes = await (0, import_promises2.readFile)(absolutePath);
1230
- const asset = await client.media.upload(local.project, { fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, body: new Blob([bytes], { type: contentType }) }, { requestId: context.requestId });
1300
+ const body = await (0, import_node_fs2.openAsBlob)(absolutePath, { type: contentType });
1301
+ const asset = await client.media.upload(local.project, { fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, body }, { requestId: context.requestId });
1231
1302
  writeSuccess(context, asset, formatMediaAsset(asset), [`rolino posts create --project ${local.project} --caption <text> --media ${asset.id} --agent --yes`]);
1232
1303
  }
1233
1304
  });
@@ -1450,7 +1521,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1450
1521
  });
1451
1522
  });
1452
1523
  const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1453
- posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, or BLUESKY; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1524
+ posts.command("create").description("Create a draft post without scheduling or publishing it").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--caption <text>", "draft caption; use an empty string for media-only drafts").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "existing project media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm creation without an interactive prompt").action(async (local) => {
1454
1525
  const global = program.opts();
1455
1526
  commandExitCode = await execute({
1456
1527
  command: "posts create",
@@ -1476,9 +1547,10 @@ async function runCli(argv = process.argv, overrides = {}) {
1476
1547
  ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1477
1548
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1478
1549
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1479
- ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1550
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1551
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1480
1552
  },
1481
- tiktokSettings: null,
1553
+ tiktokSettings: tiktokSettings(local) ?? null,
1482
1554
  youtubeSettings: youtubeSettings(local)
1483
1555
  };
1484
1556
  const post = await client.posts.create(local.project, input, {
@@ -1494,7 +1566,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1494
1566
  }
1495
1567
  });
1496
1568
  });
1497
- posts.command("update").description("Replace an existing draft using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--caption <text>", "replacement draft caption").option("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, or BLUESKY; repeat as needed", collectPublishingProvider, []).option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString, []).option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "YouTube tag; repeat as needed", collectString, []).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1569
+ posts.command("update").description("Update selected draft fields using optimistic concurrency").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).option("--caption <text>", "replacement draft caption; omitted fields are preserved").option("--platform <provider>", "replacement destinations; repeat as needed", collectPublishingProvider).option("--clear-platforms", "remove every draft destination").option("--media <asset-id>", "replacement media asset ID; repeat for multiple", collectString).option("--clear-media", "remove every media asset from the draft").option("--instagram-caption <text>", "Instagram-specific caption override").option("--tiktok-caption <text>", "TikTok-specific caption override").option("--tiktok-mode <mode>", "direct publishing or inbox draft delivery", tiktokPostMode).option("--tiktok-visibility <value>", "exact visibility returned by integrations delivery-options").option("--tiktok-comments <yes|no>", "allow TikTok comments", yesOrNo).option("--tiktok-duet <yes|no>", "allow TikTok duets", yesOrNo).option("--tiktok-stitch <yes|no>", "allow TikTok stitches", yesOrNo).option("--tiktok-commercial-content <yes|no>", "declare commercial TikTok content", yesOrNo).option("--tiktok-promotes-own-brand <yes|no>", "declare own-brand promotion", yesOrNo).option("--tiktok-promotes-third-party <yes|no>", "declare third-party promotion", yesOrNo).option("--tiktok-ai-generated <yes|no>", "declare AI-generated TikTok content", yesOrNo).option("--tiktok-cover-timestamp-ms <number>", "video cover timestamp in milliseconds", nonnegativeInteger).option("--tiktok-settings-reviewed", "confirm the TikTok account choices were reviewed; separate from --yes").option("--youtube-caption <text>", "YouTube description override; otherwise the shared caption is used").option("--bluesky-caption <text>", "Bluesky-specific caption override; limited to 300 graphemes").option("--linkedin-caption <text>", "LinkedIn-specific caption override; limited to 3,000 characters").option("--youtube-title <text>", "required YouTube video title").option("--youtube-category-id <id>", "required numeric YouTube video category ID").option("--youtube-privacy <status>", "required PUBLIC, UNLISTED, or PRIVATE visibility", youtubePrivacy).option("--youtube-made-for-kids <yes|no>", "required explicit YouTube audience declaration", yesOrNo).option("--youtube-synthetic-media", "declare realistic altered or synthetic media to YouTube").option("--no-youtube-notify-subscribers", "disable eligible YouTube subscriber notifications").option("--youtube-tag <tag>", "replacement YouTube tag; repeat as needed", collectString).option("--idempotency-key <key>", "stable retry key; defaults to the request ID").option("--yes", "confirm the update without an interactive prompt").action(async (postId, local) => {
1498
1570
  const global = program.opts();
1499
1571
  commandExitCode = await execute({
1500
1572
  command: "posts update",
@@ -1506,24 +1578,34 @@ async function runCli(argv = process.argv, overrides = {}) {
1506
1578
  global,
1507
1579
  runtime,
1508
1580
  preview: [
1509
- `Replace draft ${postId}?`,
1581
+ `Update draft ${postId}?`,
1510
1582
  `Project: ${local.project}`,
1511
1583
  `Expected version: ${local.expectedVersion}`,
1512
1584
  "This will not schedule or publish the post."
1513
1585
  ].join("\n")
1514
1586
  });
1587
+ if (local.clearPlatforms && local.platform !== void 0) {
1588
+ throw new TypeError("Use either --platform or --clear-platforms, not both.");
1589
+ }
1590
+ if (local.clearMedia && local.media !== void 0) {
1591
+ throw new TypeError("Use either --media or --clear-media, not both.");
1592
+ }
1593
+ const captionOverrides = {
1594
+ ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1595
+ ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1596
+ ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1597
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1598
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1599
+ };
1600
+ const resolvedTikTokSettings = tiktokSettings(local);
1601
+ const resolvedYouTubeSettings = youtubeSettings(local);
1515
1602
  const input = {
1516
- caption: local.caption,
1517
- platforms: local.platform,
1518
- mediaAssetIds: local.media,
1519
- captionOverrides: {
1520
- ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1521
- ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1522
- ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1523
- ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1524
- },
1525
- tiktokSettings: null,
1526
- youtubeSettings: youtubeSettings(local)
1603
+ ...local.caption === void 0 ? {} : { caption: local.caption },
1604
+ ...local.platform === void 0 && !local.clearPlatforms ? {} : { platforms: local.clearPlatforms ? [] : local.platform },
1605
+ ...local.media === void 0 && !local.clearMedia ? {} : { mediaAssetIds: local.clearMedia ? [] : local.media },
1606
+ ...Object.keys(captionOverrides).length ? { captionOverrides } : {},
1607
+ ...resolvedTikTokSettings === void 0 ? {} : { tiktokSettings: resolvedTikTokSettings },
1608
+ ...resolvedYouTubeSettings === null ? {} : { youtubeSettings: resolvedYouTubeSettings }
1527
1609
  };
1528
1610
  const post = await client.posts.update(local.project, postId, input, {
1529
1611
  requestId: context.requestId,
@@ -1661,7 +1743,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1661
1743
  });
1662
1744
  });
1663
1745
  const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1664
- publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, or BLUESKY; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1746
+ publish.command("preview").description("Validate exact destinations and issue a five-minute confirmation").argument("<post-id>").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--expected-version <number>", "current post version from posts show", positiveInteger).requiredOption("--platform <provider>", "INSTAGRAM, TIKTOK, YOUTUBE, BLUESKY, or LINKEDIN; repeat as needed", collectPublishingProvider).action(async (postId, local) => {
1665
1747
  const global = program.opts();
1666
1748
  commandExitCode = await execute({
1667
1749
  command: "posts publish preview",
@@ -1744,6 +1826,27 @@ async function runCli(argv = process.argv, overrides = {}) {
1744
1826
  }
1745
1827
  });
1746
1828
  });
1829
+ integrations.command("delivery-options").description("Refresh secret-safe creator delivery choices for one provider").requiredOption("--project <project-id>", "exact Rolino project ID").requiredOption("--provider <provider>", "publishing provider; TIKTOK or LINKEDIN", deliveryOptionsProvider).action(async (local) => {
1830
+ const global = program.opts();
1831
+ commandExitCode = await execute({
1832
+ command: "integrations delivery-options",
1833
+ global,
1834
+ runtime,
1835
+ async action(context, client) {
1836
+ const options = await client.integrations.deliveryOptions(
1837
+ local.project,
1838
+ local.provider,
1839
+ { requestId: context.requestId }
1840
+ );
1841
+ writeSuccess(
1842
+ context,
1843
+ options,
1844
+ formatProviderDeliveryOptions(options),
1845
+ [`rolino posts create --project ${local.project} --caption <text> --platform ${local.provider} --agent --yes`]
1846
+ );
1847
+ }
1848
+ });
1849
+ });
1747
1850
  const calendar = program.command("calendar").description("Read scheduled publishing events");
1748
1851
  calendar.command("list").description("List scheduled posts in a bounded calendar window").requiredOption("--project <project-id>", "exact Rolino project ID").option("--from <datetime>", "inclusive ISO-8601 window start", isoDateTime).option("--to <datetime>", "exclusive ISO-8601 window end", isoDateTime).option("--limit <number>", "maximum events to return", (value) => {
1749
1852
  const parsed = Number(value);