@autoposting.ai/cli 0.5.0 → 0.5.2

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/cli.cjs CHANGED
@@ -132,7 +132,7 @@ var init_dist = __esm({
132
132
  );
133
133
  }
134
134
  };
135
- VERSION = "0.5.0";
135
+ VERSION = "0.5.2";
136
136
  AutopostingError = class extends Error {
137
137
  status;
138
138
  code;
@@ -222,6 +222,14 @@ var init_dist = __esm({
222
222
  authStatus(slug) {
223
223
  return this.get(`/brands/${slug}/auth/status`);
224
224
  }
225
+ /** Browser session owns beta checks, official authorization and explicit Page selection. */
226
+ async facebookConnection(slug) {
227
+ const brand = await this.retrieve(slug);
228
+ const url = new URL("/brands", this.client.baseUrl);
229
+ url.searchParams.set("brand", brand.slug);
230
+ url.searchParams.set("platform", "facebook");
231
+ return { url: url.toString(), brandSlug: brand.slug, platform: "facebook" };
232
+ }
225
233
  };
226
234
  CarouselsResource = class extends Resource {
227
235
  /** GET /carousels — backend paginates: `{ items, total, limit, offset }`. */
@@ -373,15 +381,20 @@ var init_dist = __esm({
373
381
  publish(id) {
374
382
  return this.client.request("POST", `/posts/${id}/publish`);
375
383
  }
376
- schedule(id, scheduledAt) {
377
- return this.client.request("PUT", `/posts/${id}/schedule`, { scheduledAt });
384
+ schedule(id, scheduledAt, platform) {
385
+ return this.client.request("PUT", `/posts/${id}/schedule`, { scheduledAt, ...platform ? { platform } : {} });
378
386
  }
379
- /** PUT /posts/:id/schedule with { cancel: true } — returns the post to draft. */
387
+ /** PUT /posts/:id/schedule with { cancel: true } — cancels scheduling; recovery restores the previous outcome. */
380
388
  unschedule(id) {
381
389
  return this.client.request("PUT", `/posts/${id}/schedule`, { cancel: true });
382
390
  }
383
- retry(id) {
384
- return this.client.request("POST", `/posts/${id}/retry`);
391
+ retry(id, platform) {
392
+ return this.client.request(
393
+ "POST",
394
+ `/posts/${id}/retry`,
395
+ void 0,
396
+ platform ? { platform } : void 0
397
+ );
385
398
  }
386
399
  rewrite(id) {
387
400
  return this.client.request("POST", `/posts/${id}/rewrite`);
@@ -808,7 +821,7 @@ var init_media_flags = __esm({
808
821
  "src/lib/media-flags.ts"() {
809
822
  "use strict";
810
823
  import_node_fs2 = require("fs");
811
- VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
824
+ VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube", "facebook"];
812
825
  EXT_TO_MIME = {
813
826
  jpg: "image/jpeg",
814
827
  jpeg: "image/jpeg",
@@ -823,7 +836,7 @@ var init_media_flags = __esm({
823
836
  });
824
837
 
825
838
  // src/mcp/tools.ts
826
- var MEDIA_ITEMS, ALL_TOOLS;
839
+ var MEDIA_ITEMS, FACEBOOK_AUTHORING, ALL_TOOLS;
827
840
  var init_tools = __esm({
828
841
  "src/mcp/tools.ts"() {
829
842
  "use strict";
@@ -842,6 +855,20 @@ var init_tools = __esm({
842
855
  additionalProperties: false
843
856
  }
844
857
  };
858
+ FACEBOOK_AUTHORING = {
859
+ facebookOptions: { type: "object", properties: {
860
+ format: { type: "string", enum: ["text", "link", "photo", "multi-photo", "video", "reel"] },
861
+ link: { type: "string", description: "Public HTTPS article URL for link format" }
862
+ }, required: ["format"], additionalProperties: false },
863
+ targetAccountIds: {
864
+ type: "object",
865
+ description: "Explicit selected connected Page IDs, including a sole Page",
866
+ properties: { facebook: { type: "array", items: { type: "string" }, minItems: 1, uniqueItems: true } },
867
+ additionalProperties: { type: "array", items: { type: "string" } }
868
+ },
869
+ platformTexts: { type: "object", additionalProperties: { type: "string" }, description: "Platform captions; empty Facebook caption is intentional" },
870
+ platformMedia: { type: "object", additionalProperties: MEDIA_ITEMS, description: "Platform media; empty Facebook array excludes shared attachments" }
871
+ };
845
872
  ALL_TOOLS = [
846
873
  { name: "delete-account-import", description: "Delete import progress only. Connected accounts and brands remain. Ask the user before discarding their progress.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Import id" } }, required: ["id"] } },
847
874
  { name: "import-accounts", description: "Import 1\u20131000 social account rows from CSV. Headers: brand_name, platform, account_url; optional brand_slug, timezone, account_type. Returns a durable import id. Repeating the same import reuses progress. No passwords or provider tokens.", inputSchema: { type: "object", properties: { csv: { type: "string", maxLength: 9e4, description: "CSV contents, including header row" } }, required: ["csv"] } },
@@ -889,14 +916,16 @@ var init_tools = __esm({
889
916
  brandSlug: { type: "string", description: "Brand slug to post under" },
890
917
  text: { type: "string", description: "Post text content" },
891
918
  platforms: {
892
- type: "string",
893
- description: "Comma-separated platform list (e.g. x,linkedin)"
919
+ type: ["string", "array"],
920
+ items: { type: "string" },
921
+ description: "Platform array or comma-separated platform list (e.g. x,linkedin)"
894
922
  },
895
923
  scheduledAt: {
896
924
  type: "string",
897
925
  description: "ISO 8601 datetime to schedule the post"
898
926
  },
899
- media: MEDIA_ITEMS
927
+ media: MEDIA_ITEMS,
928
+ ...FACEBOOK_AUTHORING
900
929
  },
901
930
  required: ["brandSlug", "text", "platforms"],
902
931
  additionalProperties: false
@@ -910,9 +939,10 @@ var init_tools = __esm({
910
939
  properties: {
911
940
  id: { type: "string", description: "Post ID" },
912
941
  text: { type: "string", description: "New post text" },
913
- platforms: { type: "string", description: "Comma-separated new platform list" },
942
+ platforms: { type: ["string", "array"], items: { type: "string" }, description: "Platform array or comma-separated new platform list" },
914
943
  scheduledAt: { type: "string", description: "New ISO 8601 scheduled datetime" },
915
- media: MEDIA_ITEMS
944
+ media: MEDIA_ITEMS,
945
+ ...FACEBOOK_AUTHORING
916
946
  },
917
947
  required: ["id"],
918
948
  additionalProperties: false
@@ -949,7 +979,8 @@ var init_tools = __esm({
949
979
  type: "object",
950
980
  properties: {
951
981
  id: { type: "string", description: "Post ID to schedule" },
952
- scheduledAt: { type: "string", description: "ISO 8601 datetime for publishing" }
982
+ scheduledAt: { type: "string", description: "ISO 8601 datetime for publishing" },
983
+ platform: { type: "string", enum: ["facebook"], description: "Explicit failed-Page recovery; preserves Published and Unknown outcomes" }
953
984
  },
954
985
  required: ["id", "scheduledAt"],
955
986
  additionalProperties: false
@@ -961,7 +992,8 @@ var init_tools = __esm({
961
992
  inputSchema: {
962
993
  type: "object",
963
994
  properties: {
964
- id: { type: "string", description: "Post ID to retry" }
995
+ id: { type: "string", description: "Post ID to retry" },
996
+ platform: { type: "string", enum: ["x", "linkedin", "instagram", "threads", "youtube", "facebook"], description: "Optional explicit platform; facebook retries only failed Pages without remote evidence, preserving Published and Unknown outcomes." }
965
997
  },
966
998
  required: ["id"],
967
999
  additionalProperties: false
@@ -1688,6 +1720,42 @@ var init_api_routes = __esm({
1688
1720
  });
1689
1721
 
1690
1722
  // src/mcp/handler.ts
1723
+ function validateAuthoringIntent(args) {
1724
+ const object = (value, field) => {
1725
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object.`);
1726
+ return value;
1727
+ };
1728
+ if (args.facebookOptions !== void 0) {
1729
+ const options = object(args.facebookOptions, "facebookOptions");
1730
+ if (!["text", "link", "photo", "multi-photo", "video", "reel"].includes(options.format)) {
1731
+ throw new Error("facebookOptions.format must be a supported Facebook format.");
1732
+ }
1733
+ if (options.link !== void 0 && typeof options.link !== "string") throw new Error("facebookOptions.link must be a string.");
1734
+ if (Object.keys(options).some((key) => !["format", "link"].includes(key))) throw new Error("Unknown facebookOptions field.");
1735
+ }
1736
+ for (const field of ["targetAccountIds", "platformTexts", "platformMedia"]) {
1737
+ if (args[field] === void 0) continue;
1738
+ for (const [platform, value] of Object.entries(object(args[field], field))) {
1739
+ if (!PLATFORM_SET.has(platform)) throw new Error(`Invalid platform in ${field}.`);
1740
+ if (field === "platformTexts") {
1741
+ if (typeof value !== "string") throw new Error("Platform captions must be strings.");
1742
+ } else if (field === "targetAccountIds") {
1743
+ if (!Array.isArray(value) || value.length === 0 || value.some((id) => typeof id !== "string" || !id.trim()) || new Set(value).size !== value.length) {
1744
+ throw new Error("Target account IDs must be a nonempty array of unique IDs.");
1745
+ }
1746
+ } else {
1747
+ if (!Array.isArray(value)) throw new Error("Platform media must be an array.");
1748
+ for (const item of value) {
1749
+ const media = object(item, "Media item");
1750
+ if (typeof media.url !== "string" || !media.url.trim() || !["image", "video", "gif"].includes(media.type)) {
1751
+ throw new Error("Media items require a URL and image, video or gif type.");
1752
+ }
1753
+ if (media.altText !== void 0 && typeof media.altText !== "string") throw new Error("Media altText must be a string.");
1754
+ }
1755
+ }
1756
+ }
1757
+ }
1758
+ }
1691
1759
  function ok(data) {
1692
1760
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1693
1761
  }
@@ -1696,8 +1764,15 @@ function fail(error) {
1696
1764
  return { content: [{ type: "text", text: message }], isError: true };
1697
1765
  }
1698
1766
  function parsePlatforms2(raw) {
1699
- if (typeof raw !== "string") return [];
1700
- const platforms = raw.split(",").map((p) => p.trim()).filter(Boolean);
1767
+ if (raw === void 0) return [];
1768
+ if (typeof raw !== "string" && !Array.isArray(raw)) {
1769
+ throw new Error("Platforms must be a comma-separated string or an array of platform names.");
1770
+ }
1771
+ const values = typeof raw === "string" ? raw.split(",") : raw;
1772
+ if (values.some((value) => typeof value !== "string")) {
1773
+ throw new Error("Every platform must be a platform name.");
1774
+ }
1775
+ const platforms = values.map((p) => p.trim()).filter(Boolean);
1701
1776
  const invalid = platforms.filter((platform) => !PLATFORM_SET.has(platform));
1702
1777
  if (invalid.length > 0) {
1703
1778
  throw new Error(
@@ -1727,6 +1802,7 @@ async function handleToolCall(name, args, client) {
1727
1802
  }
1728
1803
  }
1729
1804
  async function dispatchToolCall(name, args, client) {
1805
+ if (name === "create-post" || name === "update-post") validateAuthoringIntent(args);
1730
1806
  switch (name) {
1731
1807
  case "delete-account-import":
1732
1808
  return ok(await client.accountOnboarding.remove(args.id));
@@ -1760,18 +1836,26 @@ async function dispatchToolCall(name, args, client) {
1760
1836
  text: args.text,
1761
1837
  platforms: parsePlatforms2(args.platforms),
1762
1838
  ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {},
1763
- ...args.media ? { media: args.media } : {}
1839
+ ...args.media ? { media: args.media } : {},
1840
+ ...args.facebookOptions !== void 0 ? { facebookOptions: args.facebookOptions } : {},
1841
+ ...args.targetAccountIds !== void 0 ? { targetAccountIds: args.targetAccountIds } : {},
1842
+ ...args.platformTexts !== void 0 ? { platformTexts: args.platformTexts } : {},
1843
+ ...args.platformMedia !== void 0 ? { platformMedia: args.platformMedia } : {}
1764
1844
  });
1765
1845
  return ok(result);
1766
1846
  }
1767
1847
  case "update-post": {
1768
1848
  const result = await client.posts.update(args.id, {
1769
- ...args.text ? { text: args.text } : {},
1849
+ ...args.text !== void 0 ? { text: args.text } : {},
1770
1850
  ...args.platforms ? {
1771
1851
  platforms: parsePlatforms2(args.platforms)
1772
1852
  } : {},
1773
1853
  ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {},
1774
- ...args.media ? { media: args.media } : {}
1854
+ ...args.media ? { media: args.media } : {},
1855
+ ...args.facebookOptions !== void 0 ? { facebookOptions: args.facebookOptions } : {},
1856
+ ...args.targetAccountIds !== void 0 ? { targetAccountIds: args.targetAccountIds } : {},
1857
+ ...args.platformTexts !== void 0 ? { platformTexts: args.platformTexts } : {},
1858
+ ...args.platformMedia !== void 0 ? { platformMedia: args.platformMedia } : {}
1775
1859
  });
1776
1860
  return ok(result);
1777
1861
  }
@@ -1784,14 +1868,14 @@ async function dispatchToolCall(name, args, client) {
1784
1868
  return ok(result);
1785
1869
  }
1786
1870
  case "schedule-post": {
1787
- const result = await client.posts.schedule(
1788
- args.id,
1789
- args.scheduledAt
1790
- );
1871
+ if (args.platform !== void 0 && args.platform !== "facebook") throw new Error("Scheduled recovery requires platform facebook");
1872
+ const result = args.platform === "facebook" ? await client.posts.schedule(args.id, args.scheduledAt, "facebook") : await client.posts.schedule(args.id, args.scheduledAt);
1791
1873
  return ok(result);
1792
1874
  }
1793
1875
  case "retry-post": {
1794
- const result = await client.posts.retry(args.id);
1876
+ const platforms = args.platform === void 0 ? void 0 : parsePlatforms2(args.platform);
1877
+ if (platforms && platforms.length !== 1) throw new Error("Retry requires exactly one platform");
1878
+ const result = platforms ? await client.posts.retry(args.id, platforms[0]) : await client.posts.retry(args.id);
1795
1879
  return ok(result);
1796
1880
  }
1797
1881
  case "rewrite-post": {
@@ -2066,7 +2150,7 @@ async function dispatchToolCall(name, args, client) {
2066
2150
  const filePath = args.filePath;
2067
2151
  const filename = (0, import_node_path4.basename)(filePath);
2068
2152
  const uploaded = await client.media.upload({
2069
- data: new Uint8Array(await (0, import_promises3.readFile)(filePath)),
2153
+ data: new Uint8Array(await (0, import_promises4.readFile)(filePath)),
2070
2154
  filename,
2071
2155
  contentType: extToMime(filename)
2072
2156
  });
@@ -2107,11 +2191,11 @@ async function dispatchToolCall(name, args, client) {
2107
2191
  };
2108
2192
  }
2109
2193
  }
2110
- var import_promises3, import_node_path4, PLATFORMS, PLATFORM_SET, routeCache;
2194
+ var import_promises4, import_node_path4, PLATFORMS, PLATFORM_SET, routeCache;
2111
2195
  var init_handler = __esm({
2112
2196
  "src/mcp/handler.ts"() {
2113
2197
  "use strict";
2114
- import_promises3 = require("fs/promises");
2198
+ import_promises4 = require("fs/promises");
2115
2199
  import_node_path4 = require("path");
2116
2200
  init_api_routes();
2117
2201
  init_media_flags();
@@ -2120,7 +2204,8 @@ var init_handler = __esm({
2120
2204
  "linkedin",
2121
2205
  "instagram",
2122
2206
  "threads",
2123
- "youtube"
2207
+ "youtube",
2208
+ "facebook"
2124
2209
  ];
2125
2210
  PLATFORM_SET = new Set(PLATFORMS);
2126
2211
  routeCache = /* @__PURE__ */ new WeakMap();
@@ -2936,6 +3021,10 @@ function createBillingCommand() {
2936
3021
  const client = new Autoposting({ apiKey: cred.apiKey });
2937
3022
  const credits = await client.billing.credits();
2938
3023
  spinner.stop();
3024
+ if (!printer.isTty()) {
3025
+ printer.log(credits);
3026
+ return;
3027
+ }
2939
3028
  printer.table(
2940
3029
  [
2941
3030
  {
@@ -3129,6 +3218,28 @@ function buildAuthStatusRow(c, now = Date.now()) {
3129
3218
  }
3130
3219
  function createBrandsCommand() {
3131
3220
  const brands = new import_commander5.Command("brands").description("Manage brands");
3221
+ brands.command("connect-facebook <slug>").description("Open the app to connect Facebook Pages; sign in, check beta access and explicitly select Pages there").option("--no-browser", "Print the application link without opening a browser").action(async (slug, opts, cmd) => {
3222
+ const globals = cmd.optsWithGlobals();
3223
+ const printer = createPrinter(globals);
3224
+ try {
3225
+ const cred = resolveAuth({ apiKey: globals.apiKey });
3226
+ const client = new Autoposting({ apiKey: cred.apiKey });
3227
+ const handoff = await client.brands.facebookConnection(slug);
3228
+ printer.log(handoff);
3229
+ if (opts.browser) {
3230
+ try {
3231
+ const { default: open2 } = await import("open");
3232
+ await open2(handoff.url, { wait: true });
3233
+ } catch {
3234
+ printer.error(`Could not open the browser. Open ${handoff.url} manually, then check ap brands auth-status ${slug}.`);
3235
+ process.exitCode = 1;
3236
+ }
3237
+ }
3238
+ } catch (error) {
3239
+ printer.error(error);
3240
+ process.exitCode = exitCodeFromError(error);
3241
+ }
3242
+ });
3132
3243
  brands.command("list").description("List all brands").action(async (_opts, cmd) => {
3133
3244
  const globals = cmd.optsWithGlobals();
3134
3245
  const printer = createPrinter(globals);
@@ -3559,6 +3670,7 @@ function createClipsCommand() {
3559
3670
 
3560
3671
  // src/commands/posts.ts
3561
3672
  var import_commander8 = require("commander");
3673
+ var import_promises3 = require("fs/promises");
3562
3674
  init_dist();
3563
3675
 
3564
3676
  // src/lib/post-create.ts
@@ -3567,7 +3679,7 @@ var import_node_path3 = __toESM(require("path"));
3567
3679
  init_media_flags();
3568
3680
 
3569
3681
  // src/lib/account-select.ts
3570
- var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
3682
+ var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube", "facebook"];
3571
3683
  var FANOUT_CONFIRM_THRESHOLD = 5;
3572
3684
  var PAGED_PICKER_THRESHOLD = 8;
3573
3685
  var isAll = (v) => v === "all" || v === "*";
@@ -3630,24 +3742,28 @@ async function resolveTargetAccounts({
3630
3742
  }
3631
3743
  if (ids.length > 0) result[platform] = ids;
3632
3744
  } else if (specifiedValue !== void 0) {
3633
- const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
3634
- const match = accounts.find(
3635
- (a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
3636
- );
3637
- if (!match?.platformUserId) {
3638
- const valid = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
3639
- throw Object.assign(
3640
- new Error(
3641
- `--account: unknown ${platform} account "${specifiedValue}".
3745
+ const values = platform === "facebook" ? specifiedValue.split(",").map((value) => value.trim()) : [specifiedValue];
3746
+ for (const value of values) {
3747
+ if (!value) throw Object.assign(new Error("--account: Page selection must not contain empty members."), { exitCode: 1 });
3748
+ const normalized = value.startsWith("@") ? value.slice(1) : value;
3749
+ const match = accounts.find(
3750
+ (a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === value
3751
+ );
3752
+ if (!match?.platformUserId) {
3753
+ const valid = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
3754
+ throw Object.assign(
3755
+ new Error(
3756
+ `--account: unknown ${platform} account "${value}".
3642
3757
  Connected accounts:
3643
3758
  ${valid || " (none)"}`
3644
- ),
3645
- { exitCode: 1 }
3646
- );
3759
+ ),
3760
+ { exitCode: 1 }
3761
+ );
3762
+ }
3763
+ if (!result[platform]) result[platform] = [];
3764
+ if (!result[platform].includes(match.platformUserId)) result[platform].push(match.platformUserId);
3647
3765
  }
3648
- if (!result[platform]) result[platform] = [];
3649
- result[platform].push(match.platformUserId);
3650
- } else if (accounts.length >= 2) {
3766
+ } else if (accounts.length >= 2 || platform === "facebook" && accounts.length > 0) {
3651
3767
  if (isTty) {
3652
3768
  const { checkbox } = await import("@inquirer/prompts");
3653
3769
  const choices = accounts.map((a) => ({
@@ -3678,12 +3794,15 @@ Pass --account ${platform}=<handle|id> to select.`
3678
3794
  );
3679
3795
  }
3680
3796
  }
3797
+ if (platform === "facebook" && !result.facebook?.length) {
3798
+ throw Object.assign(new Error("Facebook requires at least one explicitly selected connected Page. Pass --account facebook=<handle|id>."), { exitCode: 1 });
3799
+ }
3681
3800
  }
3682
3801
  return result;
3683
3802
  }
3684
3803
 
3685
3804
  // src/lib/post-create.ts
3686
- var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
3805
+ var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube", "facebook"];
3687
3806
  function parsePlatforms(raw) {
3688
3807
  const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
3689
3808
  if (parts.length === 0) {
@@ -3711,21 +3830,73 @@ function validateScheduledAt(value) {
3711
3830
  }
3712
3831
  return value;
3713
3832
  }
3833
+ function validateCanonicalMedia(items) {
3834
+ for (const item of items) {
3835
+ if (!item || typeof item !== "object" || typeof item.url !== "string" || !["image", "video", "gif"].includes(item.type)) throw new Error("Media items require a URL and supported type");
3836
+ let url;
3837
+ try {
3838
+ url = new URL(item.url);
3839
+ } catch {
3840
+ throw new Error("Media URL must use HTTPS");
3841
+ }
3842
+ if (url.protocol !== "https:" || url.username || url.password) throw new Error("Media URL must use credential-free HTTPS");
3843
+ if (item.altText !== void 0 && typeof item.altText !== "string") throw new Error("Media altText must be a string");
3844
+ }
3845
+ }
3714
3846
  async function buildAndCreatePost(client, fields, opts) {
3715
3847
  const platforms = parsePlatforms(fields.platforms);
3716
3848
  const scheduledAt = fields.at ? validateScheduledAt(fields.at) : void 0;
3717
- if (fields.media && fields.media.length > 0) {
3718
- validateMediaCount(fields.media);
3719
- validateMediaPaths(fields.media);
3720
- validateMediaExtensions(fields.media);
3849
+ const sharedMedia = fields.media ?? [];
3850
+ if (!Array.isArray(sharedMedia)) throw new Error("media must be an array");
3851
+ const localMedia = sharedMedia.every((item) => typeof item === "string") ? sharedMedia : [];
3852
+ const sharedCanonicalMedia = localMedia.length || !sharedMedia.length ? [] : sharedMedia;
3853
+ validateCanonicalMedia(sharedCanonicalMedia);
3854
+ if (localMedia.length) {
3855
+ validateMediaCount(localMedia);
3856
+ validateMediaPaths(localMedia);
3857
+ validateMediaExtensions(localMedia);
3721
3858
  }
3722
3859
  const platformTexts = fields.platformText && fields.platformText.length > 0 ? parsePairs("--platform-text", fields.platformText) : void 0;
3723
- const platformMediaPaths = fields.platformMedia && fields.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", fields.platformMedia) : {};
3860
+ const suppliedMedia = fields.platformMedia;
3861
+ const canonicalMedia = {};
3862
+ if (suppliedMedia !== void 0 && !Array.isArray(suppliedMedia)) {
3863
+ if (!suppliedMedia || typeof suppliedMedia !== "object") throw new Error("platformMedia must be an object or local path flags");
3864
+ for (const [platform, items] of Object.entries(suppliedMedia)) {
3865
+ if (!VALID_PLATFORMS3.includes(platform) || !Array.isArray(items)) throw new Error("platformMedia requires supported platforms and media arrays");
3866
+ validateCanonicalMedia(items);
3867
+ canonicalMedia[platform] = items;
3868
+ }
3869
+ }
3870
+ const platformMediaPaths = Array.isArray(suppliedMedia) && suppliedMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", suppliedMedia) : {};
3724
3871
  for (const paths of Object.values(platformMediaPaths)) {
3725
3872
  validateMediaPaths(paths);
3726
3873
  validateMediaExtensions(paths);
3727
3874
  }
3728
- const altTexts = alignAltText(fields.media ?? [], fields.altText ?? []);
3875
+ let facebookOptions;
3876
+ if (platforms.includes("facebook")) {
3877
+ const format = fields.facebookFormat;
3878
+ if (!format || !["text", "link", "photo", "multi-photo", "video", "reel"].includes(format)) {
3879
+ throw new Error("--facebook-format must be text, link, photo, multi-photo, video or reel");
3880
+ }
3881
+ const paths = canonicalMedia.facebook ?? platformMediaPaths.facebook ?? fields.media ?? [];
3882
+ const images = paths.every((item) => typeof item === "string" ? extToMime(item).startsWith("image/") : item.type === "image");
3883
+ const videos = paths.every((item) => typeof item === "string" ? extToMime(item).startsWith("video/") : item.type === "video");
3884
+ if ((format === "text" || format === "link") && paths.length || format === "photo" && (paths.length !== 1 || !images) || format === "multi-photo" && (paths.length < 2 || !images) || (format === "video" || format === "reel") && (paths.length !== 1 || !videos)) {
3885
+ throw new Error("Facebook attachments do not match --facebook-format");
3886
+ }
3887
+ if (format === "text" && !(platformTexts?.facebook ?? fields.text).trim()) throw new Error("Facebook text format requires a caption");
3888
+ if (format === "link") {
3889
+ let link;
3890
+ try {
3891
+ link = new URL(fields.facebookLink ?? "");
3892
+ } catch {
3893
+ throw new Error("--facebook-link must be a public HTTPS URL");
3894
+ }
3895
+ if (link.protocol !== "https:" || link.username || link.password) throw new Error("--facebook-link must be a public HTTPS URL");
3896
+ }
3897
+ facebookOptions = { format, ...format === "link" ? { link: fields.facebookLink } : {} };
3898
+ }
3899
+ const altTexts = alignAltText(localMedia, fields.altText ?? []);
3729
3900
  const youtubeOptions = buildYoutubeOptions({
3730
3901
  ytTitle: fields.ytTitle,
3731
3902
  ytDescription: fields.ytDescription,
@@ -3764,6 +3935,7 @@ async function buildAndCreatePost(client, fields, opts) {
3764
3935
  ...instagramOptions ? { instagramOptions } : {},
3765
3936
  ...threadsOptions ? { threadsOptions } : {},
3766
3937
  ...youtubeOptions ? { youtubeOptions } : {},
3938
+ ...facebookOptions ? { facebookOptions } : {},
3767
3939
  source: "cli"
3768
3940
  };
3769
3941
  if (opts.dryRun) {
@@ -3772,19 +3944,19 @@ async function buildAndCreatePost(client, fields, opts) {
3772
3944
  request: {
3773
3945
  ...common,
3774
3946
  ...fields.media && fields.media.length > 0 ? {
3775
- media: fields.media.map((path3, i) => ({
3947
+ media: sharedCanonicalMedia.length ? sharedCanonicalMedia : localMedia.map((path3, i) => ({
3776
3948
  path: path3,
3777
3949
  ...altTexts[i] ? { altText: altTexts[i] } : {}
3778
3950
  }))
3779
3951
  } : {},
3780
- ...Object.keys(platformMediaPaths).length > 0 ? { platformMedia: platformMediaPaths } : {}
3952
+ ...Object.keys(platformMediaPaths).length + Object.keys(canonicalMedia).length > 0 ? { platformMedia: { ...canonicalMedia, ...platformMediaPaths } } : {}
3781
3953
  }
3782
3954
  };
3783
3955
  }
3784
3956
  opts.onBeforeNetwork?.();
3785
- const mediaInputs = [];
3786
- for (let i = 0; i < (fields.media ?? []).length; i++) {
3787
- const filePath = fields.media[i];
3957
+ const mediaInputs = [...sharedCanonicalMedia];
3958
+ for (let i = 0; i < localMedia.length; i++) {
3959
+ const filePath = localMedia[i];
3788
3960
  const data = await import_promises2.default.readFile(filePath);
3789
3961
  const filename = import_node_path3.default.basename(filePath);
3790
3962
  const uploaded = await client.media.upload({
@@ -3798,7 +3970,7 @@ async function buildAndCreatePost(client, fields, opts) {
3798
3970
  ...altTexts[i] ? { altText: altTexts[i] } : {}
3799
3971
  });
3800
3972
  }
3801
- const platformMediaResult = {};
3973
+ const platformMediaResult = { ...canonicalMedia };
3802
3974
  for (const [p, paths] of Object.entries(platformMediaPaths)) {
3803
3975
  const uploads = [];
3804
3976
  for (const filePath of paths) {
@@ -3890,16 +4062,44 @@ function rowToFields(row, cliBrand) {
3890
4062
  if (!brand) {
3891
4063
  throw new Error('No brand: set "brand" in the row, pass --brand, or set a default context.');
3892
4064
  }
3893
- if (!row.text) throw new Error('Missing "text".');
4065
+ if (typeof row.text !== "string") throw new Error('Missing "text" or text is not a string.');
3894
4066
  if (!platforms) throw new Error('Missing "platforms".');
3895
4067
  for (const key of ["media", "thread", "account", "altText"]) {
3896
4068
  if (row[key] !== void 0 && !Array.isArray(row[key])) {
3897
4069
  throw new Error(`"${key}" is multi-value \u2014 use a JSON input file with an array (CSV can't express it).`);
3898
4070
  }
3899
4071
  }
4072
+ let platformText = row.platformText;
4073
+ if (row.platformTexts !== void 0) {
4074
+ if (!row.platformTexts || typeof row.platformTexts !== "object" || Array.isArray(row.platformTexts)) throw new Error("platformTexts must map platforms to caption strings");
4075
+ const captions = Object.entries(row.platformTexts).map(([platform, text]) => {
4076
+ if (typeof text !== "string") throw new Error("platformTexts values must be strings");
4077
+ return `${platform}=${text}`;
4078
+ });
4079
+ platformText = platformText ?? captions;
4080
+ }
4081
+ let facebook;
4082
+ if (row.facebookOptions !== void 0) {
4083
+ if (!row.facebookOptions || typeof row.facebookOptions !== "object" || Array.isArray(row.facebookOptions)) throw new Error("facebookOptions must be an object");
4084
+ facebook = row.facebookOptions;
4085
+ if (typeof facebook.format !== "string" || facebook.link !== void 0 && typeof facebook.link !== "string") throw new Error("facebookOptions requires a string format and optional string link");
4086
+ }
4087
+ let account = row.account;
4088
+ if (row.targetAccountIds !== void 0) {
4089
+ if (!row.targetAccountIds || typeof row.targetAccountIds !== "object" || Array.isArray(row.targetAccountIds)) throw new Error("targetAccountIds must map platforms to account ID arrays");
4090
+ const flags = Object.entries(row.targetAccountIds).map(([platform, ids]) => {
4091
+ if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => typeof id !== "string" || !id.trim() || id.includes(","))) throw new Error("targetAccountIds requires nonempty account ID arrays");
4092
+ return `${platform}=${ids.join(",")}`;
4093
+ });
4094
+ account = account ?? flags;
4095
+ }
3900
4096
  return {
3901
4097
  ...row,
4098
+ ...facebook ? { facebookFormat: facebook.format, facebookLink: facebook.link } : {},
4099
+ ...account ? { account } : {},
4100
+ ...platformText ? { platformText } : {},
3902
4101
  brandSlug: brand,
4102
+ at: row.at ?? row.scheduledAt,
3903
4103
  text: String(row.text),
3904
4104
  platforms: String(platforms)
3905
4105
  };
@@ -3988,7 +4188,7 @@ function createPostsCommand() {
3988
4188
  posts.command("create").description("Create a new post").option("--brand <slug>", "Brand slug (defaults to the saved context: ap config set-context)").option("--text <text>", "Post text content (required unless --from)").option("--platforms <list>", "Comma-separated platforms, e.g. x,linkedin (required unless --from)").option("--from <file>", "Create posts in bulk from a CSV or JSON file (one post per row)").option("--at <iso>", "Schedule date/time (ISO 8601)").option(
3989
4189
  "--thread <text...>",
3990
4190
  "Additional posts appended after --text to form a thread (X/Threads only, max 25)"
3991
- ).option("--media <path...>", "Media file paths to attach (max 10)").option("--alt-text <text...>", "Alt text for --media files, aligned by index").option("--platform-text <pairs...>", "Per-platform text, e.g. x=Hello linkedin=World").option("--platform-media <pairs...>", "Per-platform media, e.g. instagram=photo.jpg x=banner.png").option("--yt-title <t>", "YouTube video title").option("--yt-description <d>", "YouTube video description").option("--yt-tags <list>", "YouTube tags (comma-separated)").option("--yt-privacy <v>", "YouTube privacy status (public|unlisted|private)").option("--yt-category <id>", "YouTube category ID").option("--yt-made-for-kids", "Mark YouTube video as made for kids").option("--ig-reel", "Post as an Instagram Reel").option("--ig-share-to-feed", "Share Reel to feed").option("--ig-cover-url <url>", "Reel cover image URL").option("--ig-thumb-offset-ms <n>", "Reel thumbnail offset in milliseconds").option("--ig-collaborators <list>", "Reel collaborator handles (comma-separated)").option("--threads-reply-to <id>", "Threads post ID to reply to").option("--threads-reply-control <v>", "Threads reply control (everyone|accounts_you_follow|mentioned_only)").option("--account <p=handle|id...>", "Target specific account per platform, e.g. x=@handle").option("--dry-run", "Print the resolved request body without uploading media or creating the post").option("--preview", "Alias for --dry-run").action(
4191
+ ).option("--media <path...>", "Media file paths to attach (max 10)").option("--alt-text <text...>", "Alt text for --media files, aligned by index").option("--platform-text <pairs...>", "Per-platform text, e.g. x=Hello linkedin=World").option("--platform-media <pairs...>", "Per-platform media, e.g. instagram=photo.jpg x=banner.png").option("--yt-title <t>", "YouTube video title").option("--yt-description <d>", "YouTube video description").option("--yt-tags <list>", "YouTube tags (comma-separated)").option("--yt-privacy <v>", "YouTube privacy status (public|unlisted|private)").option("--yt-category <id>", "YouTube category ID").option("--yt-made-for-kids", "Mark YouTube video as made for kids").option("--ig-reel", "Post as an Instagram Reel").option("--ig-share-to-feed", "Share Reel to feed").option("--ig-cover-url <url>", "Reel cover image URL").option("--ig-thumb-offset-ms <n>", "Reel thumbnail offset in milliseconds").option("--ig-collaborators <list>", "Reel collaborator handles (comma-separated)").option("--threads-reply-to <id>", "Threads post ID to reply to").option("--threads-reply-control <v>", "Threads reply control (everyone|accounts_you_follow|mentioned_only)").option("--facebook-format <format>", "Facebook Pages: text|link|photo|multi-photo|video|reel").option("--facebook-link <url>", "Public HTTPS article URL for Facebook link format").option("--account <p=handle|id...>", "Target specific account per platform, e.g. x=@handle; Facebook Page lists: facebook=page-id-1,page-id-2").option("--dry-run", "Print the resolved request body without uploading media or creating the post").option("--preview", "Alias for --dry-run").action(
3992
4192
  async (opts, cmd) => {
3993
4193
  const globals = cmd.optsWithGlobals();
3994
4194
  const printer = createPrinter(globals);
@@ -4054,7 +4254,7 @@ function createPostsCommand() {
4054
4254
  }
4055
4255
  }
4056
4256
  );
4057
- posts.command("update <id>").description("Update a post").option("--text <text>", "New post text").option("--platforms <list>", "New comma-separated platforms").option("--at <iso>", "New scheduled date/time (ISO 8601)").action(
4257
+ posts.command("update <id>").description("Update a post").option("--from <file>", "JSON update object with formats, target account IDs, captions and media URLs; explicit flags override file fields").option("--text <text>", "New post text").option("--platforms <list>", "New comma-separated platforms").option("--at <iso>", "New scheduled date/time (ISO 8601)").action(
4058
4258
  async (id, opts, cmd) => {
4059
4259
  const globals = cmd.optsWithGlobals();
4060
4260
  const printer = createPrinter(globals);
@@ -4062,8 +4262,26 @@ function createPostsCommand() {
4062
4262
  try {
4063
4263
  const cred = resolveAuth({ apiKey: globals.apiKey });
4064
4264
  const client = new Autoposting({ apiKey: cred.apiKey });
4265
+ let patch = {};
4266
+ if (opts.from) {
4267
+ const parsed = JSON.parse(await (0, import_promises3.readFile)(opts.from, "utf8"));
4268
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--from requires one JSON update object");
4269
+ const allowed = ["text", "platforms", "scheduledAt", "media", "platformMedia", "platformTexts", "targetAccountIds", "facebookOptions"];
4270
+ if (Object.keys(parsed).some((key) => !allowed.includes(key))) throw new Error("--from contains an unsupported update field");
4271
+ patch = parsed;
4272
+ if (patch.text !== void 0 && typeof patch.text !== "string") throw new Error("Update text must be a string");
4273
+ if (patch.platforms !== void 0) {
4274
+ if (!Array.isArray(patch.platforms) || patch.platforms.some((platform) => typeof platform !== "string" || !platform.trim() || platform.includes(","))) throw new Error("Update platforms must be an array of platform names");
4275
+ patch.platforms = parsePlatforms(patch.platforms.join(","));
4276
+ }
4277
+ if (patch.scheduledAt !== void 0) {
4278
+ if (typeof patch.scheduledAt !== "string") throw new Error("Update scheduledAt must be an ISO datetime string");
4279
+ patch.scheduledAt = validateScheduledAt(patch.scheduledAt);
4280
+ }
4281
+ }
4065
4282
  const post = await client.posts.update(id, {
4066
- ...opts.text ? { text: opts.text } : {},
4283
+ ...patch,
4284
+ ...opts.text !== void 0 ? { text: opts.text } : {},
4067
4285
  ...opts.platforms ? { platforms: parsePlatforms(opts.platforms) } : {},
4068
4286
  ...opts.at ? { scheduledAt: validateScheduledAt(opts.at) } : {}
4069
4287
  });
@@ -4115,7 +4333,7 @@ function createPostsCommand() {
4115
4333
  process.exit(resolveExitCode5(err));
4116
4334
  }
4117
4335
  });
4118
- posts.command("schedule <id>").description("Schedule a post for a specific time, or --cancel to unschedule it").option("--at <iso>", "ISO 8601 datetime to schedule the post").option("--cancel", "Unschedule the post (return it to draft)").action(async (id, opts, cmd) => {
4336
+ posts.command("schedule <id>").description("Schedule a post for a specific time, or --cancel to unschedule it").option("--at <iso>", "ISO 8601 datetime to schedule the post").option("--cancel", "Cancel scheduling").option("--platform <platform>", "facebook schedules only eligible failed Pages for recovery").action(async (id, opts, cmd) => {
4119
4337
  const globals = cmd.optsWithGlobals();
4120
4338
  const printer = createPrinter(globals);
4121
4339
  const spinner = printer.spinner(
@@ -4128,9 +4346,10 @@ function createPostsCommand() {
4128
4346
  if (!opts.cancel && !opts.at) {
4129
4347
  throw new Error("Provide either --at <iso> to schedule, or --cancel to unschedule.");
4130
4348
  }
4349
+ if (opts.platform && (opts.platform !== "facebook" || opts.cancel)) throw new Error("--platform facebook requires --at and cannot be used with --cancel.");
4131
4350
  const cred = resolveAuth({ apiKey: globals.apiKey });
4132
4351
  const client = new Autoposting({ apiKey: cred.apiKey });
4133
- const post = opts.cancel ? await client.posts.unschedule(id) : await client.posts.schedule(id, validateScheduledAt(opts.at));
4352
+ const post = opts.cancel ? await client.posts.unschedule(id) : opts.platform === "facebook" ? await client.posts.schedule(id, validateScheduledAt(opts.at), "facebook") : await client.posts.schedule(id, validateScheduledAt(opts.at));
4134
4353
  spinner.stop();
4135
4354
  printer.log(post);
4136
4355
  } catch (err) {
@@ -4139,14 +4358,16 @@ function createPostsCommand() {
4139
4358
  process.exit(resolveExitCode5(err));
4140
4359
  }
4141
4360
  });
4142
- posts.command("retry <id>").description("Retry a failed post").action(async (id, _opts, cmd) => {
4361
+ posts.command("retry <id>").description("Retry a failed post").option("--platform <platform>", "Select a platform; facebook retries only failed Pages without remote evidence").action(async (id, opts, cmd) => {
4143
4362
  const globals = cmd.optsWithGlobals();
4144
4363
  const printer = createPrinter(globals);
4145
4364
  const spinner = printer.spinner(`Retrying post "${id}"\u2026`);
4146
4365
  try {
4147
4366
  const cred = resolveAuth({ apiKey: globals.apiKey });
4148
4367
  const client = new Autoposting({ apiKey: cred.apiKey });
4149
- const post = await client.posts.retry(id);
4368
+ const platform = opts.platform ? parsePlatforms(opts.platform) : void 0;
4369
+ if (platform && platform.length !== 1) throw new Error("--platform requires exactly one platform");
4370
+ const post = platform ? await client.posts.retry(id, platform[0]) : await client.posts.retry(id);
4150
4371
  spinner.stop();
4151
4372
  printer.log(post);
4152
4373
  } catch (err) {
@@ -4479,6 +4700,10 @@ function createUsageCommand() {
4479
4700
  const client = new Autoposting({ apiKey: cred.apiKey });
4480
4701
  const summary = await client.usage.summary();
4481
4702
  spinner.stop();
4703
+ if (!printer.isTty()) {
4704
+ printer.log(summary);
4705
+ return;
4706
+ }
4482
4707
  printer.log(`Period: ${summary.range.from.slice(0, 10)} \u2192 ${summary.range.to.slice(0, 10)}`);
4483
4708
  printer.table(
4484
4709
  [