@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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @rolino/cli
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5267911: Add LinkedIn member publishing as a gated provider across public contracts, SDK, CLI, and MCP workflows.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [5267911]
12
+ - @rolino/contracts@0.3.0
13
+ - @rolino/sdk@0.3.0
14
+ - @rolino/local-auth@0.3.0
15
+
16
+ ## 0.2.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 7d34580: Add account-specific TikTok delivery options, explicit settings review, safe partial draft updates, provider-keyed post settings, and streamed media uploads with server-verified metadata.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [7d34580]
25
+ - @rolino/contracts@0.2.0
26
+ - @rolino/sdk@0.2.0
27
+ - @rolino/local-auth@0.2.0
28
+
3
29
  ## 0.1.0
4
30
 
5
31
  ### Minor Changes
package/README.md CHANGED
@@ -6,9 +6,9 @@ Network requests go through `@rolino/sdk`, which uses the Node.js native
6
6
  `fetch` implementation; the CLI does not depend on Axios or another HTTP
7
7
  client package.
8
8
 
9
- ## Installation
10
-
11
- The supported installation keeps the CLI independent from MCP:
9
+ ## Installation
10
+
11
+ The supported installation keeps the CLI independent from MCP:
12
12
 
13
13
  ```bash
14
14
  npm install --global @rolino/cli
@@ -36,19 +36,20 @@ rolino setup mcp --client claude-code [--scope user|project]
36
36
  rolino whoami
37
37
  rolino projects create --name NAME [--type PRODUCT|SERVICE|PERSONAL|COMPANY] [--website URL] [--description TEXT] [--idempotency-key KEY] --yes
38
38
  rolino projects list [--limit 50] [--cursor ID]
39
- rolino projects show PROJECT_ID
40
- rolino media list --project PROJECT_ID [--type IMAGE|VIDEO] [--query TEXT] [--limit 50] [--cursor ID]
41
- rolino media upload FILE_PATH --project PROJECT_ID --yes
42
- rolino posts create --project PROJECT_ID --caption TEXT [--platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY] [--media ASSET_ID] --yes
43
- rolino posts update POST_ID --project PROJECT_ID --expected-version VERSION --caption TEXT [--platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY] [--media ASSET_ID] --yes
39
+ rolino projects show PROJECT_ID
40
+ rolino media list --project PROJECT_ID [--type IMAGE|VIDEO] [--query TEXT] [--limit 50] [--cursor ID]
41
+ rolino media upload FILE_PATH --project PROJECT_ID --yes
42
+ rolino posts create --project PROJECT_ID --caption TEXT [--platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY|LINKEDIN] [--media ASSET_ID] --yes
43
+ rolino posts update POST_ID --project PROJECT_ID --expected-version VERSION [--caption TEXT] [--platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY|LINKEDIN] [--media ASSET_ID] [--clear-platforms] [--clear-media] --yes
44
44
  rolino posts list --project PROJECT_ID [--limit 50] [--cursor ID] [--status STATUS]
45
45
  rolino posts show POST_ID --project PROJECT_ID
46
46
  rolino posts readiness POST_ID --project PROJECT_ID
47
47
  rolino posts schedule preview POST_ID --project PROJECT_ID --expected-version VERSION --at ISO_DATE --timezone IANA_TIMEZONE
48
48
  rolino posts schedule execute POST_ID --project PROJECT_ID --expected-version VERSION --at ISO_DATE --timezone IANA_TIMEZONE --confirmation-token TOKEN [--idempotency-key KEY]
49
- rolino posts publish preview POST_ID --project PROJECT_ID --expected-version VERSION --platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY
50
- rolino posts publish execute POST_ID --project PROJECT_ID --expected-version VERSION --platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY --confirmation-token TOKEN [--idempotency-key KEY]
49
+ rolino posts publish preview POST_ID --project PROJECT_ID --expected-version VERSION --platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY|LINKEDIN
50
+ rolino posts publish execute POST_ID --project PROJECT_ID --expected-version VERSION --platform INSTAGRAM|TIKTOK|YOUTUBE|BLUESKY|LINKEDIN --confirmation-token TOKEN [--idempotency-key KEY]
51
51
  rolino integrations health --project PROJECT_ID
52
+ rolino integrations delivery-options --project PROJECT_ID --provider TIKTOK|LINKEDIN
52
53
  rolino calendar list --project PROJECT_ID [--from ISO_DATE --to ISO_DATE] [--limit 50] [--cursor CURSOR]
53
54
  rolino doctor
54
55
  ```
@@ -78,17 +79,21 @@ directory. Existing file-backed configuration is backed up with a
78
79
  `.rolino-backup` suffix. Use `--dry-run` to preview in scripts and `--yes` to
79
80
  provide explicit non-interactive consent; `--agent` never implies consent.
80
81
 
81
- Draft creation and update never schedule or publish. Both require interactive
82
+ Draft creation and update never schedule or publish. Updates change only the
83
+ fields supplied; omitted media, destinations, captions, and provider settings
84
+ are preserved. Both require interactive
82
85
  confirmation or `--yes`; non-interactive and agent-mode calls fail before an
83
86
  HTTP request unless `--yes` is present. Retries use `--idempotency-key` (the
84
87
  request ID by default), and updates require the current `--expected-version`
85
- shown by `posts show` so stale automation cannot overwrite a newer edit.
86
-
87
- Media uploads use the deployment's existing local, S3-compatible, or
88
- Cloudflare R2 storage. The CLI validates the local file, requests a short-lived
89
- signed upload, sends bytes directly to storage, and registers the reusable
90
- asset only after the stored size and content type match. Uploading requires
91
- interactive confirmation or `--yes` and never creates or schedules a post.
88
+ shown by `posts show` so stale automation cannot overwrite a newer edit.
89
+
90
+ Media uploads use the deployment's existing local, S3-compatible, or
91
+ Cloudflare R2 storage. The CLI validates the local file, requests a short-lived
92
+ signed upload, streams the file directly to storage, and registers the reusable
93
+ asset only after the stored size and content type match. Rolino derives image
94
+ dimensions and video duration from bounded reads of the stored object rather
95
+ than trusting caller metadata. Uploading requires
96
+ interactive confirmation or `--yes` and never creates or schedules a post.
92
97
 
93
98
  YouTube drafts require exactly one stored video and explicit settings. Supply
94
99
  `--youtube-title`, `--youtube-category-id`, `--youtube-privacy`, and
@@ -101,19 +106,47 @@ description; otherwise Rolino uses the shared caption. For example:
101
106
  rolino posts create --project PROJECT_ID --caption "Launch description" --platform YOUTUBE --media VIDEO_ASSET_ID --youtube-title "Launch video" --youtube-category-id 22 --youtube-privacy PRIVATE --youtube-made-for-kids no --youtube-tag launch --yes
102
107
  ```
103
108
 
104
- Google may restrict uploads from unaudited API projects to Private. Rolino does
105
- not infer audit approval from configuration. A scheduled YouTube post must
106
- target Public; confirmed scheduling starts its private resumable upload
107
- immediately so processing can finish before the target time.
108
-
109
- Bluesky drafts can be text-only or include up to four stored JPEG, PNG, or WebP
110
- images. Use `--bluesky-caption` for a destination-specific override of up to
111
- 300 graphemes:
112
-
113
- ```text
114
- rolino posts create --project PROJECT_ID --caption "Shared launch" --platform BLUESKY --bluesky-caption "Launch day 🦋" --yes
115
- ```
116
-
109
+ Google may restrict uploads from unaudited API projects to Private. Rolino does
110
+ not infer audit approval from configuration. A scheduled YouTube post must
111
+ target Public; confirmed scheduling starts its private resumable upload
112
+ immediately so processing can finish before the target time.
113
+
114
+ TikTok drafts require account-specific choices. Refresh them immediately before
115
+ preparing the draft:
116
+
117
+ ```text
118
+ rolino integrations delivery-options --project PROJECT_ID --provider TIKTOK
119
+ ```
120
+
121
+ For direct publishing, pass the exact returned visibility and explicit yes/no
122
+ choices for comments, duet, stitch, commercial content, own-brand promotion,
123
+ third-party promotion, and AI-generated content. Use `--tiktok-mode inbox` for
124
+ a mobile inbox handoff. Add `--tiktok-settings-reviewed` only after reviewing
125
+ those fresh choices; it is deliberately separate from `--yes`, which confirms
126
+ the Rolino draft write. Editing TikTok caption, media, destinations, or its
127
+ override clears the prior settings review.
128
+
129
+ Bluesky drafts can be text-only or include up to four stored JPEG, PNG, or WebP
130
+ images. Use `--bluesky-caption` for a destination-specific override of up to
131
+ 300 graphemes:
132
+
133
+ ```text
134
+ rolino posts create --project PROJECT_ID --caption "Shared launch" --platform BLUESKY --bluesky-caption "Launch day 🦋" --yes
135
+ ```
136
+
137
+ LinkedIn drafts can be text-only or include up to ten stored JPEG/PNG images.
138
+ Refresh the connected member and server policy before preparing the draft, and
139
+ use `--linkedin-caption` for a destination override:
140
+
141
+ ```text
142
+ rolino integrations delivery-options --project PROJECT_ID --provider LINKEDIN
143
+ rolino posts create --project PROJECT_ID --caption "Shared launch" --platform LINKEDIN --linkedin-caption "Launch day for our LinkedIn network" --media IMAGE_ASSET_ID --yes
144
+ ```
145
+
146
+ The connected OAuth member is the author; the developer app's associated
147
+ Company Page is not a publishing target. If final post creation has an unknown
148
+ outcome, inspect that member's LinkedIn feed before retrying.
149
+
117
150
  Project creation also requires interactive confirmation or `--yes`. It
118
151
  atomically saves a project and its paired brand, but does not start research,
119
152
  connect accounts, schedule posts, or publish. A fresh CLI invocation generates
@@ -147,13 +180,14 @@ arguments. YouTube remains `PREPARING` while its resumable upload and provider
147
180
  processing are incomplete; only provider-confirmed visibility becomes
148
181
  published.
149
182
 
150
- The CLI cannot enable YouTube or Bluesky, or infer Google/YouTube approval.
151
- Those are server-side deployment controls. Self-hosted operators should
152
- complete the
153
- [YouTube operations guide](https://github.com/deifos/rolino/blob/main/docs/integrations/youtube.md)
154
- and
155
- [Bluesky operations guide](https://github.com/deifos/rolino/blob/main/docs/integrations/bluesky.md)
156
- before live testing.
183
+ The CLI cannot enable YouTube, Bluesky, or LinkedIn, or infer provider approval.
184
+ Those are server-side deployment controls. Self-hosted operators should
185
+ complete the
186
+ [YouTube operations guide](https://github.com/deifos/rolino/blob/main/docs/integrations/youtube.md)
187
+ and
188
+ [Bluesky operations guide](https://github.com/deifos/rolino/blob/main/docs/integrations/bluesky.md), and
189
+ [LinkedIn operations guide](https://github.com/deifos/rolino/blob/main/docs/integrations/linkedin.md)
190
+ before live testing.
157
191
 
158
192
  Configuration is read from `ROLINO_URL`, `ROLINO_TOKEN`, `ROLINO_CONFIG_DIR`,
159
193
  `ROLINO_OUTPUT`, `ROLINO_TIMEOUT`, and the short-lived
@@ -183,8 +217,8 @@ Stable exit codes:
183
217
 
184
218
  ## Support, security, and license
185
219
 
186
- Stable releases are published only after the maintainer approves the release
187
- gate. Report bugs through the
220
+ Stable releases are published only after the maintainer approves the release
221
+ gate. Report bugs through the
188
222
  [Rolino issue tracker](https://github.com/deifos/rolino/issues) and security
189
223
  issues through the repository's
190
224
  [security policy](https://github.com/deifos/rolino/security/policy).
package/dist/bin.cjs CHANGED
@@ -25,6 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli.ts
27
27
  var import_node_crypto2 = require("crypto");
28
+ var import_node_fs2 = require("fs");
28
29
  var import_promises2 = require("fs/promises");
29
30
  var import_node_path2 = require("path");
30
31
  var import_promises3 = require("readline/promises");
@@ -33,7 +34,7 @@ var import_commander = require("commander");
33
34
  // package.json
34
35
  var package_default = {
35
36
  name: "@rolino/cli",
36
- version: "0.1.0",
37
+ version: "0.3.0",
37
38
  description: "Agent-friendly command-line interface for Rolino",
38
39
  type: "module",
39
40
  license: "MIT",
@@ -90,9 +91,9 @@ var package_default = {
90
91
  dev: "tsx src/bin.ts"
91
92
  },
92
93
  dependencies: {
93
- "@rolino/contracts": "0.1.0",
94
- "@rolino/local-auth": "0.1.0",
95
- "@rolino/sdk": "0.1.0",
94
+ "@rolino/contracts": "0.3.0",
95
+ "@rolino/local-auth": "0.3.0",
96
+ "@rolino/sdk": "0.3.0",
96
97
  commander: "^15.0.0",
97
98
  open: "^11.0.0"
98
99
  },
@@ -476,7 +477,13 @@ function formatPost(post) {
476
477
  ] : [],
477
478
  ...post.publishedAt ? [`Published: ${post.publishedAt}`] : [],
478
479
  `Media: ${post.media.length}`,
479
- `Destinations: ${post.destinations.map((destination) => `${destination.provider}:${queued && destination.status === "SCHEDULED" ? "QUEUED" : destination.status}`).join(", ") || "none"}`
480
+ `Destinations: ${post.destinations.map((destination) => `${destination.provider}:${queued && destination.status === "SCHEDULED" ? "QUEUED" : destination.status}`).join(", ") || "none"}`,
481
+ ...post.providerSettings?.TIKTOK ? [
482
+ `TikTok: ${post.providerSettings.TIKTOK.postMode}, ${post.providerSettings.TIKTOK.privacyLevel ?? "visibility not set"}, settings ${post.providerSettings.TIKTOK.consentedAt ? "reviewed" : "not reviewed"}`
483
+ ] : [],
484
+ ...post.providerSettings?.YOUTUBE ? [
485
+ `YouTube: ${post.providerSettings.YOUTUBE.privacyStatus}, ${post.providerSettings.YOUTUBE.title}`
486
+ ] : []
480
487
  ].join("\n");
481
488
  }
482
489
  function formatPostSchedulePending(pending) {
@@ -490,6 +497,28 @@ function formatIntegrationHealth(data) {
490
497
  const rows = data.items.map((item) => `${item.provider} ${item.connected ? "connected" : "disconnected"} ${item.health.status} ${item.health.isStale ? "stale" : "current"} ${item.health.message}`);
491
498
  return ["PROVIDER CONNECTION HEALTH FRESHNESS DETAIL", ...rows].join("\n");
492
499
  }
500
+ function formatProviderDeliveryOptions(options) {
501
+ switch (options.provider) {
502
+ case "TIKTOK":
503
+ return [
504
+ `TikTok delivery options for ${options.account.displayName ?? options.account.username ?? "connected account"}`,
505
+ `Checked: ${options.checkedAt}`,
506
+ `Health: ${options.health.status} (${options.health.code}) ${options.health.message}`,
507
+ `Modes: ${options.allowedPostModes.join(", ") || "none"}`,
508
+ `Visibility: ${options.visibilityOptions.join(", ") || "none"}`,
509
+ `Interactions: comments ${options.interactions.comment.available ? "available" : "disabled"}, duet ${options.interactions.duet.available ? "available" : "disabled"}, stitch ${options.interactions.stitch.available ? "available" : "disabled"}`,
510
+ `Maximum video duration: ${options.maxVideoDurationMs === null ? "unknown" : `${options.maxVideoDurationMs} ms`}`
511
+ ].join("\n");
512
+ case "LINKEDIN":
513
+ return [
514
+ `LinkedIn delivery options for ${options.account.displayName ?? "connected member"}`,
515
+ `Checked: ${options.checkedAt}`,
516
+ `Health: ${options.health.status} (${options.health.code}) ${options.health.message}`,
517
+ `Post types: ${options.supportedPostTypes.join(", ")}`,
518
+ `Images: up to ${options.image.maxItems}; ${options.image.mimeTypes.join(", ")}; under ${options.image.maxPixelsExclusive.toLocaleString()} pixels`
519
+ ].join("\n");
520
+ }
521
+ }
493
522
  function formatPostReadiness(readiness) {
494
523
  const attention = readiness.checks.filter((check) => check.status !== "pass");
495
524
  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.";
@@ -834,7 +863,32 @@ function collectPublishingProvider(value, previous) {
834
863
  return [...previous ?? [], publishingProvider(value)];
835
864
  }
836
865
  function collectString(value, previous) {
837
- return [...previous, value];
866
+ return [...previous ?? [], value];
867
+ }
868
+ function deliveryOptionsProvider(value) {
869
+ const normalized = value.toUpperCase();
870
+ const parsed = import_contracts2.ProviderDeliveryOptionsProviderSchema.safeParse(normalized);
871
+ if (parsed.success) return parsed.data;
872
+ throw new import_commander.InvalidArgumentError(
873
+ `Provider must be one of ${import_contracts2.ProviderDeliveryOptionsProviderSchema.options.join(", ")}.`
874
+ );
875
+ }
876
+ function tiktokPostMode(value) {
877
+ const normalized = value.trim().toLowerCase();
878
+ if (normalized === "direct" || normalized === "direct_post") {
879
+ return "DIRECT_POST";
880
+ }
881
+ if (normalized === "inbox" || normalized === "media_upload") {
882
+ return "MEDIA_UPLOAD";
883
+ }
884
+ throw new import_commander.InvalidArgumentError("TikTok mode must be direct or inbox.");
885
+ }
886
+ function nonnegativeInteger(value) {
887
+ const parsed = Number(value);
888
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
889
+ throw new import_commander.InvalidArgumentError("Expected a nonnegative integer.");
890
+ }
891
+ return parsed;
838
892
  }
839
893
  function youtubePrivacy(value) {
840
894
  const normalized = value.toUpperCase();
@@ -852,7 +906,7 @@ function yesOrNo(value) {
852
906
  throw new import_commander.InvalidArgumentError("Expected yes or no.");
853
907
  }
854
908
  function youtubeSettings(options) {
855
- 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;
909
+ 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;
856
910
  if (!hasYouTubeOptions) return null;
857
911
  return import_contracts2.YouTubePostSettingsSchema.parse({
858
912
  title: options.youtubeTitle,
@@ -861,7 +915,7 @@ function youtubeSettings(options) {
861
915
  madeForKids: options.youtubeMadeForKids,
862
916
  containsSyntheticMedia: options.youtubeSyntheticMedia ?? false,
863
917
  notifySubscribers: options.youtubeNotifySubscribers ?? true,
864
- tags: options.youtubeTag
918
+ tags: options.youtubeTag ?? []
865
919
  });
866
920
  }
867
921
  function positiveInteger(value) {
@@ -1007,6 +1061,23 @@ function errorForOutput(error) {
1007
1061
  }
1008
1062
  return { code: "CLI_ERROR", message: "The command failed unexpectedly." };
1009
1063
  }
1064
+ function tiktokSettings(options) {
1065
+ 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;
1066
+ if (!hasTikTokOptions) return void 0;
1067
+ return import_contracts2.TikTokDraftSettingsSchema.parse({
1068
+ postMode: options.tiktokMode,
1069
+ privacyLevel: options.tiktokVisibility,
1070
+ allowComment: options.tiktokComments,
1071
+ allowDuet: options.tiktokDuet,
1072
+ allowStitch: options.tiktokStitch,
1073
+ commercialContentEnabled: options.tiktokCommercialContent,
1074
+ promotesOwnBrand: options.tiktokPromotesOwnBrand,
1075
+ promotesThirdParty: options.tiktokPromotesThirdParty,
1076
+ isAiGenerated: options.tiktokAiGenerated,
1077
+ videoCoverTimestampMs: options.tiktokCoverTimestampMs,
1078
+ reviewed: options.tiktokSettingsReviewed === true
1079
+ });
1080
+ }
1010
1081
  function recoverySuggestions(code, command) {
1011
1082
  switch (code) {
1012
1083
  case "AUTH_REQUIRED":
@@ -1210,8 +1281,8 @@ async function runCli(argv = process.argv, overrides = {}) {
1210
1281
  const contentType = contentTypes[(0, import_node_path2.extname)(absolutePath).toLowerCase()];
1211
1282
  if (!contentType) throw new TypeError("Use a JPEG, PNG, WebP, MP4, or MOV file.");
1212
1283
  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") });
1213
- const bytes = await (0, import_promises2.readFile)(absolutePath);
1214
- 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 });
1284
+ const body = await (0, import_node_fs2.openAsBlob)(absolutePath, { type: contentType });
1285
+ const asset = await client.media.upload(local.project, { fileName: (0, import_node_path2.basename)(absolutePath), contentType, fileSize: details.size, body }, { requestId: context.requestId });
1215
1286
  writeSuccess(context, asset, formatMediaAsset(asset), [`rolino posts create --project ${local.project} --caption <text> --media ${asset.id} --agent --yes`]);
1216
1287
  }
1217
1288
  });
@@ -1434,7 +1505,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1434
1505
  });
1435
1506
  });
1436
1507
  const posts = program.command("posts").description("Read and prepare posts in a Rolino project");
1437
- 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) => {
1508
+ 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) => {
1438
1509
  const global = program.opts();
1439
1510
  commandExitCode = await execute({
1440
1511
  command: "posts create",
@@ -1460,9 +1531,10 @@ async function runCli(argv = process.argv, overrides = {}) {
1460
1531
  ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1461
1532
  ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1462
1533
  ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1463
- ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1534
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1535
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1464
1536
  },
1465
- tiktokSettings: null,
1537
+ tiktokSettings: tiktokSettings(local) ?? null,
1466
1538
  youtubeSettings: youtubeSettings(local)
1467
1539
  };
1468
1540
  const post = await client.posts.create(local.project, input, {
@@ -1478,7 +1550,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1478
1550
  }
1479
1551
  });
1480
1552
  });
1481
- 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) => {
1553
+ 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) => {
1482
1554
  const global = program.opts();
1483
1555
  commandExitCode = await execute({
1484
1556
  command: "posts update",
@@ -1490,24 +1562,34 @@ async function runCli(argv = process.argv, overrides = {}) {
1490
1562
  global,
1491
1563
  runtime,
1492
1564
  preview: [
1493
- `Replace draft ${postId}?`,
1565
+ `Update draft ${postId}?`,
1494
1566
  `Project: ${local.project}`,
1495
1567
  `Expected version: ${local.expectedVersion}`,
1496
1568
  "This will not schedule or publish the post."
1497
1569
  ].join("\n")
1498
1570
  });
1571
+ if (local.clearPlatforms && local.platform !== void 0) {
1572
+ throw new TypeError("Use either --platform or --clear-platforms, not both.");
1573
+ }
1574
+ if (local.clearMedia && local.media !== void 0) {
1575
+ throw new TypeError("Use either --media or --clear-media, not both.");
1576
+ }
1577
+ const captionOverrides = {
1578
+ ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1579
+ ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1580
+ ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1581
+ ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption },
1582
+ ...local.linkedinCaption === void 0 ? {} : { LINKEDIN: local.linkedinCaption }
1583
+ };
1584
+ const resolvedTikTokSettings = tiktokSettings(local);
1585
+ const resolvedYouTubeSettings = youtubeSettings(local);
1499
1586
  const input = {
1500
- caption: local.caption,
1501
- platforms: local.platform,
1502
- mediaAssetIds: local.media,
1503
- captionOverrides: {
1504
- ...local.instagramCaption === void 0 ? {} : { INSTAGRAM: local.instagramCaption },
1505
- ...local.tiktokCaption === void 0 ? {} : { TIKTOK: local.tiktokCaption },
1506
- ...local.youtubeCaption === void 0 ? {} : { YOUTUBE: local.youtubeCaption },
1507
- ...local.blueskyCaption === void 0 ? {} : { BLUESKY: local.blueskyCaption }
1508
- },
1509
- tiktokSettings: null,
1510
- youtubeSettings: youtubeSettings(local)
1587
+ ...local.caption === void 0 ? {} : { caption: local.caption },
1588
+ ...local.platform === void 0 && !local.clearPlatforms ? {} : { platforms: local.clearPlatforms ? [] : local.platform },
1589
+ ...local.media === void 0 && !local.clearMedia ? {} : { mediaAssetIds: local.clearMedia ? [] : local.media },
1590
+ ...Object.keys(captionOverrides).length ? { captionOverrides } : {},
1591
+ ...resolvedTikTokSettings === void 0 ? {} : { tiktokSettings: resolvedTikTokSettings },
1592
+ ...resolvedYouTubeSettings === null ? {} : { youtubeSettings: resolvedYouTubeSettings }
1511
1593
  };
1512
1594
  const post = await client.posts.update(local.project, postId, input, {
1513
1595
  requestId: context.requestId,
@@ -1645,7 +1727,7 @@ async function runCli(argv = process.argv, overrides = {}) {
1645
1727
  });
1646
1728
  });
1647
1729
  const publish = posts.command("publish").description("Preview and queue server-confirmed immediate publishing");
1648
- 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) => {
1730
+ 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) => {
1649
1731
  const global = program.opts();
1650
1732
  commandExitCode = await execute({
1651
1733
  command: "posts publish preview",
@@ -1728,6 +1810,27 @@ async function runCli(argv = process.argv, overrides = {}) {
1728
1810
  }
1729
1811
  });
1730
1812
  });
1813
+ 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) => {
1814
+ const global = program.opts();
1815
+ commandExitCode = await execute({
1816
+ command: "integrations delivery-options",
1817
+ global,
1818
+ runtime,
1819
+ async action(context, client) {
1820
+ const options = await client.integrations.deliveryOptions(
1821
+ local.project,
1822
+ local.provider,
1823
+ { requestId: context.requestId }
1824
+ );
1825
+ writeSuccess(
1826
+ context,
1827
+ options,
1828
+ formatProviderDeliveryOptions(options),
1829
+ [`rolino posts create --project ${local.project} --caption <text> --platform ${local.provider} --agent --yes`]
1830
+ );
1831
+ }
1832
+ });
1833
+ });
1731
1834
  const calendar = program.command("calendar").description("Read scheduled publishing events");
1732
1835
  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) => {
1733
1836
  const parsed = Number(value);