@autoposting.ai/cli 0.3.2 → 0.3.3

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/README.md CHANGED
@@ -69,7 +69,10 @@ ap doctor
69
69
  # Posts — create, schedule, publish, AI-rewrite, AI-score
70
70
  ap posts create --brand my-brand --text "Launch day!" --platforms x,linkedin,threads
71
71
  ap posts create --brand my-brand --text "Tweet 1" --thread "Tweet 2" "Tweet 3" --platforms x # multi-tweet thread (X/Threads, max 25)
72
+ ap posts create --brand my-brand --text "Ship day 🚀" --platforms x --media chart.png demo.mp4 --alt-text "Q2 chart" # attach media (alt text aligns by index)
73
+ ap posts create --brand my-brand --text "Update" --platforms linkedin --account linkedin=@acme-corp # pick one of several accounts on a platform
72
74
  ap posts schedule <id> --at "2025-06-01T09:00:00Z"
75
+ ap posts schedule <id> --cancel # unschedule, back to draft
73
76
  ap posts publish <id>
74
77
  ap posts rewrite <id> # AI-rewrite for better engagement
75
78
  ap posts score <id> # AI scoring with feedback
package/dist/cli.cjs CHANGED
@@ -42,11 +42,11 @@ 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, AgentsResource, BillingResource, BrandsResource, CarouselsResource, ClipsResource, PostsResource, KbResource, IdeasResource, UsageResource, WebhooksResource, WorkspacesResource, IDEMPOTENT_METHODS, RETRYABLE_STATUS, sleep, Autoposting;
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;
46
46
  var init_dist = __esm({
47
47
  "../sdk/dist/index.mjs"() {
48
48
  "use strict";
49
- VERSION = "0.3.2";
49
+ VERSION = "0.3.3";
50
50
  AutopostingError = class extends Error {
51
51
  status;
52
52
  code;
@@ -92,6 +92,15 @@ var init_dist = __esm({
92
92
  return this.client.request("DELETE", path2);
93
93
  }
94
94
  };
95
+ MediaResource = class extends Resource {
96
+ /** POST /media/upload — multipart/form-data, ≤20MB, image/* | video/*. */
97
+ upload(file) {
98
+ const form = new FormData();
99
+ const blob = file.data instanceof Blob ? file.data : new Blob([file.data], { type: file.contentType });
100
+ form.append("file", blob, file.filename);
101
+ return this.client.request("POST", "/media/upload", form);
102
+ }
103
+ };
95
104
  AgentsResource = class extends Resource {
96
105
  /** GET /agents — backend returns `{ items, total, limit, offset }`. */
97
106
  list() {
@@ -233,6 +242,10 @@ var init_dist = __esm({
233
242
  schedule(id, scheduledAt) {
234
243
  return this.client.request("PUT", `/posts/${id}/schedule`, { scheduledAt });
235
244
  }
245
+ /** PUT /posts/:id/schedule with { cancel: true } — returns the post to draft. */
246
+ unschedule(id) {
247
+ return this.client.request("PUT", `/posts/${id}/schedule`, { cancel: true });
248
+ }
236
249
  retry(id) {
237
250
  return this.client.request("POST", `/posts/${id}/retry`);
238
251
  }
@@ -361,6 +374,7 @@ var init_dist = __esm({
361
374
  authSource;
362
375
  maxRetries;
363
376
  retryBaseMs;
377
+ media;
364
378
  agents;
365
379
  billing;
366
380
  brands;
@@ -386,6 +400,7 @@ var init_dist = __esm({
386
400
  this.authSource = config.authSource ?? "api-key";
387
401
  this.maxRetries = Math.max(0, config.maxRetries ?? 2);
388
402
  this.retryBaseMs = Math.max(0, config.retryBaseMs ?? 300);
403
+ this.media = new MediaResource(this);
389
404
  this.agents = new AgentsResource(this);
390
405
  this.billing = new BillingResource(this);
391
406
  this.brands = new BrandsResource(this);
@@ -2777,16 +2792,226 @@ function createClipsCommand() {
2777
2792
  // src/commands/posts.ts
2778
2793
  var import_commander7 = require("commander");
2779
2794
  init_dist();
2795
+ var import_promises = __toESM(require("fs/promises"));
2796
+ var import_node_path2 = __toESM(require("path"));
2797
+
2798
+ // src/lib/media-flags.ts
2799
+ var import_node_fs2 = require("fs");
2780
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
+ }
2916
+
2917
+ // src/lib/account-select.ts
2918
+ var VALID_PLATFORMS2 = ["x", "linkedin", "instagram", "threads", "youtube"];
2919
+ async function resolveTargetAccounts({
2920
+ brandSlug,
2921
+ platforms,
2922
+ accountFlags,
2923
+ client,
2924
+ isTty
2925
+ }) {
2926
+ const accountMap = {};
2927
+ for (const flag of accountFlags) {
2928
+ const eqIdx = flag.indexOf("=");
2929
+ if (eqIdx < 1) {
2930
+ throw Object.assign(
2931
+ new Error(`--account: expected "platform=handle|id" format, got "${flag}"`),
2932
+ { exitCode: 1 }
2933
+ );
2934
+ }
2935
+ const p = flag.slice(0, eqIdx).trim();
2936
+ if (!VALID_PLATFORMS2.includes(p)) {
2937
+ throw Object.assign(
2938
+ new Error(`--account: unknown platform "${p}". Valid: ${VALID_PLATFORMS2.join(", ")}`),
2939
+ { exitCode: 1 }
2940
+ );
2941
+ }
2942
+ accountMap[p] = flag.slice(eqIdx + 1).trim();
2943
+ }
2944
+ const connections = await client.brands.authStatus(brandSlug);
2945
+ const byPlatform = {};
2946
+ for (const conn of connections) {
2947
+ if (!conn.connected) continue;
2948
+ const p = conn.platform;
2949
+ if (!byPlatform[p]) byPlatform[p] = [];
2950
+ byPlatform[p].push(conn);
2951
+ }
2952
+ const result = {};
2953
+ for (const platform of platforms) {
2954
+ const accounts = byPlatform[platform] ?? [];
2955
+ const specifiedValue = accountMap[platform];
2956
+ if (specifiedValue !== void 0) {
2957
+ const normalized = specifiedValue.startsWith("@") ? specifiedValue.slice(1) : specifiedValue;
2958
+ const match = accounts.find(
2959
+ (a) => a.platformUsername?.toLowerCase() === normalized.toLowerCase() || a.platformUserId === specifiedValue
2960
+ );
2961
+ if (!match?.platformUserId) {
2962
+ const valid = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
2963
+ throw Object.assign(
2964
+ new Error(
2965
+ `--account: unknown ${platform} account "${specifiedValue}".
2966
+ Connected accounts:
2967
+ ${valid || " (none)"}`
2968
+ ),
2969
+ { exitCode: 1 }
2970
+ );
2971
+ }
2972
+ if (!result[platform]) result[platform] = [];
2973
+ result[platform].push(match.platformUserId);
2974
+ } else if (accounts.length >= 2) {
2975
+ if (isTty) {
2976
+ const { checkbox } = await import("@inquirer/prompts");
2977
+ const choices = accounts.map((a) => ({
2978
+ name: `@${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`,
2979
+ value: a.platformUserId ?? ""
2980
+ }));
2981
+ const selected = await checkbox({
2982
+ message: `Multiple ${platform} accounts connected \u2014 select which to post to:`,
2983
+ choices
2984
+ });
2985
+ const valid = selected.filter(Boolean);
2986
+ if (valid.length > 0) result[platform] = valid;
2987
+ } else {
2988
+ const list = accounts.map((a) => ` @${a.platformUsername ?? "?"} (${a.platformUserId ?? "?"})`).join("\n");
2989
+ throw Object.assign(
2990
+ new Error(
2991
+ `Multiple ${platform} accounts connected but no --account specified (non-interactive mode).
2992
+ Connected accounts:
2993
+ ${list}
2994
+ Pass --account ${platform}=<handle|id> to select.`
2995
+ ),
2996
+ { exitCode: 1 }
2997
+ );
2998
+ }
2999
+ }
3000
+ }
3001
+ return result;
3002
+ }
3003
+
3004
+ // src/commands/posts.ts
3005
+ var VALID_PLATFORMS3 = ["x", "linkedin", "instagram", "threads", "youtube"];
2781
3006
  function parsePlatforms(raw) {
2782
3007
  const parts = raw.split(",").map((p) => p.trim()).filter(Boolean);
2783
3008
  if (parts.length === 0) {
2784
3009
  throw new Error("No platforms provided. Pass a comma-separated list, e.g. --platforms x,linkedin");
2785
3010
  }
2786
- const invalid = parts.filter((p) => !VALID_PLATFORMS.includes(p));
3011
+ const invalid = parts.filter((p) => !VALID_PLATFORMS3.includes(p));
2787
3012
  if (invalid.length > 0) {
2788
3013
  throw new Error(
2789
- `Unsupported platform(s): ${invalid.join(", ")}. Valid platforms: ${VALID_PLATFORMS.join(", ")}`
3014
+ `Unsupported platform(s): ${invalid.join(", ")}. Valid platforms: ${VALID_PLATFORMS3.join(", ")}`
2790
3015
  );
2791
3016
  }
2792
3017
  return parts;
@@ -2867,25 +3092,109 @@ function createPostsCommand() {
2867
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(
2868
3093
  "--thread <text...>",
2869
3094
  "Additional posts appended after --text to form a thread (X/Threads only, max 25)"
2870
- ).action(
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(
2871
3096
  async (opts, cmd) => {
2872
3097
  const globals = cmd.optsWithGlobals();
2873
3098
  const printer = createPrinter(globals);
2874
- const spinner = printer.spinner("Creating post\u2026");
3099
+ let spinner;
2875
3100
  try {
2876
3101
  const cred = resolveAuth({ apiKey: globals.apiKey });
2877
3102
  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);
3109
+ }
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);
3115
+ }
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
+ });
3162
+ }
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;
3178
+ }
2878
3179
  const post = await client.posts.create({
2879
3180
  brandSlug: opts.brand,
2880
3181
  text: opts.text,
2881
- platforms: parsePlatforms(opts.platforms),
2882
- ...opts.at ? { scheduledAt: validateScheduledAt(opts.at) } : {},
2883
- ...opts.thread && opts.thread.length > 0 ? { thread: opts.thread } : {}
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"
2884
3193
  });
2885
3194
  spinner.stop();
2886
3195
  printer.log(post);
2887
3196
  } catch (err) {
2888
- spinner.fail();
3197
+ spinner?.fail();
2889
3198
  printer.error(err);
2890
3199
  process.exit(resolveExitCode5(err));
2891
3200
  }
@@ -2952,14 +3261,22 @@ function createPostsCommand() {
2952
3261
  process.exit(resolveExitCode5(err));
2953
3262
  }
2954
3263
  });
2955
- posts.command("schedule <id>").description("Schedule a post for a specific time").requiredOption("--at <iso>", "ISO 8601 datetime to schedule the post").action(async (id, opts, cmd) => {
3264
+ posts.command("schedule <id>").description("Schedule a post for a specific time, or --cancel to unschedule it").option("--at <iso>", "ISO 8601 datetime to schedule the post").option("--cancel", "Unschedule the post (return it to draft)").action(async (id, opts, cmd) => {
2956
3265
  const globals = cmd.optsWithGlobals();
2957
3266
  const printer = createPrinter(globals);
2958
- const spinner = printer.spinner(`Scheduling post "${id}"\u2026`);
3267
+ const spinner = printer.spinner(
3268
+ opts.cancel ? `Unscheduling post "${id}"\u2026` : `Scheduling post "${id}"\u2026`
3269
+ );
2959
3270
  try {
3271
+ if (opts.cancel && opts.at) {
3272
+ throw new Error("Cannot use --at together with --cancel \u2014 choose one.");
3273
+ }
3274
+ if (!opts.cancel && !opts.at) {
3275
+ throw new Error("Provide either --at <iso> to schedule, or --cancel to unschedule.");
3276
+ }
2960
3277
  const cred = resolveAuth({ apiKey: globals.apiKey });
2961
3278
  const client = new Autoposting({ apiKey: cred.apiKey });
2962
- const post = await client.posts.schedule(id, validateScheduledAt(opts.at));
3279
+ const post = opts.cancel ? await client.posts.unschedule(id) : await client.posts.schedule(id, validateScheduledAt(opts.at));
2963
3280
  spinner.stop();
2964
3281
  printer.log(post);
2965
3282
  } catch (err) {
@@ -3525,7 +3842,7 @@ function createWorkspacesCommand() {
3525
3842
  // src/commands/doctor.ts
3526
3843
  var import_commander13 = require("commander");
3527
3844
  init_dist();
3528
- var import_node_fs2 = __toESM(require("fs"));
3845
+ var import_node_fs3 = __toESM(require("fs"));
3529
3846
  var API_HEALTH_URL = "https://app.autoposting.ai/api-proxy/health";
3530
3847
  async function runChecks() {
3531
3848
  const results = [];
@@ -3545,7 +3862,7 @@ async function runChecks() {
3545
3862
  }
3546
3863
  const credPath = getCredentialsPath();
3547
3864
  try {
3548
- const stat = import_node_fs2.default.statSync(credPath);
3865
+ const stat = import_node_fs3.default.statSync(credPath);
3549
3866
  const mode = stat.mode & 511;
3550
3867
  const isSecure = mode === 384;
3551
3868
  results.push({