@autoposting.ai/cli 0.3.5 → 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.5";
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);
@@ -298,8 +298,16 @@ var init_dist = __esm({
298
298
  return this.client.request("POST", "/clips/import-url", params);
299
299
  }
300
300
  /** POST /clips/:id/render — backend returns render job ids, not a Clip. */
301
- render(id) {
302
- return this.client.request("POST", `/clips/${id}/render`);
301
+ async render(id, params = {}) {
302
+ const editRevision = params.editRevision ?? (await this.retrieve(id)).editRevision ?? 0;
303
+ return this.client.request("POST", `/clips/${id}/render`, {
304
+ editRevision,
305
+ ...params.candidateId ? { candidateId: params.candidateId } : {}
306
+ });
307
+ }
308
+ /** POST /clips/:id/create-draft — turns a rendered clip into a draft post. */
309
+ createDraft(id, params = {}) {
310
+ return this.client.request("POST", `/clips/${id}/create-draft`, params);
303
311
  }
304
312
  remove(id) {
305
313
  return this.client.request("DELETE", `/clips/${id}`);
@@ -655,11 +663,151 @@ var init_dist = __esm({
655
663
  }
656
664
  });
657
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
+
658
791
  // src/mcp/tools.ts
659
- var ALL_TOOLS;
792
+ var MEDIA_ITEMS, ALL_TOOLS;
660
793
  var init_tools = __esm({
661
794
  "src/mcp/tools.ts"() {
662
795
  "use strict";
796
+ MEDIA_ITEMS = {
797
+ type: "array",
798
+ maxItems: 10,
799
+ description: "Images or videos to attach, as https URLs. For a local file, call upload-media first and pass its result here.",
800
+ items: {
801
+ type: "object",
802
+ properties: {
803
+ url: { type: "string", description: "Public https URL of the image or video" },
804
+ type: { type: "string", enum: ["image", "video", "gif"] },
805
+ altText: { type: "string", description: "Optional alt text" }
806
+ },
807
+ required: ["url", "type"],
808
+ additionalProperties: false
809
+ }
810
+ };
663
811
  ALL_TOOLS = [
664
812
  // Posts
665
813
  {
@@ -707,7 +855,8 @@ var init_tools = __esm({
707
855
  scheduledAt: {
708
856
  type: "string",
709
857
  description: "ISO 8601 datetime to schedule the post"
710
- }
858
+ },
859
+ media: MEDIA_ITEMS
711
860
  },
712
861
  required: ["brandSlug", "text", "platforms"],
713
862
  additionalProperties: false
@@ -715,14 +864,15 @@ var init_tools = __esm({
715
864
  },
716
865
  {
717
866
  name: "update-post",
718
- description: "Update the text, platforms, or scheduled time of an existing post.",
867
+ description: "Update the text, platforms, scheduled time, or media of an existing post.",
719
868
  inputSchema: {
720
869
  type: "object",
721
870
  properties: {
722
871
  id: { type: "string", description: "Post ID" },
723
872
  text: { type: "string", description: "New post text" },
724
873
  platforms: { type: "string", description: "Comma-separated new platform list" },
725
- scheduledAt: { type: "string", description: "New ISO 8601 scheduled datetime" }
874
+ scheduledAt: { type: "string", description: "New ISO 8601 scheduled datetime" },
875
+ media: MEDIA_ITEMS
726
876
  },
727
877
  required: ["id"],
728
878
  additionalProperties: false
@@ -1156,29 +1306,79 @@ var init_tools = __esm({
1156
1306
  },
1157
1307
  {
1158
1308
  name: "import-clip",
1159
- description: "Import a video clip from a remote URL.",
1309
+ description: "Start AI clipping: import a long video from a public URL (e.g. a YouTube video). Poll get-clip until status is ready.",
1160
1310
  inputSchema: {
1161
1311
  type: "object",
1162
1312
  properties: {
1163
1313
  url: { type: "string", description: "URL of the video to import" },
1314
+ brandId: { type: "string", description: "Brand ID (the `id` from list-brands)" },
1164
1315
  name: { type: "string", description: "Optional name for the imported clip" }
1165
1316
  },
1166
- required: ["url"],
1317
+ required: ["url", "brandId"],
1167
1318
  additionalProperties: false
1168
1319
  }
1169
1320
  },
1170
1321
  {
1171
1322
  name: "render-clip",
1172
- description: "Trigger rendering for a video clip.",
1323
+ description: "Render a clip (uses credits). Call get-clip first for its editRevision and candidates.",
1324
+ inputSchema: {
1325
+ type: "object",
1326
+ properties: {
1327
+ id: { type: "string", description: "Clip ID to render" },
1328
+ editRevision: { type: "number", description: "Current editRevision from get-clip" },
1329
+ candidateId: { type: "string", description: "Optional: render only this candidate" }
1330
+ },
1331
+ required: ["id", "editRevision"],
1332
+ additionalProperties: false
1333
+ }
1334
+ },
1335
+ {
1336
+ name: "create-clip-draft",
1337
+ description: "Create a draft post from a rendered clip. Publish it with publish-post or schedule it.",
1173
1338
  inputSchema: {
1174
1339
  type: "object",
1175
1340
  properties: {
1176
- id: { type: "string", description: "Clip ID to render" }
1341
+ id: { type: "string", description: "Clip ID" },
1342
+ candidateId: { type: "string", description: "Optional candidate ID (defaults to the first rendered one)" },
1343
+ aspectRatio: { type: "string", enum: ["9:16", "16:9", "1:1", "4:5"], description: "Optional render format" }
1177
1344
  },
1178
1345
  required: ["id"],
1179
1346
  additionalProperties: false
1180
1347
  }
1181
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
+ },
1182
1382
  {
1183
1383
  name: "delete-clip",
1184
1384
  description: "Permanently delete a video clip by ID.",
@@ -1191,6 +1391,45 @@ var init_tools = __esm({
1191
1391
  additionalProperties: false
1192
1392
  }
1193
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
+ },
1194
1433
  // Carousels
1195
1434
  {
1196
1435
  name: "list-carousels",
@@ -1373,6 +1612,40 @@ var init_tools = __esm({
1373
1612
  }
1374
1613
  });
1375
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
+
1376
1649
  // src/mcp/handler.ts
1377
1650
  function ok(data) {
1378
1651
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -1392,6 +1665,15 @@ function parsePlatforms2(raw) {
1392
1665
  }
1393
1666
  return platforms;
1394
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
+ }
1395
1677
  function parseEvents(raw) {
1396
1678
  if (typeof raw !== "string") return [];
1397
1679
  return raw.split(",").map((e) => e.trim());
@@ -1424,7 +1706,8 @@ async function dispatchToolCall(name, args, client) {
1424
1706
  brandSlug: args.brandSlug,
1425
1707
  text: args.text,
1426
1708
  platforms: parsePlatforms2(args.platforms),
1427
- ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {}
1709
+ ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {},
1710
+ ...args.media ? { media: args.media } : {}
1428
1711
  });
1429
1712
  return ok(result);
1430
1713
  }
@@ -1434,7 +1717,8 @@ async function dispatchToolCall(name, args, client) {
1434
1717
  ...args.platforms ? {
1435
1718
  platforms: parsePlatforms2(args.platforms)
1436
1719
  } : {},
1437
- ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {}
1720
+ ...args.scheduledAt ? { scheduledAt: args.scheduledAt } : {},
1721
+ ...args.media ? { media: args.media } : {}
1438
1722
  });
1439
1723
  return ok(result);
1440
1724
  }
@@ -1622,14 +1906,40 @@ async function dispatchToolCall(name, args, client) {
1622
1906
  case "import-clip": {
1623
1907
  const result = await client.clips.importUrl({
1624
1908
  url: args.url,
1625
- ...args.name ? { name: args.name } : {}
1909
+ brandId: args.brandId,
1910
+ ...args.name ? { title: args.name } : {}
1626
1911
  });
1627
1912
  return ok(result);
1628
1913
  }
1629
1914
  case "render-clip": {
1630
- const result = await client.clips.render(args.id);
1915
+ const result = await client.clips.render(args.id, {
1916
+ editRevision: args.editRevision,
1917
+ ...args.candidateId ? { candidateId: args.candidateId } : {}
1918
+ });
1631
1919
  return ok(result);
1632
1920
  }
1921
+ case "create-clip-draft": {
1922
+ const result = await client.clips.createDraft(args.id, {
1923
+ ...args.candidateId ? { candidateId: args.candidateId } : {},
1924
+ ...args.aspectRatio ? { aspectRatio: args.aspectRatio } : {}
1925
+ });
1926
+ return ok(result);
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
+ }
1633
1943
  case "delete-clip": {
1634
1944
  await client.clips.remove(args.id);
1635
1945
  return ok({ deleted: true, id: args.id });
@@ -1698,6 +2008,31 @@ async function dispatchToolCall(name, args, client) {
1698
2008
  await client.webhooks.test(args.id);
1699
2009
  return ok({ sent: true, id: args.id });
1700
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
+ }
1701
2036
  // Billing
1702
2037
  case "billing-status": {
1703
2038
  const result = await client.billing.status();
@@ -1719,10 +2054,14 @@ async function dispatchToolCall(name, args, client) {
1719
2054
  };
1720
2055
  }
1721
2056
  }
1722
- var PLATFORMS, PLATFORM_SET;
2057
+ var import_promises2, import_node_path4, PLATFORMS, PLATFORM_SET, routeCache;
1723
2058
  var init_handler = __esm({
1724
2059
  "src/mcp/handler.ts"() {
1725
2060
  "use strict";
2061
+ import_promises2 = require("fs/promises");
2062
+ import_node_path4 = require("path");
2063
+ init_api_routes();
2064
+ init_media_flags();
1726
2065
  PLATFORMS = [
1727
2066
  "x",
1728
2067
  "linkedin",
@@ -1731,6 +2070,7 @@ var init_handler = __esm({
1731
2070
  "youtube"
1732
2071
  ];
1733
2072
  PLATFORM_SET = new Set(PLATFORMS);
2073
+ routeCache = /* @__PURE__ */ new WeakMap();
1734
2074
  }
1735
2075
  });
1736
2076
 
@@ -1769,7 +2109,7 @@ var init_server = __esm({
1769
2109
  });
1770
2110
 
1771
2111
  // src/cli.ts
1772
- var import_commander20 = require("commander");
2112
+ var import_commander21 = require("commander");
1773
2113
  init_dist();
1774
2114
 
1775
2115
  // src/commands/agents.ts
@@ -2320,16 +2660,26 @@ async function requestDeviceCode(baseUrl) {
2320
2660
  const text = await res.text().catch(() => res.statusText);
2321
2661
  throw new Error(`Device code request failed (${res.status}): ${text}`);
2322
2662
  }
2323
- 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;
2324
2668
  }
2325
2669
  async function pollDeviceCode(baseUrl, deviceCode) {
2326
2670
  const url = `${baseUrl}/auth/cli/poll?device_code=${encodeURIComponent(deviceCode)}`;
2327
2671
  const res = await fetchWithTimeout(url);
2328
- if (!res.ok) {
2329
- const text = await res.text().catch(() => res.statusText);
2330
- 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;
2331
2681
  }
2332
- return parseJson(res);
2682
+ throw new Error(`Poll request failed (${res.status}): ${text}`);
2333
2683
  }
2334
2684
 
2335
2685
  // src/commands/auth.ts
@@ -2563,125 +2913,7 @@ function createBillingCommand() {
2563
2913
  // src/commands/brands.ts
2564
2914
  var import_commander4 = require("commander");
2565
2915
  init_dist();
2566
-
2567
- // src/lib/media-flags.ts
2568
- var import_node_fs2 = require("fs");
2569
- var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
2570
- var EXT_TO_MIME = {
2571
- jpg: "image/jpeg",
2572
- jpeg: "image/jpeg",
2573
- png: "image/png",
2574
- gif: "image/gif",
2575
- webp: "image/webp",
2576
- mp4: "video/mp4",
2577
- mov: "video/quicktime",
2578
- webm: "video/webm"
2579
- };
2580
- function extToMime(filename) {
2581
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
2582
- const mime = EXT_TO_MIME[ext];
2583
- if (!mime) {
2584
- throw new Error(
2585
- `Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
2586
- );
2587
- }
2588
- return mime;
2589
- }
2590
- function parsePairs(flag, vals) {
2591
- const result = {};
2592
- for (const val of vals) {
2593
- const eqIdx = val.indexOf("=");
2594
- if (eqIdx < 1) {
2595
- throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
2596
- }
2597
- const p = val.slice(0, eqIdx).trim();
2598
- const v = val.slice(eqIdx + 1);
2599
- if (!VALID_PLATFORMS.includes(p)) {
2600
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2601
- }
2602
- result[p] = v;
2603
- }
2604
- return result;
2605
- }
2606
- function parsePlatformMediaPairs(flag, vals) {
2607
- const result = {};
2608
- for (const val of vals) {
2609
- const eqIdx = val.indexOf("=");
2610
- if (eqIdx < 1) {
2611
- throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
2612
- }
2613
- const p = val.slice(0, eqIdx).trim();
2614
- const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
2615
- if (!VALID_PLATFORMS.includes(p)) {
2616
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2617
- }
2618
- result[p] = paths;
2619
- }
2620
- return result;
2621
- }
2622
- function validateMediaExtensions(paths) {
2623
- for (const p of paths) extToMime(p);
2624
- }
2625
- function validateMediaCount(paths) {
2626
- if (paths.length > 10) {
2627
- throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
2628
- }
2629
- }
2630
- function validateMediaPaths(paths) {
2631
- for (const p of paths) {
2632
- if (!(0, import_node_fs2.existsSync)(p)) {
2633
- throw new Error(`Media file not found: "${p}"`);
2634
- }
2635
- }
2636
- }
2637
- function alignAltText(mediaPaths, altTexts) {
2638
- if (altTexts.length > mediaPaths.length) {
2639
- throw new Error(
2640
- `--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
2641
- );
2642
- }
2643
- return mediaPaths.map((_, i) => altTexts[i]);
2644
- }
2645
- function buildYoutubeOptions(opts) {
2646
- const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
2647
- const o = {};
2648
- if (ytTitle) o.title = ytTitle;
2649
- if (ytDescription) o.description = ytDescription;
2650
- if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
2651
- if (ytPrivacy) o.privacyStatus = ytPrivacy;
2652
- if (ytCategory) o.categoryId = ytCategory;
2653
- if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
2654
- return Object.keys(o).length > 0 ? o : void 0;
2655
- }
2656
- function buildInstagramOptions(opts) {
2657
- const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
2658
- const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
2659
- if (!hasAnyReelFlag) return void 0;
2660
- const reel = {};
2661
- if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
2662
- if (igCoverUrl) reel.coverUrl = igCoverUrl;
2663
- if (igThumbOffsetMs !== void 0) {
2664
- const n = Number(igThumbOffsetMs);
2665
- if (!Number.isFinite(n) || n < 0) {
2666
- throw new Error(
2667
- `--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
2668
- );
2669
- }
2670
- reel.thumbOffsetMs = n;
2671
- }
2672
- if (igCollaborators) {
2673
- reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
2674
- }
2675
- return { reel };
2676
- }
2677
- function buildThreadsOptions(opts) {
2678
- const { threadsReplyTo, threadsReplyControl } = opts;
2679
- if (!threadsReplyTo && !threadsReplyControl) return void 0;
2680
- const o = {};
2681
- if (threadsReplyTo) o.replyToId = threadsReplyTo;
2682
- if (threadsReplyControl) o.replyControl = threadsReplyControl;
2683
- return o;
2684
- }
2916
+ init_media_flags();
2685
2917
 
2686
2918
  // src/auth/config-store.ts
2687
2919
  var import_node_fs3 = __toESM(require("fs"));
@@ -3118,16 +3350,18 @@ function createClipsCommand() {
3118
3350
  process.exit(resolveExitCode4(err));
3119
3351
  }
3120
3352
  });
3121
- clips.command("import").description("Import a clip from a URL").requiredOption("--url <url>", "URL of the video to import").option("--name <name>", "Name for the imported clip").action(async (opts, cmd) => {
3353
+ clips.command("import").description("Start AI clipping from a video URL (e.g. a YouTube video)").requiredOption("--url <url>", "URL of the video to import").option("--brand <id>", "Brand ID (auto-picked when the workspace has exactly one brand)").option("--name <name>", "Name for the imported clip").action(async (opts, cmd) => {
3122
3354
  const globals = cmd.optsWithGlobals();
3123
3355
  const printer = createPrinter(globals);
3124
3356
  const spinner = printer.spinner("Importing clip\u2026");
3125
3357
  try {
3126
3358
  const cred = resolveAuth({ apiKey: globals.apiKey });
3127
3359
  const client = new Autoposting({ apiKey: cred.apiKey });
3360
+ const brandId = await resolveBrandId(client, opts.brand);
3128
3361
  const { clipId } = await client.clips.importUrl({
3129
3362
  url: opts.url,
3130
- ...opts.name ? { name: opts.name } : {}
3363
+ brandId,
3364
+ ...opts.name ? { title: opts.name } : {}
3131
3365
  });
3132
3366
  spinner.stop();
3133
3367
  printer.log({ clipId });
@@ -3137,14 +3371,17 @@ function createClipsCommand() {
3137
3371
  process.exit(resolveExitCode4(err));
3138
3372
  }
3139
3373
  });
3140
- clips.command("render <id>").description("Trigger rendering for a clip").action(async (id, _opts, cmd) => {
3374
+ clips.command("render <id>").description("Render a clip (uses credits)").option("--candidate <id>", "Render only this candidate").option("--revision <n>", "Clip editRevision (read from the clip when omitted)", (v) => Number(v)).action(async (id, opts, cmd) => {
3141
3375
  const globals = cmd.optsWithGlobals();
3142
3376
  const printer = createPrinter(globals);
3143
3377
  const spinner = printer.spinner(`Rendering clip "${id}"\u2026`);
3144
3378
  try {
3145
3379
  const cred = resolveAuth({ apiKey: globals.apiKey });
3146
3380
  const client = new Autoposting({ apiKey: cred.apiKey });
3147
- const { jobIds, activeJobIds, reusedJobIds } = await client.clips.render(id);
3381
+ const { jobIds, activeJobIds, reusedJobIds } = await client.clips.render(id, {
3382
+ ...opts.revision !== void 0 ? { editRevision: opts.revision } : {},
3383
+ ...opts.candidate ? { candidateId: opts.candidate } : {}
3384
+ });
3148
3385
  spinner.stop();
3149
3386
  printer.log({ jobIds, activeJobIds, reusedJobIds });
3150
3387
  } catch (err) {
@@ -3153,6 +3390,25 @@ function createClipsCommand() {
3153
3390
  process.exit(resolveExitCode4(err));
3154
3391
  }
3155
3392
  });
3393
+ clips.command("draft <id>").description("Create a draft post from a rendered clip").option("--candidate <id>", "Candidate to use (defaults to the first rendered one)").option("--aspect-ratio <ratio>", "Render format: 9:16, 16:9, 1:1 or 4:5").action(async (id, opts, cmd) => {
3394
+ const globals = cmd.optsWithGlobals();
3395
+ const printer = createPrinter(globals);
3396
+ const spinner = printer.spinner(`Creating draft from clip "${id}"\u2026`);
3397
+ try {
3398
+ const cred = resolveAuth({ apiKey: globals.apiKey });
3399
+ const client = new Autoposting({ apiKey: cred.apiKey });
3400
+ const { postId } = await client.clips.createDraft(id, {
3401
+ ...opts.candidate ? { candidateId: opts.candidate } : {},
3402
+ ...opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}
3403
+ });
3404
+ spinner.stop();
3405
+ printer.log({ postId });
3406
+ } catch (err) {
3407
+ spinner.fail();
3408
+ printer.error(err);
3409
+ process.exit(resolveExitCode4(err));
3410
+ }
3411
+ });
3156
3412
  clips.command("delete <id>").description("Delete a clip (requires --force)").option("--force", "Confirm deletion without interactive prompt").action(async (id, opts, cmd) => {
3157
3413
  const globals = cmd.optsWithGlobals();
3158
3414
  const printer = createPrinter(globals);
@@ -3183,6 +3439,7 @@ init_dist();
3183
3439
  // src/lib/post-create.ts
3184
3440
  var import_promises = __toESM(require("fs/promises"));
3185
3441
  var import_node_path3 = __toESM(require("path"));
3442
+ init_media_flags();
3186
3443
 
3187
3444
  // src/lib/account-select.ts
3188
3445
  var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
@@ -4640,8 +4897,73 @@ function createConfigCommand() {
4640
4897
  return config;
4641
4898
  }
4642
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
+
4643
4965
  // src/cli.ts
4644
- 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) => {
4645
4967
  const opts = thisCommand.opts();
4646
4968
  if (!opts.color) disableColor();
4647
4969
  });
@@ -4650,6 +4972,7 @@ program.addCommand(createBillingCommand());
4650
4972
  program.addCommand(createBrandsCommand());
4651
4973
  program.addCommand(createCarouselsCommand());
4652
4974
  program.addCommand(createClipsCommand());
4975
+ program.addCommand(createApiCommand());
4653
4976
  program.addCommand(createPostsCommand());
4654
4977
  program.addCommand(createAgentsCommand());
4655
4978
  program.addCommand(createKbCommand());