@autoposting.ai/cli 0.3.3 → 0.3.5

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
@@ -42,11 +42,44 @@ function createError(status, body) {
42
42
  if (status >= 500) return new ServerError(message, status, code);
43
43
  return new AutopostingError(message, status, code);
44
44
  }
45
- var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, ValidationError, RateLimitError, ServerError, Resource, MediaResource, AgentsResource, BillingResource, BrandsResource, CarouselsResource, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
45
+ function resolvePartSize(fileSize, requested) {
46
+ let size = requested && requested > 0 ? requested : DEFAULT_PART_SIZE;
47
+ if (Math.ceil(fileSize / size) > MAX_PARTS) size = Math.ceil(fileSize / MAX_PARTS);
48
+ return Math.max(size, MIN_PART_SIZE);
49
+ }
50
+ function guessVideoMime(fileName) {
51
+ const ext = fileName.split(".").pop()?.toLowerCase() ?? "";
52
+ return VIDEO_MIME[ext] ?? "video/mp4";
53
+ }
54
+ async function putPart(url, body) {
55
+ let res;
56
+ try {
57
+ res = await fetch(url, { method: "PUT", body });
58
+ } catch (err) {
59
+ throw new AutopostingError(
60
+ `Failed to upload part: ${err instanceof Error ? err.message : String(err)}`,
61
+ 0,
62
+ "UPLOAD_FAILED"
63
+ );
64
+ }
65
+ if (!res.ok) {
66
+ throw new AutopostingError(
67
+ `Failed to upload part: HTTP ${res.status} ${res.statusText}`,
68
+ res.status,
69
+ "UPLOAD_FAILED"
70
+ );
71
+ }
72
+ const etag = res.headers.get("etag");
73
+ if (!etag) {
74
+ throw new AutopostingError("Upload part response is missing the ETag header.", res.status, "UPLOAD_FAILED");
75
+ }
76
+ return etag;
77
+ }
78
+ var VERSION, AutopostingError, AuthenticationError, ScopeError, NotFoundError, ValidationError, RateLimitError, ServerError, Resource, MediaResource, AgentsResource, BillingResource, BrandsResource, CarouselsResource, MIN_PART_SIZE, DEFAULT_PART_SIZE, MAX_PARTS, VIDEO_MIME, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
46
79
  var init_dist = __esm({
47
80
  "../sdk/dist/index.mjs"() {
48
81
  "use strict";
49
- VERSION = "0.3.3";
82
+ VERSION = "0.3.5";
50
83
  AutopostingError = class extends Error {
51
84
  status;
52
85
  code;
@@ -76,20 +109,20 @@ var init_dist = __esm({
76
109
  this.client = client;
77
110
  }
78
111
  client;
79
- get(path2, query) {
80
- return this.client.request("GET", path2, void 0, query);
112
+ get(path3, query) {
113
+ return this.client.request("GET", path3, void 0, query);
81
114
  }
82
- post(path2, body) {
83
- return this.client.request("POST", path2, body);
115
+ post(path3, body) {
116
+ return this.client.request("POST", path3, body);
84
117
  }
85
- put(path2, body) {
86
- return this.client.request("PUT", path2, body);
118
+ put(path3, body) {
119
+ return this.client.request("PUT", path3, body);
87
120
  }
88
- patch(path2, body) {
89
- return this.client.request("PATCH", path2, body);
121
+ patch(path3, body) {
122
+ return this.client.request("PATCH", path3, body);
90
123
  }
91
- delete(path2) {
92
- return this.client.request("DELETE", path2);
124
+ delete(path3) {
125
+ return this.client.request("DELETE", path3);
93
126
  }
94
127
  };
95
128
  MediaResource = class extends Resource {
@@ -180,6 +213,17 @@ var init_dist = __esm({
180
213
  return this.client.request("DELETE", `/carousels/${id}`);
181
214
  }
182
215
  };
216
+ MIN_PART_SIZE = 5 * 1024 * 1024;
217
+ DEFAULT_PART_SIZE = 16 * 1024 * 1024;
218
+ MAX_PARTS = 1e4;
219
+ VIDEO_MIME = {
220
+ mp4: "video/mp4",
221
+ mov: "video/quicktime",
222
+ webm: "video/webm",
223
+ mkv: "video/x-matroska",
224
+ avi: "video/x-msvideo",
225
+ m4v: "video/x-m4v"
226
+ };
183
227
  ClipsResource = class extends Resource {
184
228
  /** GET /clips — backend returns `{ clips, pagination }`, not a bare array. */
185
229
  list() {
@@ -188,16 +232,66 @@ var init_dist = __esm({
188
232
  retrieve(id) {
189
233
  return this.client.request("GET", `/clips/${id}`);
190
234
  }
191
- // ponytail: the backend has no single-request /clips/upload; uploading a file means the
192
- // multipart init/presign/complete flow (upload-initiate → upload-presign → upload-complete).
193
- // That's too much surface for v0.3.1 — fail honestly and point at importUrl. Upgrade path:
194
- // implement the init/presign/complete dance here when direct upload is actually needed.
195
- async upload(_filePath, _options) {
196
- throw new AutopostingError(
197
- "Direct clip file upload is not supported by the API yet \u2014 use `importUrl` with a public video URL instead.",
198
- 0,
199
- "NOT_IMPLEMENTED"
235
+ /**
236
+ * Upload a local video file as a clip by driving the backend multipart flow:
237
+ * `init → presign → PUT each part → complete`. On any failure after init, the
238
+ * multipart upload is aborted so it leaves no dangling clip. Returns the created clip.
239
+ *
240
+ * Node-only: reads the file in chunks (memory-bounded for multi-GB videos). `node:fs`
241
+ * is imported dynamically so the rest of the SDK stays runtime-agnostic.
242
+ */
243
+ async upload(filePath, options) {
244
+ if (!options?.brandId) {
245
+ throw new AutopostingError("brandId is required to upload a clip.", 0, "VALIDATION_ERROR");
246
+ }
247
+ const { open: open2, stat } = await import("fs/promises");
248
+ const { basename } = await import("path");
249
+ const fileName = basename(filePath);
250
+ const title = options.title ?? fileName;
251
+ const mimeType = options.contentType ?? guessVideoMime(fileName);
252
+ const { size: fileSize } = await stat(filePath);
253
+ if (fileSize <= 0) {
254
+ throw new AutopostingError(`File is empty: ${filePath}`, 0, "VALIDATION_ERROR");
255
+ }
256
+ const partSize = resolvePartSize(fileSize, options.partSize);
257
+ const partNumbers = Array.from(
258
+ { length: Math.max(1, Math.ceil(fileSize / partSize)) },
259
+ (_, i) => i + 1
200
260
  );
261
+ const { clipId, uploadId, key } = await this.client.request("POST", "/clips/upload/init", { brandId: options.brandId, title, fileName, fileSize, mimeType });
262
+ let completed;
263
+ const handle = await open2(filePath, "r");
264
+ try {
265
+ const { presignedUrls } = await this.client.request("POST", "/clips/upload/presign", { clipId, uploadId, key, partNumbers });
266
+ const urlByPart = new Map(presignedUrls.map((p) => [p.partNumber, p.url]));
267
+ const parts = [];
268
+ let uploaded = 0;
269
+ for (const partNumber of partNumbers) {
270
+ const url = urlByPart.get(partNumber);
271
+ if (!url) {
272
+ throw new AutopostingError(`Missing presigned URL for part ${partNumber}.`, 0, "UPLOAD_FAILED");
273
+ }
274
+ const offset = (partNumber - 1) * partSize;
275
+ const length = Math.min(partSize, fileSize - offset);
276
+ const buffer = Buffer.allocUnsafe(length);
277
+ await handle.read(buffer, 0, length, offset);
278
+ parts.push({ partNumber, etag: await putPart(url, buffer) });
279
+ uploaded += length;
280
+ options.onProgress?.(Math.round(uploaded / fileSize * 100));
281
+ }
282
+ completed = await this.client.request(
283
+ "POST",
284
+ "/clips/upload/complete",
285
+ { clipId, uploadId, key, parts }
286
+ );
287
+ } catch (err) {
288
+ await this.client.request("POST", "/clips/upload/abort", { clipId, uploadId, key }).catch(() => {
289
+ });
290
+ throw err;
291
+ } finally {
292
+ await handle.close();
293
+ }
294
+ return this.retrieve(completed.clipId);
201
295
  }
202
296
  /** POST /clips/import-url — backend returns `{ clipId }`, not a full Clip. */
203
297
  importUrl(params) {
@@ -425,7 +519,7 @@ var init_dist = __esm({
425
519
  * failures — network errors, timeouts, and 5xx — with exponential backoff + jitter.
426
520
  * Non-idempotent methods (POST/PATCH) are never retried, to avoid duplicate side effects.
427
521
  */
428
- async request(method, path2, body, query) {
522
+ async request(method, path3, body, query) {
429
523
  let lastErr;
430
524
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
431
525
  if (attempt > 0) {
@@ -433,7 +527,7 @@ var init_dist = __esm({
433
527
  await sleep(backoff + Math.floor(Math.random() * this.retryBaseMs));
434
528
  }
435
529
  try {
436
- return await this._send(method, path2, body, query);
530
+ return await this._send(method, path3, body, query);
437
531
  } catch (err) {
438
532
  lastErr = err;
439
533
  if (attempt < this.maxRetries && this.isRetryable(method, err)) continue;
@@ -448,8 +542,8 @@ var init_dist = __esm({
448
542
  if (err.code === "NETWORK_ERROR" || err.code === "TIMEOUT") return true;
449
543
  return RETRYABLE_STATUS.has(err.status);
450
544
  }
451
- async _send(method, path2, body, query) {
452
- let url = `${this.baseUrl}${path2}`;
545
+ async _send(method, path3, body, query) {
546
+ let url = `${this.baseUrl}${path3}`;
453
547
  if (query && Object.keys(query).length > 0) {
454
548
  const params = new URLSearchParams();
455
549
  for (const [k, v] of Object.entries(query)) {
@@ -483,13 +577,13 @@ var init_dist = __esm({
483
577
  } catch (err) {
484
578
  if (err instanceof Error && err.name === "AbortError") {
485
579
  throw new AutopostingError(
486
- `Request to ${method} ${path2} timed out after ${this.timeout}ms.`,
580
+ `Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
487
581
  0,
488
582
  "TIMEOUT"
489
583
  );
490
584
  }
491
585
  throw new AutopostingError(
492
- `Network request to ${method} ${path2} failed: ${err instanceof Error ? err.message : String(err)}`,
586
+ `Network request to ${method} ${path3} failed: ${err instanceof Error ? err.message : String(err)}`,
493
587
  0,
494
588
  "NETWORK_ERROR"
495
589
  );
@@ -534,7 +628,7 @@ var init_dist = __esm({
534
628
  parsed = JSON.parse(text);
535
629
  } catch {
536
630
  throw new AutopostingError(
537
- `Expected a JSON response from ${method} ${path2} but received non-JSON (HTTP ${response.status}). Verify the API base URL (AUTOPOSTING_BASE_URL / --base-url) points at the API, not the web app.`,
631
+ `Expected a JSON response from ${method} ${path3} but received non-JSON (HTTP ${response.status}). Verify the API base URL (AUTOPOSTING_BASE_URL / --base-url) points at the API, not the web app.`,
538
632
  response.status,
539
633
  "INVALID_RESPONSE"
540
634
  );
@@ -547,7 +641,7 @@ var init_dist = __esm({
547
641
  } catch (err) {
548
642
  if (err instanceof Error && err.name === "AbortError") {
549
643
  throw new AutopostingError(
550
- `Request to ${method} ${path2} timed out after ${this.timeout}ms.`,
644
+ `Request to ${method} ${path3} timed out after ${this.timeout}ms.`,
551
645
  0,
552
646
  "TIMEOUT"
553
647
  );
@@ -1675,7 +1769,7 @@ var init_server = __esm({
1675
1769
  });
1676
1770
 
1677
1771
  // src/cli.ts
1678
- var import_commander19 = require("commander");
1772
+ var import_commander20 = require("commander");
1679
1773
  init_dist();
1680
1774
 
1681
1775
  // src/commands/agents.ts
@@ -1823,6 +1917,67 @@ function formatError(error, mode) {
1823
1917
  if (mode === "tty") return red(`Error: ${message}`);
1824
1918
  return JSON.stringify({ error: message }, null, 2);
1825
1919
  }
1920
+ function parseJq(expr) {
1921
+ const e = expr.trim();
1922
+ if (e === "" || e === ".") return [];
1923
+ if (!e.startsWith(".")) {
1924
+ throw new Error(`--jq: expression must start with "." (got "${expr}")`);
1925
+ }
1926
+ const body = e.slice(1);
1927
+ const ops = [];
1928
+ let i = 0;
1929
+ while (i < body.length) {
1930
+ const ch = body[i];
1931
+ if (ch === ".") {
1932
+ i++;
1933
+ continue;
1934
+ }
1935
+ if (ch === "[") {
1936
+ const close = body.indexOf("]", i);
1937
+ if (close === -1) throw new Error(`--jq: unclosed "[" in "${expr}"`);
1938
+ const inner = body.slice(i + 1, close).trim();
1939
+ if (inner === "") {
1940
+ ops.push({ type: "iter" });
1941
+ } else {
1942
+ const n = Number(inner);
1943
+ if (!Number.isInteger(n)) throw new Error(`--jq: invalid index "[${inner}]" in "${expr}"`);
1944
+ ops.push({ type: "index", n });
1945
+ }
1946
+ i = close + 1;
1947
+ continue;
1948
+ }
1949
+ const m = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(body.slice(i));
1950
+ if (!m) throw new Error(`--jq: unexpected character "${ch}" in "${expr}"`);
1951
+ ops.push({ type: "prop", name: m[0] });
1952
+ i += m[0].length;
1953
+ }
1954
+ return ops;
1955
+ }
1956
+ function applyJq(data, expr) {
1957
+ const ops = parseJq(expr);
1958
+ let stream = [data];
1959
+ for (const op of ops) {
1960
+ const next = [];
1961
+ for (const v of stream) {
1962
+ if (op.type === "prop") {
1963
+ next.push(v == null ? null : v[op.name] ?? null);
1964
+ } else if (op.type === "index") {
1965
+ if (!Array.isArray(v)) throw new Error(`--jq: cannot index non-array with [${op.n}]`);
1966
+ const idx = op.n < 0 ? v.length + op.n : op.n;
1967
+ next.push(v[idx] ?? null);
1968
+ } else {
1969
+ if (Array.isArray(v)) next.push(...v);
1970
+ else if (v && typeof v === "object") next.push(...Object.values(v));
1971
+ else throw new Error(`--jq: cannot iterate over ${v === null ? "null" : typeof v} \u2014 .[] needs an array or object`);
1972
+ }
1973
+ }
1974
+ stream = next;
1975
+ }
1976
+ return stream;
1977
+ }
1978
+ function formatJq(data, expr) {
1979
+ return applyJq(data, expr).map((r) => typeof r === "string" ? r : r === void 0 ? "null" : JSON.stringify(r)).join("\n");
1980
+ }
1826
1981
 
1827
1982
  // src/output/spinner.ts
1828
1983
  var NO_OP_SPINNER = {
@@ -1870,19 +2025,23 @@ function createSpinner(mode) {
1870
2025
 
1871
2026
  // src/output/printer.ts
1872
2027
  var Printer = class {
1873
- constructor(mode) {
2028
+ constructor(mode, jq) {
1874
2029
  this.mode = mode;
2030
+ this.jq = jq;
1875
2031
  }
1876
2032
  mode;
2033
+ jq;
1877
2034
  /** True only for human-readable output, where extra hint lines are safe to print. */
1878
2035
  isTty() {
1879
- return this.mode === "tty";
2036
+ return this.mode === "tty" && !this.jq;
1880
2037
  }
1881
2038
  log(data) {
2039
+ if (this.jq) return this.emitJq(data);
1882
2040
  const out = formatOutput(data, this.mode);
1883
2041
  if (out !== "") console.log(out);
1884
2042
  }
1885
2043
  table(rows, columns) {
2044
+ if (this.jq) return this.emitJq(rows);
1886
2045
  if (this.mode === "quiet") return;
1887
2046
  if (this.mode === "json") {
1888
2047
  console.log(formatOutput(rows, "json"));
@@ -1891,19 +2050,23 @@ var Printer = class {
1891
2050
  const out = formatTable(rows, columns);
1892
2051
  if (out !== "") console.log(out);
1893
2052
  }
2053
+ emitJq(data) {
2054
+ const out = formatJq(data, this.jq);
2055
+ if (out !== "") console.log(out);
2056
+ }
1894
2057
  error(err) {
1895
- const out = formatError(err, this.mode);
2058
+ const out = formatError(err, this.jq ? "json" : this.mode);
1896
2059
  console.error(out);
1897
2060
  }
1898
2061
  spinner(message) {
1899
- const s = createSpinner(this.mode);
2062
+ const s = createSpinner(this.jq ? "json" : this.mode);
1900
2063
  s.start(message);
1901
2064
  return s;
1902
2065
  }
1903
2066
  };
1904
2067
  function createPrinter(options) {
1905
2068
  const mode = detectOutputMode(options);
1906
- return new Printer(mode);
2069
+ return new Printer(mode, options.jq);
1907
2070
  }
1908
2071
 
1909
2072
  // src/output/exit-codes.ts
@@ -2400,6 +2563,195 @@ function createBillingCommand() {
2400
2563
  // src/commands/brands.ts
2401
2564
  var import_commander4 = require("commander");
2402
2565
  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
+ }
2685
+
2686
+ // src/auth/config-store.ts
2687
+ var import_node_fs3 = __toESM(require("fs"));
2688
+ var import_node_os2 = __toESM(require("os"));
2689
+ var import_node_path2 = __toESM(require("path"));
2690
+ function getConfigPath() {
2691
+ const xdg = process.env.XDG_CONFIG_HOME;
2692
+ const base = xdg && import_node_path2.default.isAbsolute(xdg) ? xdg : import_node_path2.default.join(import_node_os2.default.homedir(), ".config");
2693
+ return import_node_path2.default.join(base, "autoposting", "config.json");
2694
+ }
2695
+ function readConfig() {
2696
+ try {
2697
+ return JSON.parse(import_node_fs3.default.readFileSync(getConfigPath(), "utf8"));
2698
+ } catch {
2699
+ return null;
2700
+ }
2701
+ }
2702
+ function writeConfig(cfg) {
2703
+ const filePath = getConfigPath();
2704
+ import_node_fs3.default.mkdirSync(import_node_path2.default.dirname(filePath), { recursive: true, mode: 448 });
2705
+ const tmp = `${filePath}.${process.pid}.tmp`;
2706
+ try {
2707
+ import_node_fs3.default.writeFileSync(tmp, JSON.stringify(cfg, null, 2), { encoding: "utf8", mode: 384 });
2708
+ import_node_fs3.default.renameSync(tmp, filePath);
2709
+ } catch (err) {
2710
+ try {
2711
+ import_node_fs3.default.unlinkSync(tmp);
2712
+ } catch {
2713
+ }
2714
+ throw err;
2715
+ }
2716
+ import_node_fs3.default.chmodSync(filePath, 384);
2717
+ }
2718
+ function getContextBrand() {
2719
+ return readConfig()?.context?.brand ?? null;
2720
+ }
2721
+ function setContextBrand(brand) {
2722
+ const cfg = readConfig() ?? {};
2723
+ cfg.context = { ...cfg.context, brand };
2724
+ writeConfig(cfg);
2725
+ }
2726
+ function unsetContextBrand() {
2727
+ const cfg = readConfig();
2728
+ if (!cfg?.context?.brand) return;
2729
+ delete cfg.context.brand;
2730
+ writeConfig(cfg);
2731
+ }
2732
+ function resolveBrand(optBrand) {
2733
+ return optBrand ?? getContextBrand();
2734
+ }
2735
+ function getDefaultAccount(brand, platform) {
2736
+ return readConfig()?.defaultAccounts?.[brand]?.[platform] ?? null;
2737
+ }
2738
+ function getDefaultAccounts(brand) {
2739
+ return readConfig()?.defaultAccounts?.[brand] ?? {};
2740
+ }
2741
+ function setDefaultAccount(brand, platform, value) {
2742
+ const cfg = readConfig() ?? {};
2743
+ cfg.defaultAccounts ??= {};
2744
+ cfg.defaultAccounts[brand] = { ...cfg.defaultAccounts[brand], [platform]: value };
2745
+ writeConfig(cfg);
2746
+ }
2747
+ function clearDefaultAccounts(brand) {
2748
+ const cfg = readConfig();
2749
+ if (!cfg?.defaultAccounts?.[brand]) return;
2750
+ delete cfg.defaultAccounts[brand];
2751
+ writeConfig(cfg);
2752
+ }
2753
+
2754
+ // src/commands/brands.ts
2403
2755
  function tokenStatus(c, now = Date.now()) {
2404
2756
  if (!c.connected) return "\u2014";
2405
2757
  if (c.refreshError) return "expired";
@@ -2533,6 +2885,28 @@ function createBrandsCommand() {
2533
2885
  process.exit(exitCodeFromError(err));
2534
2886
  }
2535
2887
  });
2888
+ brands.command("set-default-account <slug> <pairs...>").description("Save default target account(s) for a brand, e.g. x=@handle linkedin=all").action((slug, pairs, _opts, cmd) => {
2889
+ const printer = createPrinter(cmd.optsWithGlobals());
2890
+ try {
2891
+ const parsed = parsePairs("--account", pairs);
2892
+ for (const [platform, value] of Object.entries(parsed)) {
2893
+ setDefaultAccount(slug, platform, value);
2894
+ }
2895
+ printer.log(getDefaultAccounts(slug));
2896
+ } catch (err) {
2897
+ printer.error(err);
2898
+ process.exit(1);
2899
+ }
2900
+ });
2901
+ brands.command("get-default-account <slug>").description("Show saved default account(s) for a brand").action((slug, _opts, cmd) => {
2902
+ const printer = createPrinter(cmd.optsWithGlobals());
2903
+ printer.log(getDefaultAccounts(slug));
2904
+ });
2905
+ brands.command("clear-default-account <slug>").description("Remove all saved default accounts for a brand").action((slug, _opts, cmd) => {
2906
+ const printer = createPrinter(cmd.optsWithGlobals());
2907
+ clearDefaultAccounts(slug);
2908
+ printer.log(getDefaultAccounts(slug));
2909
+ });
2536
2910
  return brands;
2537
2911
  }
2538
2912
 
@@ -2670,6 +3044,17 @@ function resolveExitCode4(err) {
2670
3044
  if (typeof attached === "number") return attached;
2671
3045
  return exitCodeFromError(err);
2672
3046
  }
3047
+ async function resolveBrandId(client, explicit) {
3048
+ if (explicit) return explicit;
3049
+ const brands = await client.brands.list();
3050
+ if (brands.length === 1) return brands[0].id;
3051
+ if (brands.length === 0) {
3052
+ throw new Error("No brands found. Create a brand first, then pass --brand <id>.");
3053
+ }
3054
+ throw new Error(
3055
+ `Multiple brands found \u2014 pass --brand <id>. Available: ${brands.map((b) => `${b.name} (${b.id})`).join(", ")}`
3056
+ );
3057
+ }
2673
3058
  function createClipsCommand() {
2674
3059
  const clips = new import_commander6.Command("clips").description("Manage video clips");
2675
3060
  clips.command("list").description("List all clips").action(async (_opts, cmd) => {
@@ -2710,15 +3095,17 @@ function createClipsCommand() {
2710
3095
  process.exit(resolveExitCode4(err));
2711
3096
  }
2712
3097
  });
2713
- clips.command("upload <file>").description("Upload a video file as a clip").option("--name <name>", "Override the clip name (defaults to filename)").action(async (file, opts, cmd) => {
3098
+ clips.command("upload <file>").description("Upload a video file as a clip").option("--brand <id>", "Brand to attach the clip to (defaults to your only brand)").option("--name <name>", "Override the clip title (defaults to filename)").action(async (file, opts, cmd) => {
2714
3099
  const globals = cmd.optsWithGlobals();
2715
3100
  const printer = createPrinter(globals);
2716
3101
  const spinner = printer.spinner("Uploading clip\u2026");
2717
3102
  try {
2718
3103
  const cred = resolveAuth({ apiKey: globals.apiKey });
2719
3104
  const client = new Autoposting({ apiKey: cred.apiKey });
3105
+ const brandId = await resolveBrandId(client, opts.brand);
2720
3106
  const clip = await client.clips.upload(file, {
2721
- name: opts.name,
3107
+ brandId,
3108
+ ...opts.name ? { title: opts.name } : {},
2722
3109
  onProgress: (pct) => {
2723
3110
  if (pct > 0 && pct < 100) spinner.stop();
2724
3111
  }
@@ -2792,136 +3179,27 @@ function createClipsCommand() {
2792
3179
  // src/commands/posts.ts
2793
3180
  var import_commander7 = require("commander");
2794
3181
  init_dist();
2795
- var import_promises = __toESM(require("fs/promises"));
2796
- var import_node_path2 = __toESM(require("path"));
2797
3182
 
2798
- // src/lib/media-flags.ts
2799
- var import_node_fs2 = require("fs");
2800
- var VALID_PLATFORMS = ["x", "linkedin", "instagram", "threads", "youtube"];
2801
- var EXT_TO_MIME = {
2802
- jpg: "image/jpeg",
2803
- jpeg: "image/jpeg",
2804
- png: "image/png",
2805
- gif: "image/gif",
2806
- webp: "image/webp",
2807
- mp4: "video/mp4",
2808
- mov: "video/quicktime",
2809
- webm: "video/webm"
2810
- };
2811
- function extToMime(filename) {
2812
- const ext = filename.split(".").pop()?.toLowerCase() ?? "";
2813
- const mime = EXT_TO_MIME[ext];
2814
- if (!mime) {
2815
- throw new Error(
2816
- `Unsupported file extension ".${ext || "(none)"}" in "${filename}". Supported: ${Object.keys(EXT_TO_MIME).join(", ")}`
2817
- );
2818
- }
2819
- return mime;
2820
- }
2821
- function parsePairs(flag, vals) {
2822
- const result = {};
2823
- for (const val of vals) {
2824
- const eqIdx = val.indexOf("=");
2825
- if (eqIdx < 1) {
2826
- throw new Error(`${flag}: expected "platform=value" format, got "${val}"`);
2827
- }
2828
- const p = val.slice(0, eqIdx).trim();
2829
- const v = val.slice(eqIdx + 1);
2830
- if (!VALID_PLATFORMS.includes(p)) {
2831
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2832
- }
2833
- result[p] = v;
2834
- }
2835
- return result;
2836
- }
2837
- function parsePlatformMediaPairs(flag, vals) {
2838
- const result = {};
2839
- for (const val of vals) {
2840
- const eqIdx = val.indexOf("=");
2841
- if (eqIdx < 1) {
2842
- throw new Error(`${flag}: expected "platform=path[,path...]" format, got "${val}"`);
2843
- }
2844
- const p = val.slice(0, eqIdx).trim();
2845
- const paths = val.slice(eqIdx + 1).split(",").map((s) => s.trim()).filter(Boolean);
2846
- if (!VALID_PLATFORMS.includes(p)) {
2847
- throw new Error(`${flag}: unknown platform "${p}". Valid: ${VALID_PLATFORMS.join(", ")}`);
2848
- }
2849
- result[p] = paths;
2850
- }
2851
- return result;
2852
- }
2853
- function validateMediaExtensions(paths) {
2854
- for (const p of paths) extToMime(p);
2855
- }
2856
- function validateMediaCount(paths) {
2857
- if (paths.length > 10) {
2858
- throw new Error(`--media accepts at most 10 files (received ${paths.length}).`);
2859
- }
2860
- }
2861
- function validateMediaPaths(paths) {
2862
- for (const p of paths) {
2863
- if (!(0, import_node_fs2.existsSync)(p)) {
2864
- throw new Error(`Media file not found: "${p}"`);
2865
- }
2866
- }
2867
- }
2868
- function alignAltText(mediaPaths, altTexts) {
2869
- if (altTexts.length > mediaPaths.length) {
2870
- throw new Error(
2871
- `--alt-text has more values (${altTexts.length}) than --media (${mediaPaths.length}). Alt text aligns to media by index.`
2872
- );
2873
- }
2874
- return mediaPaths.map((_, i) => altTexts[i]);
2875
- }
2876
- function buildYoutubeOptions(opts) {
2877
- const { ytTitle, ytDescription, ytTags, ytPrivacy, ytCategory, ytMadeForKids } = opts;
2878
- const o = {};
2879
- if (ytTitle) o.title = ytTitle;
2880
- if (ytDescription) o.description = ytDescription;
2881
- if (ytTags) o.tags = ytTags.split(",").map((t) => t.trim()).filter(Boolean);
2882
- if (ytPrivacy) o.privacyStatus = ytPrivacy;
2883
- if (ytCategory) o.categoryId = ytCategory;
2884
- if (ytMadeForKids !== void 0) o.madeForKids = ytMadeForKids;
2885
- return Object.keys(o).length > 0 ? o : void 0;
2886
- }
2887
- function buildInstagramOptions(opts) {
2888
- const { igReel, igShareToFeed, igCoverUrl, igThumbOffsetMs, igCollaborators } = opts;
2889
- const hasAnyReelFlag = igReel !== void 0 || igShareToFeed !== void 0 || igCoverUrl !== void 0 || igThumbOffsetMs !== void 0 || igCollaborators !== void 0;
2890
- if (!hasAnyReelFlag) return void 0;
2891
- const reel = {};
2892
- if (igShareToFeed !== void 0) reel.shareToFeed = igShareToFeed;
2893
- if (igCoverUrl) reel.coverUrl = igCoverUrl;
2894
- if (igThumbOffsetMs !== void 0) {
2895
- const n = Number(igThumbOffsetMs);
2896
- if (!Number.isFinite(n) || n < 0) {
2897
- throw new Error(
2898
- `--ig-thumb-offset-ms must be a non-negative number (received "${igThumbOffsetMs}")`
2899
- );
2900
- }
2901
- reel.thumbOffsetMs = n;
2902
- }
2903
- if (igCollaborators) {
2904
- reel.collaborators = igCollaborators.split(",").map((s) => s.trim()).filter(Boolean);
2905
- }
2906
- return { reel };
2907
- }
2908
- function buildThreadsOptions(opts) {
2909
- const { threadsReplyTo, threadsReplyControl } = opts;
2910
- if (!threadsReplyTo && !threadsReplyControl) return void 0;
2911
- const o = {};
2912
- if (threadsReplyTo) o.replyToId = threadsReplyTo;
2913
- if (threadsReplyControl) o.replyControl = threadsReplyControl;
2914
- return o;
2915
- }
3183
+ // src/lib/post-create.ts
3184
+ var import_promises = __toESM(require("fs/promises"));
3185
+ var import_node_path3 = __toESM(require("path"));
2916
3186
 
2917
3187
  // src/lib/account-select.ts
2918
3188
  var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
3189
+ var FANOUT_CONFIRM_THRESHOLD = 5;
3190
+ var PAGED_PICKER_THRESHOLD = 8;
3191
+ var isAll = (v) => v === "all" || v === "*";
3192
+ function needsFanoutConfirm(count, isTty, explicit) {
3193
+ return isTty && explicit && count > FANOUT_CONFIRM_THRESHOLD;
3194
+ }
2919
3195
  async function resolveTargetAccounts({
2920
3196
  brandSlug,
2921
3197
  platforms,
2922
3198
  accountFlags,
2923
3199
  client,
2924
- isTty
3200
+ isTty,
3201
+ emit = (m) => process.stderr.write(`${m}
3202
+ `)
2925
3203
  }) {
2926
3204
  const accountMap = {};
2927
3205
  for (const flag of accountFlags) {
@@ -2952,8 +3230,24 @@ async function resolveTargetAccounts({
2952
3230
  const result = {};
2953
3231
  for (const platform of platforms) {
2954
3232
  const accounts = byPlatform[platform] ?? [];
2955
- const specifiedValue = accountMap[platform];
2956
- if (specifiedValue !== void 0) {
3233
+ const flagValue = accountMap[platform];
3234
+ const explicit = flagValue !== void 0;
3235
+ const specifiedValue = flagValue ?? getDefaultAccount(brandSlug, platform) ?? void 0;
3236
+ if (specifiedValue !== void 0 && isAll(specifiedValue)) {
3237
+ const ids = accounts.map((a) => a.platformUserId).filter((id) => Boolean(id));
3238
+ emit(`${platform}: posting to all ${ids.length} connected account(s).`);
3239
+ if (needsFanoutConfirm(ids.length, isTty, explicit)) {
3240
+ const { confirm } = await import("@inquirer/prompts");
3241
+ const ok2 = await confirm({
3242
+ message: `Post to all ${ids.length} ${platform} accounts?`,
3243
+ default: false
3244
+ });
3245
+ if (!ok2) {
3246
+ throw Object.assign(new Error("Aborted: fan-out not confirmed."), { exitCode: 1 });
3247
+ }
3248
+ }
3249
+ if (ids.length > 0) result[platform] = ids;
3250
+ } else if (specifiedValue !== void 0) {
2957
3251
  const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
2958
3252
  const match = accounts.find(
2959
3253
  (a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
@@ -2980,7 +3274,12 @@ ${valid || " (none)"}`
2980
3274
  }));
2981
3275
  const selected = await checkbox({
2982
3276
  message: `Multiple ${platform} accounts connected \u2014 select which to post to:`,
2983
- choices
3277
+ choices,
3278
+ // ponytail: paged scroll for long lists (23 LinkedIn orgs). @inquirer's
3279
+ // checkbox has no type-to-filter; if many-account users need it, swap in
3280
+ // a search-capable multiselect dep — or save a default to skip the picker.
3281
+ pageSize: accounts.length >= PAGED_PICKER_THRESHOLD ? 12 : 7,
3282
+ loop: false
2984
3283
  });
2985
3284
  const valid = selected.filter(Boolean);
2986
3285
  if (valid.length > 0) result[platform] = valid;
@@ -3001,7 +3300,7 @@ Pass --account ${platform}=<handle|id> to select.`
3001
3300
  return result;
3002
3301
  }
3003
3302
 
3004
- // src/commands/posts.ts
3303
+ // src/lib/post-create.ts
3005
3304
  var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
3006
3305
  function parsePlatforms(raw) {
3007
3306
  const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
@@ -3016,13 +3315,6 @@ function parsePlatforms(raw) {
3016
3315
  }
3017
3316
  return parts;
3018
3317
  }
3019
- function parsePositiveInt(value, flag) {
3020
- const n = Number(value);
3021
- if (!Number.isInteger(n) || n < 1) {
3022
- throw new Error(`${flag} must be a positive integer (received "${value}").`);
3023
- }
3024
- return n;
3025
- }
3026
3318
  function validateScheduledAt(value) {
3027
3319
  const ms = Date.parse(value);
3028
3320
  if (Number.isNaN(ms) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
@@ -3037,6 +3329,228 @@ function validateScheduledAt(value) {
3037
3329
  }
3038
3330
  return value;
3039
3331
  }
3332
+ async function buildAndCreatePost(client, fields, opts) {
3333
+ const platforms = parsePlatforms(fields.platforms);
3334
+ const scheduledAt = fields.at ? validateScheduledAt(fields.at) : void 0;
3335
+ if (fields.media && fields.media.length > 0) {
3336
+ validateMediaCount(fields.media);
3337
+ validateMediaPaths(fields.media);
3338
+ validateMediaExtensions(fields.media);
3339
+ }
3340
+ const platformTexts = fields.platformText && fields.platformText.length > 0 ? parsePairs("--platform-text", fields.platformText) : void 0;
3341
+ const platformMediaPaths = fields.platformMedia && fields.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", fields.platformMedia) : {};
3342
+ for (const paths of Object.values(platformMediaPaths)) {
3343
+ validateMediaPaths(paths);
3344
+ validateMediaExtensions(paths);
3345
+ }
3346
+ const altTexts = alignAltText(fields.media ?? [], fields.altText ?? []);
3347
+ const youtubeOptions = buildYoutubeOptions({
3348
+ ytTitle: fields.ytTitle,
3349
+ ytDescription: fields.ytDescription,
3350
+ ytTags: fields.ytTags,
3351
+ ytPrivacy: fields.ytPrivacy,
3352
+ ytCategory: fields.ytCategory,
3353
+ ytMadeForKids: fields.ytMadeForKids
3354
+ });
3355
+ const instagramOptions = buildInstagramOptions({
3356
+ igReel: fields.igReel,
3357
+ igShareToFeed: fields.igShareToFeed,
3358
+ igCoverUrl: fields.igCoverUrl,
3359
+ igThumbOffsetMs: fields.igThumbOffsetMs,
3360
+ igCollaborators: fields.igCollaborators
3361
+ });
3362
+ const threadsOptions = buildThreadsOptions({
3363
+ threadsReplyTo: fields.threadsReplyTo,
3364
+ threadsReplyControl: fields.threadsReplyControl
3365
+ });
3366
+ const targetAccountIds = await resolveTargetAccounts({
3367
+ brandSlug: fields.brandSlug,
3368
+ platforms,
3369
+ accountFlags: fields.account ?? [],
3370
+ client,
3371
+ isTty: opts.isTty,
3372
+ emit: opts.emit
3373
+ });
3374
+ const common = {
3375
+ brandSlug: fields.brandSlug,
3376
+ text: fields.text,
3377
+ platforms,
3378
+ ...scheduledAt ? { scheduledAt } : {},
3379
+ ...fields.thread && fields.thread.length > 0 ? { thread: fields.thread } : {},
3380
+ ...platformTexts && Object.keys(platformTexts).length > 0 ? { platformTexts } : {},
3381
+ ...Object.keys(targetAccountIds).length > 0 ? { targetAccountIds } : {},
3382
+ ...instagramOptions ? { instagramOptions } : {},
3383
+ ...threadsOptions ? { threadsOptions } : {},
3384
+ ...youtubeOptions ? { youtubeOptions } : {},
3385
+ source: "cli"
3386
+ };
3387
+ if (opts.dryRun) {
3388
+ return {
3389
+ dryRun: true,
3390
+ request: {
3391
+ ...common,
3392
+ ...fields.media && fields.media.length > 0 ? {
3393
+ media: fields.media.map((path3, i) => ({
3394
+ path: path3,
3395
+ ...altTexts[i] ? { altText: altTexts[i] } : {}
3396
+ }))
3397
+ } : {},
3398
+ ...Object.keys(platformMediaPaths).length > 0 ? { platformMedia: platformMediaPaths } : {}
3399
+ }
3400
+ };
3401
+ }
3402
+ opts.onBeforeNetwork?.();
3403
+ const mediaInputs = [];
3404
+ for (let i = 0; i < (fields.media ?? []).length; i++) {
3405
+ const filePath = fields.media[i];
3406
+ const data = await import_promises.default.readFile(filePath);
3407
+ const filename = import_node_path3.default.basename(filePath);
3408
+ const uploaded = await client.media.upload({
3409
+ data: new Uint8Array(data),
3410
+ filename,
3411
+ contentType: extToMime(filename)
3412
+ });
3413
+ mediaInputs.push({
3414
+ url: uploaded.url,
3415
+ type: uploaded.type,
3416
+ ...altTexts[i] ? { altText: altTexts[i] } : {}
3417
+ });
3418
+ }
3419
+ const platformMediaResult = {};
3420
+ for (const [p, paths] of Object.entries(platformMediaPaths)) {
3421
+ const uploads = [];
3422
+ for (const filePath of paths) {
3423
+ const data = await import_promises.default.readFile(filePath);
3424
+ const filename = import_node_path3.default.basename(filePath);
3425
+ const uploaded = await client.media.upload({
3426
+ data: new Uint8Array(data),
3427
+ filename,
3428
+ contentType: extToMime(filename)
3429
+ });
3430
+ uploads.push({ url: uploaded.url, type: uploaded.type });
3431
+ }
3432
+ platformMediaResult[p] = uploads;
3433
+ }
3434
+ return client.posts.create({
3435
+ ...common,
3436
+ ...mediaInputs.length > 0 ? { media: mediaInputs } : {},
3437
+ ...Object.keys(platformMediaResult).length > 0 ? { platformMedia: platformMediaResult } : {}
3438
+ });
3439
+ }
3440
+
3441
+ // src/lib/post-bulk.ts
3442
+ var import_node_fs4 = __toESM(require("fs"));
3443
+ function parseBulkFile(filePath) {
3444
+ const raw = import_node_fs4.default.readFileSync(filePath, "utf8");
3445
+ const isJson = filePath.toLowerCase().endsWith(".json") || raw.trimStart().startsWith("[");
3446
+ const rows = isJson ? parseJsonRows(raw) : parseCsvRows(raw);
3447
+ if (rows.length === 0) {
3448
+ throw new Error(`No records found in ${filePath}.`);
3449
+ }
3450
+ return rows;
3451
+ }
3452
+ function parseJsonRows(raw) {
3453
+ let data;
3454
+ try {
3455
+ data = JSON.parse(raw);
3456
+ } catch (e) {
3457
+ throw new Error(`Invalid JSON bulk input: ${e.message}`);
3458
+ }
3459
+ if (!Array.isArray(data)) {
3460
+ throw new Error("JSON bulk input must be an array of post objects.");
3461
+ }
3462
+ return data;
3463
+ }
3464
+ function parseCsvRows(raw) {
3465
+ const lines = raw.split(/\r?\n/).filter((l) => l.trim().length > 0);
3466
+ if (lines.length === 0) return [];
3467
+ const headers = splitCsvLine(lines[0]);
3468
+ return lines.slice(1).map((line) => {
3469
+ const cells = splitCsvLine(line);
3470
+ const row = {};
3471
+ headers.forEach((h, i) => {
3472
+ if (cells[i] !== void 0 && cells[i] !== "") row[h] = cells[i];
3473
+ });
3474
+ return row;
3475
+ });
3476
+ }
3477
+ function splitCsvLine(line) {
3478
+ const out = [];
3479
+ let cur = "";
3480
+ let inQuotes = false;
3481
+ for (let i = 0; i < line.length; i++) {
3482
+ const c = line[i];
3483
+ if (inQuotes) {
3484
+ if (c === '"' && line[i + 1] === '"') {
3485
+ cur += '"';
3486
+ i++;
3487
+ } else if (c === '"') {
3488
+ inQuotes = false;
3489
+ } else {
3490
+ cur += c;
3491
+ }
3492
+ } else if (c === '"') {
3493
+ inQuotes = true;
3494
+ } else if (c === ",") {
3495
+ out.push(cur);
3496
+ cur = "";
3497
+ } else {
3498
+ cur += c;
3499
+ }
3500
+ }
3501
+ out.push(cur);
3502
+ return out.map((s) => s.trim());
3503
+ }
3504
+ function rowToFields(row, cliBrand) {
3505
+ const platformsRaw = row.platforms;
3506
+ const platforms = Array.isArray(platformsRaw) ? platformsRaw.join(",") : platformsRaw;
3507
+ const brand = resolveBrand(row.brand ?? row.brandSlug ?? cliBrand);
3508
+ if (!brand) {
3509
+ throw new Error('No brand: set "brand" in the row, pass --brand, or set a default context.');
3510
+ }
3511
+ if (!row.text) throw new Error('Missing "text".');
3512
+ if (!platforms) throw new Error('Missing "platforms".');
3513
+ for (const key of ["media", "thread", "account", "altText"]) {
3514
+ if (row[key] !== void 0 && !Array.isArray(row[key])) {
3515
+ throw new Error(`"${key}" is multi-value \u2014 use a JSON input file with an array (CSV can't express it).`);
3516
+ }
3517
+ }
3518
+ return {
3519
+ ...row,
3520
+ brandSlug: brand,
3521
+ text: String(row.text),
3522
+ platforms: String(platforms)
3523
+ };
3524
+ }
3525
+ async function createPostsBulk(client, rows, opts = {}) {
3526
+ const results = [];
3527
+ for (let i = 0; i < rows.length; i++) {
3528
+ const row = rows[i];
3529
+ try {
3530
+ const fields = rowToFields(row, opts.cliBrand);
3531
+ const created = await buildAndCreatePost(client, fields, { isTty: false });
3532
+ results.push({ index: i, status: "created", id: created?.id ?? "", text: fields.text, error: "" });
3533
+ } catch (err) {
3534
+ results.push({
3535
+ index: i,
3536
+ status: "failed",
3537
+ id: "",
3538
+ text: String(row.text ?? ""),
3539
+ error: err.message
3540
+ });
3541
+ }
3542
+ }
3543
+ return results;
3544
+ }
3545
+
3546
+ // src/commands/posts.ts
3547
+ function parsePositiveInt(value, flag) {
3548
+ const n = Number(value);
3549
+ if (!Number.isInteger(n) || n < 1) {
3550
+ throw new Error(`${flag} must be a positive integer (received "${value}").`);
3551
+ }
3552
+ return n;
3553
+ }
3040
3554
  function resolveExitCode5(err) {
3041
3555
  const attached = err.exitCode;
3042
3556
  if (typeof attached === "number") return attached;
@@ -3044,7 +3558,7 @@ function resolveExitCode5(err) {
3044
3558
  }
3045
3559
  function createPostsCommand() {
3046
3560
  const posts = new import_commander7.Command("posts").description("Manage posts");
3047
- posts.command("list").description("List posts").option("--brand <slug>", "Filter by brand slug").option("--status <status>", "Filter by status (draft|scheduled|published|failed)").option("--limit <n>", "Max results per page", "20").option("--page <n>", "Page number", "1").action(async (opts, cmd) => {
3561
+ posts.command("list").description("List posts").option("--brand <slug>", "Filter by brand slug (defaults to the saved context)").option("--status <status>", "Filter by status (draft|scheduled|published|failed)").option("--limit <n>", "Max results per page", "20").option("--page <n>", "Page number", "1").action(async (opts, cmd) => {
3048
3562
  const globals = cmd.optsWithGlobals();
3049
3563
  const printer = createPrinter(globals);
3050
3564
  const spinner = printer.spinner("Fetching posts\u2026");
@@ -3052,7 +3566,7 @@ function createPostsCommand() {
3052
3566
  const cred = resolveAuth({ apiKey: globals.apiKey });
3053
3567
  const client = new Autoposting({ apiKey: cred.apiKey });
3054
3568
  const result = await client.posts.list({
3055
- brandSlug: opts.brand,
3569
+ brandSlug: resolveBrand(opts.brand) ?? void 0,
3056
3570
  status: opts.status,
3057
3571
  limit: parsePositiveInt(opts.limit, "--limit"),
3058
3572
  page: parsePositiveInt(opts.page, "--page")
@@ -3089,10 +3603,10 @@ function createPostsCommand() {
3089
3603
  process.exit(resolveExitCode5(err));
3090
3604
  }
3091
3605
  });
3092
- posts.command("create").description("Create a new post").requiredOption("--brand <slug>", "Brand slug").requiredOption("--text <text>", "Post text content").requiredOption("--platforms <list>", "Comma-separated platforms (e.g. x,linkedin)").option("--at <iso>", "Schedule date/time (ISO 8601)").option(
3606
+ posts.command("create").description("Create a new post").option("--brand <slug>", "Brand slug (defaults to the saved context: ap config set-context)").option("--text <text>", "Post text content (required unless --from)").option("--platforms <list>", "Comma-separated platforms, e.g. x,linkedin (required unless --from)").option("--from <file>", "Create posts in bulk from a CSV or JSON file (one post per row)").option("--at <iso>", "Schedule date/time (ISO 8601)").option(
3093
3607
  "--thread <text...>",
3094
3608
  "Additional posts appended after --text to form a thread (X/Threads only, max 25)"
3095
- ).option("--media <path...>", "Media file paths to attach (max 10)").option("--alt-text <text...>", "Alt text for --media files, aligned by index").option("--platform-text <pairs...>", "Per-platform text, e.g. x=Hello linkedin=World").option("--platform-media <pairs...>", "Per-platform media, e.g. instagram=photo.jpg x=banner.png").option("--yt-title <t>", "YouTube video title").option("--yt-description <d>", "YouTube video description").option("--yt-tags <list>", "YouTube tags (comma-separated)").option("--yt-privacy <v>", "YouTube privacy status (public|unlisted|private)").option("--yt-category <id>", "YouTube category ID").option("--yt-made-for-kids", "Mark YouTube video as made for kids").option("--ig-reel", "Post as an Instagram Reel").option("--ig-share-to-feed", "Share Reel to feed").option("--ig-cover-url <url>", "Reel cover image URL").option("--ig-thumb-offset-ms <n>", "Reel thumbnail offset in milliseconds").option("--ig-collaborators <list>", "Reel collaborator handles (comma-separated)").option("--threads-reply-to <id>", "Threads post ID to reply to").option("--threads-reply-control <v>", "Threads reply control (everyone|accounts_you_follow|mentioned_only)").option("--account <p=handle|id...>", "Target specific account per platform, e.g. x=@handle").action(
3609
+ ).option("--media <path...>", "Media file paths to attach (max 10)").option("--alt-text <text...>", "Alt text for --media files, aligned by index").option("--platform-text <pairs...>", "Per-platform text, e.g. x=Hello linkedin=World").option("--platform-media <pairs...>", "Per-platform media, e.g. instagram=photo.jpg x=banner.png").option("--yt-title <t>", "YouTube video title").option("--yt-description <d>", "YouTube video description").option("--yt-tags <list>", "YouTube tags (comma-separated)").option("--yt-privacy <v>", "YouTube privacy status (public|unlisted|private)").option("--yt-category <id>", "YouTube category ID").option("--yt-made-for-kids", "Mark YouTube video as made for kids").option("--ig-reel", "Post as an Instagram Reel").option("--ig-share-to-feed", "Share Reel to feed").option("--ig-cover-url <url>", "Reel cover image URL").option("--ig-thumb-offset-ms <n>", "Reel thumbnail offset in milliseconds").option("--ig-collaborators <list>", "Reel collaborator handles (comma-separated)").option("--threads-reply-to <id>", "Threads post ID to reply to").option("--threads-reply-control <v>", "Threads reply control (everyone|accounts_you_follow|mentioned_only)").option("--account <p=handle|id...>", "Target specific account per platform, e.g. x=@handle").option("--dry-run", "Print the resolved request body without uploading media or creating the post").option("--preview", "Alias for --dry-run").action(
3096
3610
  async (opts, cmd) => {
3097
3611
  const globals = cmd.optsWithGlobals();
3098
3612
  const printer = createPrinter(globals);
@@ -3100,99 +3614,57 @@ function createPostsCommand() {
3100
3614
  try {
3101
3615
  const cred = resolveAuth({ apiKey: globals.apiKey });
3102
3616
  const client = new Autoposting({ apiKey: cred.apiKey });
3103
- const platforms = parsePlatforms(opts.platforms);
3104
- const scheduledAt = opts.at ? validateScheduledAt(opts.at) : void 0;
3105
- if (opts.media && opts.media.length > 0) {
3106
- validateMediaCount(opts.media);
3107
- validateMediaPaths(opts.media);
3108
- validateMediaExtensions(opts.media);
3617
+ if (opts.from) {
3618
+ const rows = parseBulkFile(opts.from);
3619
+ const records = await createPostsBulk(client, rows, {
3620
+ cliBrand: resolveBrand(opts.brand) ?? void 0
3621
+ });
3622
+ const truncate = printer.isTty();
3623
+ printer.table(
3624
+ records.map((r) => ({
3625
+ "#": r.index + 1,
3626
+ status: r.status,
3627
+ id: r.id || "\u2014",
3628
+ text: truncate && r.text.length > 40 ? `${r.text.slice(0, 37)}\u2026` : r.text,
3629
+ error: r.error
3630
+ })),
3631
+ ["#", "status", "id", "text", "error"]
3632
+ );
3633
+ if (records.some((r) => r.status === "failed")) {
3634
+ process.exit(1);
3635
+ }
3636
+ return;
3109
3637
  }
3110
- const platformTexts = opts.platformText && opts.platformText.length > 0 ? parsePairs("--platform-text", opts.platformText) : void 0;
3111
- const platformMediaPaths = opts.platformMedia && opts.platformMedia.length > 0 ? parsePlatformMediaPairs("--platform-media", opts.platformMedia) : {};
3112
- for (const paths of Object.values(platformMediaPaths)) {
3113
- validateMediaPaths(paths);
3114
- validateMediaExtensions(paths);
3638
+ const brand = resolveBrand(opts.brand);
3639
+ if (!brand) {
3640
+ throw new Error(
3641
+ "No brand specified. Pass --brand <slug> or set a default with: ap config set-context --brand <slug>"
3642
+ );
3115
3643
  }
3116
- const altTexts = alignAltText(opts.media ?? [], opts.altText ?? []);
3117
- const youtubeOptions = buildYoutubeOptions({
3118
- ytTitle: opts.ytTitle,
3119
- ytDescription: opts.ytDescription,
3120
- ytTags: opts.ytTags,
3121
- ytPrivacy: opts.ytPrivacy,
3122
- ytCategory: opts.ytCategory,
3123
- ytMadeForKids: opts.ytMadeForKids
3124
- });
3125
- const instagramOptions = buildInstagramOptions({
3126
- igReel: opts.igReel,
3127
- igShareToFeed: opts.igShareToFeed,
3128
- igCoverUrl: opts.igCoverUrl,
3129
- igThumbOffsetMs: opts.igThumbOffsetMs,
3130
- igCollaborators: opts.igCollaborators
3131
- });
3132
- const threadsOptions = buildThreadsOptions({
3133
- threadsReplyTo: opts.threadsReplyTo,
3134
- threadsReplyControl: opts.threadsReplyControl
3135
- });
3136
- const targetAccountIds = await resolveTargetAccounts({
3137
- brandSlug: opts.brand,
3138
- platforms,
3139
- accountFlags: opts.account ?? [],
3140
- client,
3141
- // Picker reads stdin — gate on a real interactive terminal, not the
3142
- // output mode (printer.isTty() is true even when stdout/stdin is piped).
3143
- isTty: Boolean(process.stdin.isTTY)
3144
- });
3145
- spinner = printer.spinner("Creating post\u2026");
3146
- const mediaInputs = [];
3147
- for (let i = 0; i < (opts.media ?? []).length; i++) {
3148
- const filePath = opts.media[i];
3149
- const data = await import_promises.default.readFile(filePath);
3150
- const filename = import_node_path2.default.basename(filePath);
3151
- const contentType = extToMime(filename);
3152
- const uploaded = await client.media.upload({
3153
- data: new Uint8Array(data),
3154
- filename,
3155
- contentType
3156
- });
3157
- mediaInputs.push({
3158
- url: uploaded.url,
3159
- type: uploaded.type,
3160
- ...altTexts[i] ? { altText: altTexts[i] } : {}
3161
- });
3644
+ if (!opts.text) {
3645
+ throw new Error("--text is required (or use --from <file> to create posts in bulk).");
3162
3646
  }
3163
- const platformMediaResult = {};
3164
- for (const [p, paths] of Object.entries(platformMediaPaths)) {
3165
- const uploads = [];
3166
- for (const filePath of paths) {
3167
- const data = await import_promises.default.readFile(filePath);
3168
- const filename = import_node_path2.default.basename(filePath);
3169
- const contentType = extToMime(filename);
3170
- const uploaded = await client.media.upload({
3171
- data: new Uint8Array(data),
3172
- filename,
3173
- contentType
3174
- });
3175
- uploads.push({ url: uploaded.url, type: uploaded.type });
3176
- }
3177
- platformMediaResult[p] = uploads;
3647
+ if (!opts.platforms) {
3648
+ throw new Error("--platforms is required (or use --from <file> to create posts in bulk).");
3178
3649
  }
3179
- const post = await client.posts.create({
3180
- brandSlug: opts.brand,
3181
- text: opts.text,
3182
- platforms,
3183
- ...scheduledAt ? { scheduledAt } : {},
3184
- ...opts.thread && opts.thread.length > 0 ? { thread: opts.thread } : {},
3185
- ...mediaInputs.length > 0 ? { media: mediaInputs } : {},
3186
- ...Object.keys(platformMediaResult).length > 0 ? { platformMedia: platformMediaResult } : {},
3187
- ...platformTexts && Object.keys(platformTexts).length > 0 ? { platformTexts } : {},
3188
- ...Object.keys(targetAccountIds).length > 0 ? { targetAccountIds } : {},
3189
- ...instagramOptions ? { instagramOptions } : {},
3190
- ...threadsOptions ? { threadsOptions } : {},
3191
- ...youtubeOptions ? { youtubeOptions } : {},
3192
- source: "cli"
3193
- });
3194
- spinner.stop();
3195
- printer.log(post);
3650
+ const result = await buildAndCreatePost(
3651
+ client,
3652
+ { ...opts, brandSlug: brand, text: opts.text, platforms: opts.platforms },
3653
+ {
3654
+ // Picker reads stdin — gate on a real interactive terminal, not the
3655
+ // output mode (printer.isTty() is true even when stdin is piped).
3656
+ isTty: Boolean(process.stdin.isTTY),
3657
+ // --dry-run/--preview: resolve + validate, then return the request body
3658
+ // without uploading media or POSTing (onBeforeNetwork never fires).
3659
+ dryRun: Boolean(opts.dryRun || opts.preview),
3660
+ // Picker done — start the spinner now (covers uploads + create).
3661
+ onBeforeNetwork: () => {
3662
+ spinner = printer.spinner("Creating post\u2026");
3663
+ }
3664
+ }
3665
+ );
3666
+ spinner?.stop();
3667
+ printer.log(result);
3196
3668
  } catch (err) {
3197
3669
  spinner?.fail();
3198
3670
  printer.error(err);
@@ -3239,7 +3711,7 @@ function createPostsCommand() {
3239
3711
  } catch (err) {
3240
3712
  spinner.fail();
3241
3713
  printer.error(err);
3242
- if (!(err instanceof NotFoundError) && printer.isTty()) {
3714
+ if (!(err instanceof NotFoundError)) {
3243
3715
  printer.error(`The post may not have been deleted \u2014 verify with: ap posts get "${id}"`);
3244
3716
  }
3245
3717
  process.exit(resolveExitCode5(err));
@@ -3842,7 +4314,7 @@ function createWorkspacesCommand() {
3842
4314
  // src/commands/doctor.ts
3843
4315
  var import_commander13 = require("commander");
3844
4316
  init_dist();
3845
- var import_node_fs3 = __toESM(require("fs"));
4317
+ var import_node_fs5 = __toESM(require("fs"));
3846
4318
  var API_HEALTH_URL = "https://app.autoposting.ai/api-proxy/health";
3847
4319
  async function runChecks() {
3848
4320
  const results = [];
@@ -3862,7 +4334,7 @@ async function runChecks() {
3862
4334
  }
3863
4335
  const credPath = getCredentialsPath();
3864
4336
  try {
3865
- const stat = import_node_fs3.default.statSync(credPath);
4337
+ const stat = import_node_fs5.default.statSync(credPath);
3866
4338
  const mode = stat.mode & 511;
3867
4339
  const isSecure = mode === 384;
3868
4340
  results.push({
@@ -4142,8 +4614,34 @@ function createMcpCommand() {
4142
4614
  });
4143
4615
  }
4144
4616
 
4617
+ // src/commands/config.ts
4618
+ var import_commander19 = require("commander");
4619
+ function createConfigCommand() {
4620
+ const config = new import_commander19.Command("config").description("Manage CLI defaults (stored in config.json)");
4621
+ config.command("set-context").description("Set the default brand used when --brand is omitted").requiredOption("--brand <slug>", "Brand slug to use as the default context").action((opts, cmd) => {
4622
+ const printer = createPrinter(cmd.optsWithGlobals());
4623
+ setContextBrand(opts.brand);
4624
+ printer.log({ brand: opts.brand });
4625
+ });
4626
+ config.command("get-context").description("Show the default brand context").action((_opts, cmd) => {
4627
+ const printer = createPrinter(cmd.optsWithGlobals());
4628
+ const brand = getContextBrand();
4629
+ if (!brand) {
4630
+ printer.error("No brand context set. Set one with: ap config set-context --brand <slug>");
4631
+ process.exit(1);
4632
+ }
4633
+ printer.log({ brand });
4634
+ });
4635
+ config.command("unset-context").description("Clear the default brand context").action((_opts, cmd) => {
4636
+ const printer = createPrinter(cmd.optsWithGlobals());
4637
+ unsetContextBrand();
4638
+ printer.log({ brand: null });
4639
+ });
4640
+ return config;
4641
+ }
4642
+
4145
4643
  // src/cli.ts
4146
- var program = new import_commander19.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: table, json", "table").option("--api-key <key>", "API key (overrides env/stored credentials)").option("--no-color", "Disable color output").hook("preAction", (thisCommand) => {
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) => {
4147
4645
  const opts = thisCommand.opts();
4148
4646
  if (!opts.color) disableColor();
4149
4647
  });
@@ -4165,5 +4663,6 @@ program.addCommand(createOpenCommand());
4165
4663
  program.addCommand(createUpdateCommand());
4166
4664
  program.addCommand(createCompletionCommand());
4167
4665
  program.addCommand(createMcpCommand());
4666
+ program.addCommand(createConfigCommand());
4168
4667
  program.parse();
4169
4668
  //# sourceMappingURL=cli.cjs.map