@autoposting.ai/cli 0.3.6 → 0.4.1

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.1";
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: {
@@ -1041,9 +1166,10 @@ var init_tools = __esm({
1041
1166
  inputSchema: {
1042
1167
  type: "object",
1043
1168
  properties: {
1044
- name: { type: "string", description: "Knowledge base name" }
1169
+ name: { type: "string", minLength: 4, maxLength: 20, description: "Knowledge base name" },
1170
+ description: { type: "string", minLength: 20, maxLength: 500, description: "Knowledge base description" }
1045
1171
  },
1046
- required: ["name"],
1172
+ required: ["name", "description"],
1047
1173
  additionalProperties: false
1048
1174
  }
1049
1175
  },
@@ -1221,6 +1347,39 @@ var init_tools = __esm({
1221
1347
  additionalProperties: false
1222
1348
  }
1223
1349
  },
1350
+ {
1351
+ name: "upload-clip",
1352
+ description: "Upload a local video file for AI clipping (multipart, any size). Poll get-clip until status is ready, then render-clip.",
1353
+ inputSchema: {
1354
+ type: "object",
1355
+ properties: {
1356
+ filePath: { type: "string", description: "Absolute path to the video file on this machine" },
1357
+ brandId: { type: "string", description: "Brand ID (the `id` from list-brands)" },
1358
+ title: { type: "string", description: "Optional clip title" }
1359
+ },
1360
+ required: ["filePath", "brandId"],
1361
+ additionalProperties: false
1362
+ }
1363
+ },
1364
+ {
1365
+ name: "publish-clip",
1366
+ description: "Publish, schedule, or save as a draft a rendered clip candidate.",
1367
+ inputSchema: {
1368
+ type: "object",
1369
+ properties: {
1370
+ id: { type: "string", description: "Clip ID" },
1371
+ mode: { type: "string", enum: ["publish", "schedule", "draft"] },
1372
+ candidateId: { type: "string", description: "Candidate to post (defaults to the best rendered one)" },
1373
+ platforms: { type: "string", description: 'Comma-separated platforms, e.g. "x,linkedin"' },
1374
+ text: { type: "string", description: "Post caption" },
1375
+ brandSlug: { type: "string", description: "Brand slug" },
1376
+ scheduledAt: { type: "string", description: 'ISO 8601 time, for mode "schedule"' },
1377
+ aspectRatio: { type: "string", enum: ["9:16", "4:5", "1:1", "16:9"] }
1378
+ },
1379
+ required: ["id", "mode"],
1380
+ additionalProperties: false
1381
+ }
1382
+ },
1224
1383
  {
1225
1384
  name: "delete-clip",
1226
1385
  description: "Permanently delete a video clip by ID.",
@@ -1233,6 +1392,45 @@ var init_tools = __esm({
1233
1392
  additionalProperties: false
1234
1393
  }
1235
1394
  },
1395
+ // Media
1396
+ {
1397
+ name: "upload-media",
1398
+ 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`.",
1399
+ inputSchema: {
1400
+ type: "object",
1401
+ properties: {
1402
+ filePath: { type: "string", description: "Absolute path to the file on this machine" },
1403
+ altText: { type: "string", description: "Optional alt text" }
1404
+ },
1405
+ required: ["filePath"],
1406
+ additionalProperties: false
1407
+ }
1408
+ },
1409
+ // Any other REST route
1410
+ {
1411
+ name: "list-api-routes",
1412
+ description: 'List the REST routes api-request may call, as "METHOD /path/:param". Optional prefix filter, e.g. "/clips".',
1413
+ inputSchema: {
1414
+ type: "object",
1415
+ properties: { prefix: { type: "string", description: "Only routes whose path starts with this" } },
1416
+ additionalProperties: false
1417
+ }
1418
+ },
1419
+ {
1420
+ name: "api-request",
1421
+ description: "Call any Autoposting REST route on the allow-list (see list-api-routes) with your API key. Replace :params with real IDs.",
1422
+ inputSchema: {
1423
+ type: "object",
1424
+ properties: {
1425
+ method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] },
1426
+ path: { type: "string", description: "Path such as /clips/abc123/jobs, no query string" },
1427
+ query: { type: "object", description: "Query string parameters" },
1428
+ body: { type: "object", description: "JSON body for POST, PUT and PATCH" }
1429
+ },
1430
+ required: ["method", "path"],
1431
+ additionalProperties: false
1432
+ }
1433
+ },
1236
1434
  // Carousels
1237
1435
  {
1238
1436
  name: "list-carousels",
@@ -1415,6 +1613,40 @@ var init_tools = __esm({
1415
1613
  }
1416
1614
  });
1417
1615
 
1616
+ // src/lib/api-routes.ts
1617
+ async function fetchApiRoutes(client) {
1618
+ const res = await client.request("POST", "/mcp", {
1619
+ jsonrpc: "2.0",
1620
+ id: 1,
1621
+ method: "tools/call",
1622
+ params: { name: "list-api-routes", arguments: {} }
1623
+ });
1624
+ try {
1625
+ const routes = JSON.parse(res.result?.content?.[0]?.text ?? "").routes;
1626
+ if (Array.isArray(routes)) return routes;
1627
+ } catch {
1628
+ }
1629
+ throw new Error(`Could not load the API allow-list from the server${res.error?.message ? `: ${res.error.message}` : ""}.`);
1630
+ }
1631
+ function assertApiRoute(routes, method, path3) {
1632
+ const m = method.toUpperCase();
1633
+ const segs = path3.startsWith("/") ? path3.slice(1).split("/") : [];
1634
+ const safe = segs.length > 0 && segs.every((s) => SAFE_SEGMENT.test(s) && s !== "." && s !== "..");
1635
+ const match = safe && routes.some((r) => {
1636
+ const [rm, rp] = r.split(" ");
1637
+ const rs = rp.slice(1).split("/");
1638
+ return rm === m && rs.length === segs.length && rs.every((s, i) => s.startsWith(":") || s === segs[i]);
1639
+ });
1640
+ if (!match) throw new Error(`${m} ${path3} is not available. Run list-api-routes (or \`ap api --list\`) to see what is.`);
1641
+ }
1642
+ var SAFE_SEGMENT;
1643
+ var init_api_routes = __esm({
1644
+ "src/lib/api-routes.ts"() {
1645
+ "use strict";
1646
+ SAFE_SEGMENT = /^[\w\-.~]+$/;
1647
+ }
1648
+ });
1649
+
1418
1650
  // src/mcp/handler.ts
1419
1651
  function ok(data) {
1420
1652
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
@@ -1434,6 +1666,15 @@ function parsePlatforms2(raw) {
1434
1666
  }
1435
1667
  return platforms;
1436
1668
  }
1669
+ function apiRoutes(client) {
1670
+ let routes = routeCache.get(client);
1671
+ if (!routes) {
1672
+ routes = fetchApiRoutes(client);
1673
+ routeCache.set(client, routes);
1674
+ routes.catch(() => routeCache.delete(client));
1675
+ }
1676
+ return routes;
1677
+ }
1437
1678
  function parseEvents(raw) {
1438
1679
  if (typeof raw !== "string") return [];
1439
1680
  return raw.split(",").map((e) => e.trim());
@@ -1598,7 +1839,7 @@ async function dispatchToolCall(name, args, client) {
1598
1839
  return ok(result);
1599
1840
  }
1600
1841
  case "create-kb": {
1601
- const result = await client.kb.create({ name: args.name });
1842
+ const result = await client.kb.create({ name: args.name, description: args.description });
1602
1843
  return ok(result);
1603
1844
  }
1604
1845
  case "delete-kb": {
@@ -1685,6 +1926,21 @@ async function dispatchToolCall(name, args, client) {
1685
1926
  });
1686
1927
  return ok(result);
1687
1928
  }
1929
+ case "upload-clip": {
1930
+ const result = await client.clips.upload(args.filePath, {
1931
+ brandId: args.brandId,
1932
+ ...args.title ? { title: args.title } : {}
1933
+ });
1934
+ return ok(result);
1935
+ }
1936
+ case "publish-clip": {
1937
+ const body = { mode: args.mode };
1938
+ for (const k of ["candidateId", "text", "brandSlug", "scheduledAt", "aspectRatio"]) {
1939
+ if (args[k] !== void 0) body[k] = args[k];
1940
+ }
1941
+ if (args.platforms !== void 0) body.platforms = parsePlatforms2(args.platforms);
1942
+ return ok(await client.request("POST", `/clips/${encodeURIComponent(args.id)}/publish`, body));
1943
+ }
1688
1944
  case "delete-clip": {
1689
1945
  await client.clips.remove(args.id);
1690
1946
  return ok({ deleted: true, id: args.id });
@@ -1753,6 +2009,31 @@ async function dispatchToolCall(name, args, client) {
1753
2009
  await client.webhooks.test(args.id);
1754
2010
  return ok({ sent: true, id: args.id });
1755
2011
  }
2012
+ // Media
2013
+ case "upload-media": {
2014
+ const filePath = args.filePath;
2015
+ const filename = (0, import_node_path4.basename)(filePath);
2016
+ const uploaded = await client.media.upload({
2017
+ data: new Uint8Array(await (0, import_promises2.readFile)(filePath)),
2018
+ filename,
2019
+ contentType: extToMime(filename)
2020
+ });
2021
+ return ok({ url: uploaded.url, type: uploaded.type, ...args.altText ? { altText: args.altText } : {} });
2022
+ }
2023
+ // Any other REST route on the server's allow-list
2024
+ case "list-api-routes": {
2025
+ const prefix = typeof args.prefix === "string" ? args.prefix : "/";
2026
+ const routes = await apiRoutes(client);
2027
+ return ok({ routes: routes.filter((r) => r.split(" ")[1].startsWith(prefix)) });
2028
+ }
2029
+ case "api-request": {
2030
+ const method = String(args.method ?? "").toUpperCase();
2031
+ const path3 = String(args.path ?? "");
2032
+ assertApiRoute(await apiRoutes(client), method, path3);
2033
+ const query = typeof args.query === "object" && args.query ? args.query : void 0;
2034
+ const body = method !== "GET" && method !== "DELETE" && typeof args.body === "object" && args.body ? args.body : void 0;
2035
+ return ok(await client.request(method, path3, body, query));
2036
+ }
1756
2037
  // Billing
1757
2038
  case "billing-status": {
1758
2039
  const result = await client.billing.status();
@@ -1774,10 +2055,14 @@ async function dispatchToolCall(name, args, client) {
1774
2055
  };
1775
2056
  }
1776
2057
  }
1777
- var PLATFORMS, PLATFORM_SET;
2058
+ var import_promises2, import_node_path4, PLATFORMS, PLATFORM_SET, routeCache;
1778
2059
  var init_handler = __esm({
1779
2060
  "src/mcp/handler.ts"() {
1780
2061
  "use strict";
2062
+ import_promises2 = require("fs/promises");
2063
+ import_node_path4 = require("path");
2064
+ init_api_routes();
2065
+ init_media_flags();
1781
2066
  PLATFORMS = [
1782
2067
  "x",
1783
2068
  "linkedin",
@@ -1786,6 +2071,7 @@ var init_handler = __esm({
1786
2071
  "youtube"
1787
2072
  ];
1788
2073
  PLATFORM_SET = new Set(PLATFORMS);
2074
+ routeCache = /* @__PURE__ */ new WeakMap();
1789
2075
  }
1790
2076
  });
1791
2077
 
@@ -1824,7 +2110,7 @@ var init_server = __esm({
1824
2110
  });
1825
2111
 
1826
2112
  // src/cli.ts
1827
- var import_commander20 = require("commander");
2113
+ var import_commander21 = require("commander");
1828
2114
  init_dist();
1829
2115
 
1830
2116
  // src/commands/agents.ts
@@ -2375,16 +2661,26 @@ async function requestDeviceCode(baseUrl) {
2375
2661
  const text = await res.text().catch(() => res.statusText);
2376
2662
  throw new Error(`Device code request failed (${res.status}): ${text}`);
2377
2663
  }
2378
- return parseJson(res);
2664
+ const { data } = await parseJson(res);
2665
+ if (!data?.deviceCode || !data.userCode || !data.verificationUri) {
2666
+ throw new Error("Device code response is missing deviceCode, userCode or verificationUri.");
2667
+ }
2668
+ return data;
2379
2669
  }
2380
2670
  async function pollDeviceCode(baseUrl, deviceCode) {
2381
2671
  const url = `${baseUrl}/auth/cli/poll?device_code=${encodeURIComponent(deviceCode)}`;
2382
2672
  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}`);
2673
+ const text = await res.text().catch(() => res.statusText);
2674
+ let body = {};
2675
+ try {
2676
+ body = JSON.parse(text);
2677
+ } catch {
2678
+ }
2679
+ if (res.ok || res.status === 400 && body.status) {
2680
+ if (!body.status) throw new Error(`Poll response (HTTP ${res.status}) has no status.`);
2681
+ return body;
2386
2682
  }
2387
- return parseJson(res);
2683
+ throw new Error(`Poll request failed (${res.status}): ${text}`);
2388
2684
  }
2389
2685
 
2390
2686
  // src/commands/auth.ts
@@ -2618,125 +2914,7 @@ function createBillingCommand() {
2618
2914
  // src/commands/brands.ts
2619
2915
  var import_commander4 = require("commander");
2620
2916
  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
- }
2917
+ init_media_flags();
2740
2918
 
2741
2919
  // src/auth/config-store.ts
2742
2920
  var import_node_fs3 = __toESM(require("fs"));
@@ -3262,6 +3440,7 @@ init_dist();
3262
3440
  // src/lib/post-create.ts
3263
3441
  var import_promises = __toESM(require("fs/promises"));
3264
3442
  var import_node_path3 = __toESM(require("path"));
3443
+ init_media_flags();
3265
3444
 
3266
3445
  // src/lib/account-select.ts
3267
3446
  var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
@@ -3935,14 +4114,14 @@ function createKbCommand() {
3935
4114
  process.exit(resolveExitCode6(err));
3936
4115
  }
3937
4116
  });
3938
- kb.command("create").description("Create a new knowledge base").requiredOption("--name <name>", "Knowledge base name").action(async (opts, cmd) => {
4117
+ kb.command("create").description("Create a new knowledge base").requiredOption("--name <name>", "Knowledge base name").requiredOption("--description <description>", "Knowledge base description (20\u2013500 characters)").action(async (opts, cmd) => {
3939
4118
  const globals = cmd.optsWithGlobals();
3940
4119
  const printer = createPrinter(globals);
3941
4120
  const spinner = printer.spinner("Creating knowledge base\u2026");
3942
4121
  try {
3943
4122
  const cred = resolveAuth({ apiKey: globals.apiKey });
3944
4123
  const client = new Autoposting({ apiKey: cred.apiKey });
3945
- const result = await client.kb.create({ name: opts.name });
4124
+ const result = await client.kb.create({ name: opts.name, description: opts.description });
3946
4125
  spinner.stop();
3947
4126
  printer.log(result);
3948
4127
  } catch (err) {
@@ -4719,8 +4898,73 @@ function createConfigCommand() {
4719
4898
  return config;
4720
4899
  }
4721
4900
 
4901
+ // src/commands/api.ts
4902
+ var import_commander20 = require("commander");
4903
+ init_dist();
4904
+ init_api_routes();
4905
+ function validationError(message) {
4906
+ return Object.assign(new Error(message), { exitCode: EXIT_CODES.VALIDATION_ERROR });
4907
+ }
4908
+ function collect(value, previous) {
4909
+ return [...previous, value];
4910
+ }
4911
+ function parseFields(pairs) {
4912
+ const out = {};
4913
+ for (const pair of pairs) {
4914
+ const i = pair.indexOf("=");
4915
+ if (i < 1) throw validationError(`Expected key=value, got "${pair}".`);
4916
+ const raw = pair.slice(i + 1);
4917
+ let value = raw;
4918
+ try {
4919
+ value = JSON.parse(raw);
4920
+ } catch {
4921
+ }
4922
+ out[pair.slice(0, i)] = value;
4923
+ }
4924
+ return out;
4925
+ }
4926
+ function createApiCommand() {
4927
+ 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) => {
4928
+ const globals = cmd.optsWithGlobals();
4929
+ const printer = createPrinter(globals);
4930
+ try {
4931
+ const cred = resolveAuth({ apiKey: globals.apiKey });
4932
+ let body;
4933
+ if (opts.data !== void 0) {
4934
+ try {
4935
+ body = JSON.parse(opts.data);
4936
+ } catch {
4937
+ throw validationError("--data is not valid JSON.");
4938
+ }
4939
+ }
4940
+ if (opts.field.length > 0) body = { ...body, ...parseFields(opts.field) };
4941
+ const query = opts.query.length > 0 ? parseFields(opts.query) : void 0;
4942
+ const client = new Autoposting({ apiKey: cred.apiKey });
4943
+ const routes = await fetchApiRoutes(client);
4944
+ if (opts.list !== void 0) {
4945
+ const prefix = typeof opts.list === "string" ? opts.list : "/";
4946
+ printer.log(routes.filter((r) => r.split(" ")[1].startsWith(prefix)));
4947
+ return;
4948
+ }
4949
+ if (!method || !path3) throw validationError("Usage: ap api <METHOD> <path>, or ap api --list.");
4950
+ const m = method.toUpperCase();
4951
+ try {
4952
+ assertApiRoute(routes, m, path3);
4953
+ } catch (err) {
4954
+ throw validationError(err.message);
4955
+ }
4956
+ const sendBody = m === "GET" || m === "DELETE" ? void 0 : body;
4957
+ printer.log(await client.request(m, path3, sendBody, query));
4958
+ } catch (err) {
4959
+ printer.error(err);
4960
+ const attached = err.exitCode;
4961
+ process.exit(typeof attached === "number" ? attached : exitCodeFromError(err));
4962
+ }
4963
+ });
4964
+ }
4965
+
4722
4966
  // 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) => {
4967
+ 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
4968
  const opts = thisCommand.opts();
4725
4969
  if (!opts.color) disableColor();
4726
4970
  });
@@ -4729,6 +4973,7 @@ program.addCommand(createBillingCommand());
4729
4973
  program.addCommand(createBrandsCommand());
4730
4974
  program.addCommand(createCarouselsCommand());
4731
4975
  program.addCommand(createClipsCommand());
4976
+ program.addCommand(createApiCommand());
4732
4977
  program.addCommand(createPostsCommand());
4733
4978
  program.addCommand(createAgentsCommand());
4734
4979
  program.addCommand(createKbCommand());