@autoposting.ai/cli 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -79,7 +79,7 @@ var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, V
79
79
  var init_dist = __esm({
80
80
  "../sdk/dist/index.mjs"() {
81
81
  "use strict";
82
- VERSION = "0.3.6";
82
+ VERSION = "0.4.0";
83
83
  AutopostingError = class extends Error {
84
84
  status;
85
85
  code;
@@ -245,8 +245,8 @@ var init_dist = __esm({
245
245
  throw new AutopostingError("brandId is required to upload a clip.", 0, "VALIDATION_ERROR");
246
246
  }
247
247
  const { open: open2, stat } = await import("fs/promises");
248
- const { basename } = await import("path");
249
- const fileName = basename(filePath);
248
+ const { basename: basename2 } = await import("path");
249
+ const fileName = basename2(filePath);
250
250
  const title = options.title ?? fileName;
251
251
  const mimeType = options.contentType ?? guessVideoMime(fileName);
252
252
  const { size: fileSize } = await stat(filePath);
@@ -663,6 +663,131 @@ var init_dist = __esm({
663
663
  }
664
664
  });
665
665
 
666
+ // src/lib/media-flags.ts
667
+ function extToMime(filename) {
668
+ const ext = filename.split(".").pop()?.toLowerCase() ?? "";
669
+ const mime = EXT_TO_MIME[ext];
670
+ if (!mime) {
671
+ throw new Error(
672
+ `Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
673
+ );
674
+ }
675
+ return mime;
676
+ }
677
+ function parsePairs(flag, vals) {
678
+ const result = {};
679
+ for (const val of vals) {
680
+ const eqIdx = val.indexOf("=");
681
+ if (eqIdx < 1) {
682
+ throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
683
+ }
684
+ const p = val.slice(0, eqIdx).trim();
685
+ const v = val.slice(eqIdx + 1);
686
+ if (!VALID_PLATFORMS.includes(p)) {
687
+ throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
688
+ }
689
+ result[p] = v;
690
+ }
691
+ return result;
692
+ }
693
+ function parsePlatformMediaPairs(flag, vals) {
694
+ const result = {};
695
+ for (const val of vals) {
696
+ const eqIdx = val.indexOf("=");
697
+ if (eqIdx < 1) {
698
+ throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
699
+ }
700
+ const p = val.slice(0, eqIdx).trim();
701
+ const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
702
+ if (!VALID_PLATFORMS.includes(p)) {
703
+ throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
704
+ }
705
+ result[p] = paths;
706
+ }
707
+ return result;
708
+ }
709
+ function validateMediaExtensions(paths) {
710
+ for (const p of paths) extToMime(p);
711
+ }
712
+ function validateMediaCount(paths) {
713
+ if (paths.length > 10) {
714
+ throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
715
+ }
716
+ }
717
+ function validateMediaPaths(paths) {
718
+ for (const p of paths) {
719
+ if (!(0, import_node_fs2.existsSync)(p)) {
720
+ throw new Error(`Media file not found: "${p}"`);
721
+ }
722
+ }
723
+ }
724
+ function alignAltText(mediaPaths, altTexts) {
725
+ if (altTexts.length > mediaPaths.length) {
726
+ throw new Error(
727
+ `--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
728
+ );
729
+ }
730
+ return mediaPaths.map((_, i) => altTexts[i]);
731
+ }
732
+ function buildYoutubeOptions(opts) {
733
+ const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
734
+ const o = {};
735
+ if (ytTitle) o.title = ytTitle;
736
+ if (ytDescription) o.description = ytDescription;
737
+ if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
738
+ if (ytPrivacy) o.privacyStatus = ytPrivacy;
739
+ if (ytCategory) o.categoryId = ytCategory;
740
+ if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
741
+ return Object.keys(o).length > 0 ? o : void 0;
742
+ }
743
+ function buildInstagramOptions(opts) {
744
+ const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
745
+ const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
746
+ if (!hasAnyReelFlag) return void 0;
747
+ const reel = {};
748
+ if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
749
+ if (igCoverUrl) reel.coverUrl = igCoverUrl;
750
+ if (igThumbOffsetMs !== void 0) {
751
+ const n = Number(igThumbOffsetMs);
752
+ if (!Number.isFinite(n) || n < 0) {
753
+ throw new Error(
754
+ `--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
755
+ );
756
+ }
757
+ reel.thumbOffsetMs = n;
758
+ }
759
+ if (igCollaborators) {
760
+ reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
761
+ }
762
+ return { reel };
763
+ }
764
+ function buildThreadsOptions(opts) {
765
+ const { threadsReplyTo, threadsReplyControl } = opts;
766
+ if (!threadsReplyTo && !threadsReplyControl) return void 0;
767
+ const o = {};
768
+ if (threadsReplyTo) o.replyToId = threadsReplyTo;
769
+ if (threadsReplyControl) o.replyControl = threadsReplyControl;
770
+ return o;
771
+ }
772
+ var import_node_fs2, VALID_PLATFORMS, EXT_TO_MIME;
773
+ var init_media_flags = __esm({
774
+ "src/lib/media-flags.ts"() {
775
+ "use strict";
776
+ import_node_fs2 = require("fs");
777
+ VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
778
+ EXT_TO_MIME = {
779
+ jpg: "image/jpeg",
780
+ jpeg: "image/jpeg",
781
+ png: "image/png",
782
+ gif: "image/gif",
783
+ webp: "image/webp",
784
+ mp4: "video/mp4",
785
+ mov: "video/quicktime",
786
+ webm: "video/webm"
787
+ };
788
+ }
789
+ });
790
+
666
791
  // src/mcp/tools.ts
667
792
  var MEDIA_ITEMS, ALL_TOOLS;
668
793
  var init_tools = __esm({
@@ -671,7 +796,7 @@ var init_tools = __esm({
671
796
  MEDIA_ITEMS = {
672
797
  type: "array",
673
798
  maxItems: 10,
674
- description: "Images or videos to attach, as public https URLs. To attach a local file, use `ap posts create --media <file>` instead.",
799
+ description: "Images or videos to attach, as https URLs. For a local file, call upload-media first and pass its result here.",
675
800
  items: {
676
801
  type: "object",
677
802
  properties: {
@@ -1221,6 +1346,39 @@ var init_tools = __esm({
1221
1346
  additionalProperties: false
1222
1347
  }
1223
1348
  },
1349
+ {
1350
+ name: "upload-clip",
1351
+ description: "Upload a local video file for AI clipping (multipart, any size). Poll get-clip until status is ready, then render-clip.",
1352
+ inputSchema: {
1353
+ type: "object",
1354
+ properties: {
1355
+ filePath: { type: "string", description: "Absolute path to the video file on this machine" },
1356
+ brandId: { type: "string", description: "Brand ID (the `id` from list-brands)" },
1357
+ title: { type: "string", description: "Optional clip title" }
1358
+ },
1359
+ required: ["filePath", "brandId"],
1360
+ additionalProperties: false
1361
+ }
1362
+ },
1363
+ {
1364
+ name: "publish-clip",
1365
+ description: "Publish, schedule, or save as a draft a rendered clip candidate.",
1366
+ inputSchema: {
1367
+ type: "object",
1368
+ properties: {
1369
+ id: { type: "string", description: "Clip ID" },
1370
+ mode: { type: "string", enum: ["publish", "schedule", "draft"] },
1371
+ candidateId: { type: "string", description: "Candidate to post (defaults to the best rendered one)" },
1372
+ platforms: { type: "string", description: 'Comma-separated platforms, e.g. "x,linkedin"' },
1373
+ text: { type: "string", description: "Post caption" },
1374
+ brandSlug: { type: "string", description: "Brand slug" },
1375
+ scheduledAt: { type: "string", description: 'ISO 8601 time, for mode "schedule"' },
1376
+ aspectRatio: { type: "string", enum: ["9:16", "4:5", "1:1", "16:9"] }
1377
+ },
1378
+ required: ["id", "mode"],
1379
+ additionalProperties: false
1380
+ }
1381
+ },
1224
1382
  {
1225
1383
  name: "delete-clip",
1226
1384
  description: "Permanently delete a video clip by ID.",
@@ -1233,6 +1391,45 @@ var init_tools = __esm({
1233
1391
  additionalProperties: false
1234
1392
  }
1235
1393
  },
1394
+ // Media
1395
+ {
1396
+ name: "upload-media",
1397
+ description: "Upload a local image or video and get back a media item ({url, type, altText}) to pass in create-post or update-post `media`.",
1398
+ inputSchema: {
1399
+ type: "object",
1400
+ properties: {
1401
+ filePath: { type: "string", description: "Absolute path to the file on this machine" },
1402
+ altText: { type: "string", description: "Optional alt text" }
1403
+ },
1404
+ required: ["filePath"],
1405
+ additionalProperties: false
1406
+ }
1407
+ },
1408
+ // Any other REST route
1409
+ {
1410
+ name: "list-api-routes",
1411
+ description: 'List the REST routes api-request may call, as "METHOD /path/:param". Optional prefix filter, e.g. "/clips".',
1412
+ inputSchema: {
1413
+ type: "object",
1414
+ properties: { prefix: { type: "string", description: "Only routes whose path starts with this" } },
1415
+ additionalProperties: false
1416
+ }
1417
+ },
1418
+ {
1419
+ name: "api-request",
1420
+ description: "Call any Autoposting REST route on the allow-list (see list-api-routes) with your API key. Replace :params with real IDs.",
1421
+ inputSchema: {
1422
+ type: "object",
1423
+ properties: {
1424
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
1425
+ path: { type: "string", description: "Path such as /clips/abc123/jobs, no query string" },
1426
+ query: { type: "object", description: "Query string parameters" },
1427
+ body: { type: "object", description: "JSON body for POST, PUT and PATCH" }
1428
+ },
1429
+ required: ["method", "path"],
1430
+ additionalProperties: false
1431
+ }
1432
+ },
1236
1433
  // Carousels
1237
1434
  {
1238
1435
  name: "list-carousels",
@@ -1415,6 +1612,40 @@ var init_tools = __esm({
1415
1612
  }
1416
1613
  });
1417
1614
 
1615
+ // src/lib/api-routes.ts
1616
+ async function fetchApiRoutes(client) {
1617
+ const res = await client.request("POST", "/mcp", {
1618
+ jsonrpc: "2.0",
1619
+ id: 1,
1620
+ method: "tools/call",
1621
+ params: { name: "list-api-routes", arguments: {} }
1622
+ });
1623
+ try {
1624
+ const routes = JSON.parse(res.result?.content?.[0]?.text ?? "").routes;
1625
+ if (Array.isArray(routes)) return routes;
1626
+ } catch {
1627
+ }
1628
+ throw new Error(`Could not load the API allow-list from the server${res.error?.message ? `: ${res.error.message}` : ""}.`);
1629
+ }
1630
+ function assertApiRoute(routes, method, path3) {
1631
+ const m = method.toUpperCase();
1632
+ const segs = path3.startsWith("/") ? path3.slice(1).split("/") : [];
1633
+ const safe = segs.length > 0 && segs.every((s) => SAFE_SEGMENT.test(s) && s !== "." && s !== "..");
1634
+ const match = safe && routes.some((r) => {
1635
+ const [rm, rp] = r.split(" ");
1636
+ const rs = rp.slice(1).split("/");
1637
+ return rm === m && rs.length === segs.length && rs.every((s, i) => s.startsWith(":") || s === segs[i]);
1638
+ });
1639
+ if (!match) throw new Error(`${m} ${path3} is not available. Run list-api-routes (or \`ap api --list\`) to see what is.`);
1640
+ }
1641
+ var SAFE_SEGMENT;
1642
+ var init_api_routes = __esm({
1643
+ "src/lib/api-routes.ts"() {
1644
+ "use strict";
1645
+ SAFE_SEGMENT = /^[\w\-.~]+$/;
1646
+ }
1647
+ });
1648
+
1418
1649
  // src/mcp/handler.ts
1419
1650
  function ok(data) {
1420
1651
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -1434,6 +1665,15 @@ function parsePlatforms2(raw) {
1434
1665
  }
1435
1666
  return platforms;
1436
1667
  }
1668
+ function apiRoutes(client) {
1669
+ let routes = routeCache.get(client);
1670
+ if (!routes) {
1671
+ routes = fetchApiRoutes(client);
1672
+ routeCache.set(client, routes);
1673
+ routes.catch(() => routeCache.delete(client));
1674
+ }
1675
+ return routes;
1676
+ }
1437
1677
  function parseEvents(raw) {
1438
1678
  if (typeof raw !== "string") return [];
1439
1679
  return raw.split(",").map((e) => e.trim());
@@ -1685,6 +1925,21 @@ async function dispatchToolCall(name, args, client) {
1685
1925
  });
1686
1926
  return ok(result);
1687
1927
  }
1928
+ case "upload-clip": {
1929
+ const result = await client.clips.upload(args.filePath, {
1930
+ brandId: args.brandId,
1931
+ ...args.title ? { title: args.title } : {}
1932
+ });
1933
+ return ok(result);
1934
+ }
1935
+ case "publish-clip": {
1936
+ const body = { mode: args.mode };
1937
+ for (const k of ["candidateId", "text", "brandSlug", "scheduledAt", "aspectRatio"]) {
1938
+ if (args[k] !== void 0) body[k] = args[k];
1939
+ }
1940
+ if (args.platforms !== void 0) body.platforms = parsePlatforms2(args.platforms);
1941
+ return ok(await client.request("POST", `/clips/${encodeURIComponent(args.id)}/publish`, body));
1942
+ }
1688
1943
  case "delete-clip": {
1689
1944
  await client.clips.remove(args.id);
1690
1945
  return ok({ deleted: true, id: args.id });
@@ -1753,6 +2008,31 @@ async function dispatchToolCall(name, args, client) {
1753
2008
  await client.webhooks.test(args.id);
1754
2009
  return ok({ sent: true, id: args.id });
1755
2010
  }
2011
+ // Media
2012
+ case "upload-media": {
2013
+ const filePath = args.filePath;
2014
+ const filename = (0, import_node_path4.basename)(filePath);
2015
+ const uploaded = await client.media.upload({
2016
+ data: new Uint8Array(await (0, import_promises2.readFile)(filePath)),
2017
+ filename,
2018
+ contentType: extToMime(filename)
2019
+ });
2020
+ return ok({ url: uploaded.url, type: uploaded.type, ...args.altText ? { altText: args.altText } : {} });
2021
+ }
2022
+ // Any other REST route on the server's allow-list
2023
+ case "list-api-routes": {
2024
+ const prefix = typeof args.prefix === "string" ? args.prefix : "/";
2025
+ const routes = await apiRoutes(client);
2026
+ return ok({ routes: routes.filter((r) => r.split(" ")[1].startsWith(prefix)) });
2027
+ }
2028
+ case "api-request": {
2029
+ const method = String(args.method ?? "").toUpperCase();
2030
+ const path3 = String(args.path ?? "");
2031
+ assertApiRoute(await apiRoutes(client), method, path3);
2032
+ const query = typeof args.query === "object" && args.query ? args.query : void 0;
2033
+ const body = method !== "GET" && method !== "DELETE" && typeof args.body === "object" && args.body ? args.body : void 0;
2034
+ return ok(await client.request(method, path3, body, query));
2035
+ }
1756
2036
  // Billing
1757
2037
  case "billing-status": {
1758
2038
  const result = await client.billing.status();
@@ -1774,10 +2054,14 @@ async function dispatchToolCall(name, args, client) {
1774
2054
  };
1775
2055
  }
1776
2056
  }
1777
- var PLATFORMS, PLATFORM_SET;
2057
+ var import_promises2, import_node_path4, PLATFORMS, PLATFORM_SET, routeCache;
1778
2058
  var init_handler = __esm({
1779
2059
  "src/mcp/handler.ts"() {
1780
2060
  "use strict";
2061
+ import_promises2 = require("fs/promises");
2062
+ import_node_path4 = require("path");
2063
+ init_api_routes();
2064
+ init_media_flags();
1781
2065
  PLATFORMS = [
1782
2066
  "x",
1783
2067
  "linkedin",
@@ -1786,6 +2070,7 @@ var init_handler = __esm({
1786
2070
  "youtube"
1787
2071
  ];
1788
2072
  PLATFORM_SET = new Set(PLATFORMS);
2073
+ routeCache = /* @__PURE__ */ new WeakMap();
1789
2074
  }
1790
2075
  });
1791
2076
 
@@ -1824,7 +2109,7 @@ var init_server = __esm({
1824
2109
  });
1825
2110
 
1826
2111
  // src/cli.ts
1827
- var import_commander20 = require("commander");
2112
+ var import_commander21 = require("commander");
1828
2113
  init_dist();
1829
2114
 
1830
2115
  // src/commands/agents.ts
@@ -2375,16 +2660,26 @@ async function requestDeviceCode(baseUrl) {
2375
2660
  const text = await res.text().catch(() => res.statusText);
2376
2661
  throw new Error(`Device code request failed (${res.status}): ${text}`);
2377
2662
  }
2378
- return parseJson(res);
2663
+ const { data } = await parseJson(res);
2664
+ if (!data?.deviceCode || !data.userCode || !data.verificationUri) {
2665
+ throw new Error("Device code response is missing deviceCode, userCode or verificationUri.");
2666
+ }
2667
+ return data;
2379
2668
  }
2380
2669
  async function pollDeviceCode(baseUrl, deviceCode) {
2381
2670
  const url = `${baseUrl}/auth/cli/poll?device_code=${encodeURIComponent(deviceCode)}`;
2382
2671
  const res = await fetchWithTimeout(url);
2383
- if (!res.ok) {
2384
- const text = await res.text().catch(() => res.statusText);
2385
- throw new Error(`Poll request failed (${res.status}): ${text}`);
2672
+ const text = await res.text().catch(() => res.statusText);
2673
+ let body = {};
2674
+ try {
2675
+ body = JSON.parse(text);
2676
+ } catch {
2677
+ }
2678
+ if (res.ok || res.status === 400 && body.status) {
2679
+ if (!body.status) throw new Error(`Poll response (HTTP ${res.status}) has no status.`);
2680
+ return body;
2386
2681
  }
2387
- return parseJson(res);
2682
+ throw new Error(`Poll request failed (${res.status}): ${text}`);
2388
2683
  }
2389
2684
 
2390
2685
  // src/commands/auth.ts
@@ -2618,125 +2913,7 @@ function createBillingCommand() {
2618
2913
  // src/commands/brands.ts
2619
2914
  var import_commander4 = require("commander");
2620
2915
  init_dist();
2621
-
2622
- // src/lib/media-flags.ts
2623
- var import_node_fs2 = require("fs");
2624
- var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
2625
- var EXT_TO_MIME = {
2626
- jpg: "image/jpeg",
2627
- jpeg: "image/jpeg",
2628
- png: "image/png",
2629
- gif: "image/gif",
2630
- webp: "image/webp",
2631
- mp4: "video/mp4",
2632
- mov: "video/quicktime",
2633
- webm: "video/webm"
2634
- };
2635
- function extToMime(filename) {
2636
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
2637
- const mime = EXT_TO_MIME[ext];
2638
- if (!mime) {
2639
- throw new Error(
2640
- `Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
2641
- );
2642
- }
2643
- return mime;
2644
- }
2645
- function parsePairs(flag, vals) {
2646
- const result = {};
2647
- for (const val of vals) {
2648
- const eqIdx = val.indexOf("=");
2649
- if (eqIdx < 1) {
2650
- throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
2651
- }
2652
- const p = val.slice(0, eqIdx).trim();
2653
- const v = val.slice(eqIdx + 1);
2654
- if (!VALID_PLATFORMS.includes(p)) {
2655
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2656
- }
2657
- result[p] = v;
2658
- }
2659
- return result;
2660
- }
2661
- function parsePlatformMediaPairs(flag, vals) {
2662
- const result = {};
2663
- for (const val of vals) {
2664
- const eqIdx = val.indexOf("=");
2665
- if (eqIdx < 1) {
2666
- throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
2667
- }
2668
- const p = val.slice(0, eqIdx).trim();
2669
- const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
2670
- if (!VALID_PLATFORMS.includes(p)) {
2671
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2672
- }
2673
- result[p] = paths;
2674
- }
2675
- return result;
2676
- }
2677
- function validateMediaExtensions(paths) {
2678
- for (const p of paths) extToMime(p);
2679
- }
2680
- function validateMediaCount(paths) {
2681
- if (paths.length > 10) {
2682
- throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
2683
- }
2684
- }
2685
- function validateMediaPaths(paths) {
2686
- for (const p of paths) {
2687
- if (!(0, import_node_fs2.existsSync)(p)) {
2688
- throw new Error(`Media file not found: "${p}"`);
2689
- }
2690
- }
2691
- }
2692
- function alignAltText(mediaPaths, altTexts) {
2693
- if (altTexts.length > mediaPaths.length) {
2694
- throw new Error(
2695
- `--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
2696
- );
2697
- }
2698
- return mediaPaths.map((_, i) => altTexts[i]);
2699
- }
2700
- function buildYoutubeOptions(opts) {
2701
- const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
2702
- const o = {};
2703
- if (ytTitle) o.title = ytTitle;
2704
- if (ytDescription) o.description = ytDescription;
2705
- if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
2706
- if (ytPrivacy) o.privacyStatus = ytPrivacy;
2707
- if (ytCategory) o.categoryId = ytCategory;
2708
- if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
2709
- return Object.keys(o).length > 0 ? o : void 0;
2710
- }
2711
- function buildInstagramOptions(opts) {
2712
- const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
2713
- const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
2714
- if (!hasAnyReelFlag) return void 0;
2715
- const reel = {};
2716
- if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
2717
- if (igCoverUrl) reel.coverUrl = igCoverUrl;
2718
- if (igThumbOffsetMs !== void 0) {
2719
- const n = Number(igThumbOffsetMs);
2720
- if (!Number.isFinite(n) || n < 0) {
2721
- throw new Error(
2722
- `--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
2723
- );
2724
- }
2725
- reel.thumbOffsetMs = n;
2726
- }
2727
- if (igCollaborators) {
2728
- reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
2729
- }
2730
- return { reel };
2731
- }
2732
- function buildThreadsOptions(opts) {
2733
- const { threadsReplyTo, threadsReplyControl } = opts;
2734
- if (!threadsReplyTo && !threadsReplyControl) return void 0;
2735
- const o = {};
2736
- if (threadsReplyTo) o.replyToId = threadsReplyTo;
2737
- if (threadsReplyControl) o.replyControl = threadsReplyControl;
2738
- return o;
2739
- }
2916
+ init_media_flags();
2740
2917
 
2741
2918
  // src/auth/config-store.ts
2742
2919
  var import_node_fs3 = __toESM(require("fs"));
@@ -3262,6 +3439,7 @@ init_dist();
3262
3439
  // src/lib/post-create.ts
3263
3440
  var import_promises = __toESM(require("fs/promises"));
3264
3441
  var import_node_path3 = __toESM(require("path"));
3442
+ init_media_flags();
3265
3443
 
3266
3444
  // src/lib/account-select.ts
3267
3445
  var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
@@ -4719,8 +4897,73 @@ function createConfigCommand() {
4719
4897
  return config;
4720
4898
  }
4721
4899
 
4900
+ // src/commands/api.ts
4901
+ var import_commander20 = require("commander");
4902
+ init_dist();
4903
+ init_api_routes();
4904
+ function validationError(message) {
4905
+ return Object.assign(new Error(message), { exitCode: EXIT_CODES.VALIDATION_ERROR });
4906
+ }
4907
+ function collect(value, previous) {
4908
+ return [...previous, value];
4909
+ }
4910
+ function parseFields(pairs) {
4911
+ const out = {};
4912
+ for (const pair of pairs) {
4913
+ const i = pair.indexOf("=");
4914
+ if (i < 1) throw validationError(`Expected key=value, got "${pair}".`);
4915
+ const raw = pair.slice(i + 1);
4916
+ let value = raw;
4917
+ try {
4918
+ value = JSON.parse(raw);
4919
+ } catch {
4920
+ }
4921
+ out[pair.slice(0, i)] = value;
4922
+ }
4923
+ return out;
4924
+ }
4925
+ function createApiCommand() {
4926
+ return new import_commander20.Command("api").description("Call any Autoposting REST route on the server allow-list, like `gh api`").argument("[method]", "GET, POST, PUT, PATCH or DELETE").argument("[path]", "Route path, e.g. /clips/abc123/jobs").option("-d, --data <json>", "JSON request body").option("-f, --field <key=value>", "Body field (repeatable)", collect, []).option("-q, --query <key=value>", "Query parameter (repeatable)", collect, []).option("--list [prefix]", "List the routes you may call").action(async (method, path3, opts, cmd) => {
4927
+ const globals = cmd.optsWithGlobals();
4928
+ const printer = createPrinter(globals);
4929
+ try {
4930
+ const cred = resolveAuth({ apiKey: globals.apiKey });
4931
+ let body;
4932
+ if (opts.data !== void 0) {
4933
+ try {
4934
+ body = JSON.parse(opts.data);
4935
+ } catch {
4936
+ throw validationError("--data is not valid JSON.");
4937
+ }
4938
+ }
4939
+ if (opts.field.length > 0) body = { ...body, ...parseFields(opts.field) };
4940
+ const query = opts.query.length > 0 ? parseFields(opts.query) : void 0;
4941
+ const client = new Autoposting({ apiKey: cred.apiKey });
4942
+ const routes = await fetchApiRoutes(client);
4943
+ if (opts.list !== void 0) {
4944
+ const prefix = typeof opts.list === "string" ? opts.list : "/";
4945
+ printer.log(routes.filter((r) => r.split(" ")[1].startsWith(prefix)));
4946
+ return;
4947
+ }
4948
+ if (!method || !path3) throw validationError("Usage: ap api <METHOD> <path>, or ap api --list.");
4949
+ const m = method.toUpperCase();
4950
+ try {
4951
+ assertApiRoute(routes, m, path3);
4952
+ } catch (err) {
4953
+ throw validationError(err.message);
4954
+ }
4955
+ const sendBody = m === "GET" || m === "DELETE" ? void 0 : body;
4956
+ printer.log(await client.request(m, path3, sendBody, query));
4957
+ } catch (err) {
4958
+ printer.error(err);
4959
+ const attached = err.exitCode;
4960
+ process.exit(typeof attached === "number" ? attached : exitCodeFromError(err));
4961
+ }
4962
+ });
4963
+ }
4964
+
4722
4965
  // src/cli.ts
4723
- var program = new import_commander20.Command().name("autoposting").description("Autoposting CLI \u2014 manage social media from the terminal").version(VERSION).option("--json", "Output as JSON").option("--quiet", "Suppress spinners and non-essential output").option("--format <type>", "Output format: auto (table on a terminal, JSON when piped), table, json", "auto").option("--jq <expr>", 'Filter JSON output with a minimal jq expression (e.g. ".[].id")').option("--api-key <key>", "API key (overrides env/stored credentials)").option("--no-color", "Disable color output").hook("preAction", (thisCommand) => {
4966
+ var program = new import_commander21.Command().name("autoposting").description("Autoposting CLI \u2014 manage social media from the terminal").version(VERSION).option("--json", "Output as JSON").option("--quiet", "Suppress spinners and non-essential output").option("--format <type>", "Output format: auto (table on a terminal, JSON when piped), table, json", "auto").option("--jq <expr>", 'Filter JSON output with a minimal jq expression (e.g. ".[].id")').option("--api-key <key>", "API key (overrides env/stored credentials)").option("--no-color", "Disable color output").hook("preAction", (thisCommand) => {
4724
4967
  const opts = thisCommand.opts();
4725
4968
  if (!opts.color) disableColor();
4726
4969
  });
@@ -4729,6 +4972,7 @@ program.addCommand(createBillingCommand());
4729
4972
  program.addCommand(createBrandsCommand());
4730
4973
  program.addCommand(createCarouselsCommand());
4731
4974
  program.addCommand(createClipsCommand());
4975
+ program.addCommand(createApiCommand());
4732
4976
  program.addCommand(createPostsCommand());
4733
4977
  program.addCommand(createAgentsCommand());
4734
4978
  program.addCommand(createKbCommand());