@autoposting.ai/cli 0.5.1 → 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.1";
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();
@@ -3133,6 +3218,28 @@ function buildAuthStatusRow(c, now = Date.now()) {
3133
3218
  }
3134
3219
  function createBrandsCommand() {
3135
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
+ });
3136
3243
  brands.command("list").description("List all brands").action(async (_opts, cmd) => {
3137
3244
  const globals = cmd.optsWithGlobals();
3138
3245
  const printer = createPrinter(globals);
@@ -3563,6 +3670,7 @@ function createClipsCommand() {
3563
3670
 
3564
3671
  // src/commands/posts.ts
3565
3672
  var import_commander8 = require("commander");
3673
+ var import_promises3 = require("fs/promises");
3566
3674
  init_dist();
3567
3675
 
3568
3676
  // src/lib/post-create.ts
@@ -3571,7 +3679,7 @@ var import_node_path3 = __toESM(require("path"));
3571
3679
  init_media_flags();
3572
3680
 
3573
3681
  // src/lib/account-select.ts
3574
- var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
3682
+ var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube", "facebook"];
3575
3683
  var FANOUT_CONFIRM_THRESHOLD = 5;
3576
3684
  var PAGED_PICKER_THRESHOLD = 8;
3577
3685
  var isAll = (v) => v === "all" || v === "*";
@@ -3634,24 +3742,28 @@ async function resolveTargetAccounts({
3634
3742
  }
3635
3743
  if (ids.length > 0) result[platform] = ids;
3636
3744
  } else if (specifiedValue !== void 0) {
3637
- const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
3638
- const match = accounts.find(
3639
- (a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
3640
- );
3641
- if (!match?.platformUserId) {
3642
- const valid = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
3643
- throw Object.assign(
3644
- new Error(
3645
- `--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}".
3646
3757
  Connected accounts:
3647
3758
  ${valid || " (none)"}`
3648
- ),
3649
- { exitCode: 1 }
3650
- );
3759
+ ),
3760
+ { exitCode: 1 }
3761
+ );
3762
+ }
3763
+ if (!result[platform]) result[platform] = [];
3764
+ if (!result[platform].includes(match.platformUserId)) result[platform].push(match.platformUserId);
3651
3765
  }
3652
- if (!result[platform]) result[platform] = [];
3653
- result[platform].push(match.platformUserId);
3654
- } else if (accounts.length >= 2) {
3766
+ } else if (accounts.length >= 2 || platform === "facebook" && accounts.length > 0) {
3655
3767
  if (isTty) {
3656
3768
  const { checkbox } = await import("@inquirer/prompts");
3657
3769
  const choices = accounts.map((a) => ({
@@ -3682,12 +3794,15 @@ Pass --account ${platform}=<handle|id> to select.`
3682
3794
  );
3683
3795
  }
3684
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
+ }
3685
3800
  }
3686
3801
  return result;
3687
3802
  }
3688
3803
 
3689
3804
  // src/lib/post-create.ts
3690
- var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
3805
+ var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube", "facebook"];
3691
3806
  function parsePlatforms(raw) {
3692
3807
  const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
3693
3808
  if (parts.length === 0) {
@@ -3715,21 +3830,73 @@ function validateScheduledAt(value) {
3715
3830
  }
3716
3831
  return value;
3717
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
+ }
3718
3846
  async function buildAndCreatePost(client, fields, opts) {
3719
3847
  const platforms = parsePlatforms(fields.platforms);
3720
3848
  const scheduledAt = fields.at ? validateScheduledAt(fields.at) : void 0;
3721
- if (fields.media && fields.media.length > 0) {
3722
- validateMediaCount(fields.media);
3723
- validateMediaPaths(fields.media);
3724
- 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);
3725
3858
  }
3726
3859
  const platformTexts = fields.platformText && fields.platformText.length > 0 ? parsePairs("--platform-text", fields.platformText) : void 0;
3727
- 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) : {};
3728
3871
  for (const paths of Object.values(platformMediaPaths)) {
3729
3872
  validateMediaPaths(paths);
3730
3873
  validateMediaExtensions(paths);
3731
3874
  }
3732
- 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 ?? []);
3733
3900
  const youtubeOptions = buildYoutubeOptions({
3734
3901
  ytTitle: fields.ytTitle,
3735
3902
  ytDescription: fields.ytDescription,
@@ -3768,6 +3935,7 @@ async function buildAndCreatePost(client, fields, opts) {
3768
3935
  ...instagramOptions ? { instagramOptions } : {},
3769
3936
  ...threadsOptions ? { threadsOptions } : {},
3770
3937
  ...youtubeOptions ? { youtubeOptions } : {},
3938
+ ...facebookOptions ? { facebookOptions } : {},
3771
3939
  source: "cli"
3772
3940
  };
3773
3941
  if (opts.dryRun) {
@@ -3776,19 +3944,19 @@ async function buildAndCreatePost(client, fields, opts) {
3776
3944
  request: {
3777
3945
  ...common,
3778
3946
  ...fields.media && fields.media.length > 0 ? {
3779
- media: fields.media.map((path3, i) => ({
3947
+ media: sharedCanonicalMedia.length ? sharedCanonicalMedia : localMedia.map((path3, i) => ({
3780
3948
  path: path3,
3781
3949
  ...altTexts[i] ? { altText: altTexts[i] } : {}
3782
3950
  }))
3783
3951
  } : {},
3784
- ...Object.keys(platformMediaPaths).length > 0 ? { platformMedia: platformMediaPaths } : {}
3952
+ ...Object.keys(platformMediaPaths).length + Object.keys(canonicalMedia).length > 0 ? { platformMedia: { ...canonicalMedia, ...platformMediaPaths } } : {}
3785
3953
  }
3786
3954
  };
3787
3955
  }
3788
3956
  opts.onBeforeNetwork?.();
3789
- const mediaInputs = [];
3790
- for (let i = 0; i < (fields.media ?? []).length; i++) {
3791
- const filePath = fields.media[i];
3957
+ const mediaInputs = [...sharedCanonicalMedia];
3958
+ for (let i = 0; i < localMedia.length; i++) {
3959
+ const filePath = localMedia[i];
3792
3960
  const data = await import_promises2.default.readFile(filePath);
3793
3961
  const filename = import_node_path3.default.basename(filePath);
3794
3962
  const uploaded = await client.media.upload({
@@ -3802,7 +3970,7 @@ async function buildAndCreatePost(client, fields, opts) {
3802
3970
  ...altTexts[i] ? { altText: altTexts[i] } : {}
3803
3971
  });
3804
3972
  }
3805
- const platformMediaResult = {};
3973
+ const platformMediaResult = { ...canonicalMedia };
3806
3974
  for (const [p, paths] of Object.entries(platformMediaPaths)) {
3807
3975
  const uploads = [];
3808
3976
  for (const filePath of paths) {
@@ -3894,16 +4062,44 @@ function rowToFields(row, cliBrand) {
3894
4062
  if (!brand) {
3895
4063
  throw new Error('No brand: set "brand" in the row, pass --brand, or set a default context.');
3896
4064
  }
3897
- 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.');
3898
4066
  if (!platforms) throw new Error('Missing "platforms".');
3899
4067
  for (const key of ["media", "thread", "account", "altText"]) {
3900
4068
  if (row[key] !== void 0 && !Array.isArray(row[key])) {
3901
4069
  throw new Error(`"${key}" is multi-value \u2014 use a JSON input file with an array (CSV can't express it).`);
3902
4070
  }
3903
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
+ }
3904
4096
  return {
3905
4097
  ...row,
4098
+ ...facebook ? { facebookFormat: facebook.format, facebookLink: facebook.link } : {},
4099
+ ...account ? { account } : {},
4100
+ ...platformText ? { platformText } : {},
3906
4101
  brandSlug: brand,
4102
+ at: row.at ?? row.scheduledAt,
3907
4103
  text: String(row.text),
3908
4104
  platforms: String(platforms)
3909
4105
  };
@@ -3992,7 +4188,7 @@ function createPostsCommand() {
3992
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(
3993
4189
  "--thread <text...>",
3994
4190
  "Additional posts appended after --text to form a thread (X/Threads only, max 25)"
3995
- ).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(
3996
4192
  async (opts, cmd) => {
3997
4193
  const globals = cmd.optsWithGlobals();
3998
4194
  const printer = createPrinter(globals);
@@ -4058,7 +4254,7 @@ function createPostsCommand() {
4058
4254
  }
4059
4255
  }
4060
4256
  );
4061
- 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(
4062
4258
  async (id, opts, cmd) => {
4063
4259
  const globals = cmd.optsWithGlobals();
4064
4260
  const printer = createPrinter(globals);
@@ -4066,8 +4262,26 @@ function createPostsCommand() {
4066
4262
  try {
4067
4263
  const cred = resolveAuth({ apiKey: globals.apiKey });
4068
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
+ }
4069
4282
  const post = await client.posts.update(id, {
4070
- ...opts.text ? { text: opts.text } : {},
4283
+ ...patch,
4284
+ ...opts.text !== void 0 ? { text: opts.text } : {},
4071
4285
  ...opts.platforms ? { platforms: parsePlatforms(opts.platforms) } : {},
4072
4286
  ...opts.at ? { scheduledAt: validateScheduledAt(opts.at) } : {}
4073
4287
  });
@@ -4119,7 +4333,7 @@ function createPostsCommand() {
4119
4333
  process.exit(resolveExitCode5(err));
4120
4334
  }
4121
4335
  });
4122
- 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) => {
4123
4337
  const globals = cmd.optsWithGlobals();
4124
4338
  const printer = createPrinter(globals);
4125
4339
  const spinner = printer.spinner(
@@ -4132,9 +4346,10 @@ function createPostsCommand() {
4132
4346
  if (!opts.cancel && !opts.at) {
4133
4347
  throw new Error("Provide either --at <iso> to schedule, or --cancel to unschedule.");
4134
4348
  }
4349
+ if (opts.platform && (opts.platform !== "facebook" || opts.cancel)) throw new Error("--platform facebook requires --at and cannot be used with --cancel.");
4135
4350
  const cred = resolveAuth({ apiKey: globals.apiKey });
4136
4351
  const client = new Autoposting({ apiKey: cred.apiKey });
4137
- 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));
4138
4353
  spinner.stop();
4139
4354
  printer.log(post);
4140
4355
  } catch (err) {
@@ -4143,14 +4358,16 @@ function createPostsCommand() {
4143
4358
  process.exit(resolveExitCode5(err));
4144
4359
  }
4145
4360
  });
4146
- 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) => {
4147
4362
  const globals = cmd.optsWithGlobals();
4148
4363
  const printer = createPrinter(globals);
4149
4364
  const spinner = printer.spinner(`Retrying post "${id}"\u2026`);
4150
4365
  try {
4151
4366
  const cred = resolveAuth({ apiKey: globals.apiKey });
4152
4367
  const client = new Autoposting({ apiKey: cred.apiKey });
4153
- 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);
4154
4371
  spinner.stop();
4155
4372
  printer.log(post);
4156
4373
  } catch (err) {