chirpie 1.0.45 → 1.0.46

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.
Files changed (3) hide show
  1. package/README.md +23 -3
  2. package/dist/index.js +71 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,11 +30,12 @@ export CHIRPIE_API_KEY=chirpie_sk_...
30
30
  | Command | What it does |
31
31
  |---------|--------------|
32
32
  | `chirpie login` / `logout` / `auth` / `whoami` | Manage credentials |
33
- | `chirpie post <text>` | Create a post. `-a` account (repeat it for several accounts), `-m` media files or URLs, `--alt` descriptions, `--config` per-account overrides, `-s` schedule |
34
- | `chirpie thread <post> <post> ...` | Create a 2 to 25 post thread. `-a` is repeatable, `--config` takes per-account posts, `-f` takes a JSON payload file |
33
+ | `chirpie post <text>` | Create a post. `-a` account (repeat it for several accounts), `-m` media files or URLs, `--alt` descriptions, `--config` per-account overrides, `-s` schedule, `--draft` save without sending |
34
+ | `chirpie thread <post> <post> ...` | Create a 2 to 25 post thread, or 1 to 25 with `--draft`. `-a` is repeatable, `--config` takes per-account posts, `-f` takes a JSON payload file |
35
35
  | `chirpie posts` | List posts. `--status`, `--account`, `--group`, `--limit` |
36
36
  | `chirpie posts get <id>` | Fetch one post |
37
- | `chirpie posts update <id>` | Edit a post that has not published yet. `--text`, `--media`, `--alt`, `--clear-media`, `--schedule-at`. Leaving `--schedule-at` out keeps the time it already has |
37
+ | `chirpie posts update <id>` | Edit a post that has not published yet, or finish a draft. `--text`, `--media`, `--alt`, `--clear-media`, `--schedule-at`, `--keep-draft`. Leaving `--schedule-at` out keeps the time it already has |
38
+ | `chirpie posts publish <id>` | Send a saved draft now. `-t` sets the final text first |
38
39
  | `chirpie posts delete <id>` | Delete a post. Deleting any post of a scheduled thread cancels the thread |
39
40
  | `chirpie accounts` | List connected accounts, active and inactive |
40
41
  | `chirpie accounts activate <id>` / `deactivate <id>` | Choose which accounts publish. Deactivating cancels that account's scheduled posts |
@@ -70,6 +71,25 @@ chirpie thread "One" "Two" "Three" -a 550e8400-e29b-41d4-a716-446655440000
70
71
  chirpie posts --status scheduled
71
72
  ```
72
73
 
74
+ ## Drafts
75
+
76
+ `--draft` saves a post or a thread without sending it. Nothing reaches the platform and nothing counts against your quota until you promote it. Anything that would go wrong is printed back, one warning per line.
77
+
78
+ ```bash
79
+ chirpie post "Half an idea" --draft
80
+ chirpie thread "Opening line" --draft # a draft thread may be one post
81
+ chirpie posts --status draft # what is saved
82
+
83
+ # Change only the time it remembers
84
+ chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z --keep-draft
85
+
86
+ # Promote it: queue it, or send it now
87
+ chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z
88
+ chirpie posts publish "$POST_ID"
89
+ ```
90
+
91
+ Promoting runs every rule a normal post runs and takes the quota, so a draft that would be refused stays a draft, unchanged. A draft thread is promoted whole.
92
+
73
93
  ## Several accounts at once
74
94
 
75
95
  Repeat `-a` to publish the same post to up to 25 accounts in one call.
package/dist/index.js CHANGED
@@ -227,6 +227,14 @@ function parseAccountConfigurations(value) {
227
227
  }
228
228
  return parsed;
229
229
  }
230
+ function printDraftWarnings(warnings) {
231
+ if (!Array.isArray(warnings)) return;
232
+ for (const warning of warnings) {
233
+ const entry = warning;
234
+ if (typeof entry?.message !== "string") continue;
235
+ info(`${entry.platform} ${entry.account_id}: ${entry.message}`);
236
+ }
237
+ }
230
238
  function printFanOut(groupId, lines) {
231
239
  const failed = lines.filter((line) => !line.success);
232
240
  console.log(`Group ${groupId}`);
@@ -257,7 +265,10 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
257
265
  ).option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--alt <text...>", ALT_OPTION_HELP).option(
258
266
  "--config <json-or-file>",
259
267
  'Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "text": "...", "media_urls": ["..."] } }. Every key must be an account named with --account'
260
- ).option("-s, --schedule <datetime>", "Schedule for later. Any ISO 8601 timestamp with a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00). Posts publish within ~5 minutes of the scheduled time.").option("--json", "Output raw JSON").action(async (text, opts) => {
268
+ ).option("-s, --schedule <datetime>", "Schedule for later. Any ISO 8601 timestamp with a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00). Posts publish within ~5 minutes of the scheduled time.").option(
269
+ "--draft",
270
+ "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
271
+ ).option("--json", "Output raw JSON").action(async (text, opts) => {
261
272
  const client = getClient();
262
273
  const named = opts.account ?? [];
263
274
  if (opts.config && named.length === 0) {
@@ -278,13 +289,15 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
278
289
  opts.config
279
290
  )
280
291
  } : {},
281
- schedule_at: opts.schedule
292
+ schedule_at: opts.schedule,
293
+ ...opts.draft ? { draft: true } : {}
282
294
  });
283
295
  if (opts.json) {
284
296
  json(result);
285
297
  if (result.results.some((r) => !r.success)) process.exit(1);
286
298
  return;
287
299
  }
300
+ if ("warnings" in result) printDraftWarnings(result.warnings);
288
301
  const failed = printFanOut(
289
302
  result.group_id,
290
303
  result.results.map((r) => ({
@@ -304,10 +317,14 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
304
317
  account_id: accountIds[0],
305
318
  text,
306
319
  ...media ? { media } : {},
307
- schedule_at: opts.schedule
320
+ schedule_at: opts.schedule,
321
+ ...opts.draft ? { draft: true } : {}
308
322
  });
309
323
  if (opts.json) {
310
324
  json(post);
325
+ } else if (post.status === "draft") {
326
+ success(`Draft saved (${post.id})`);
327
+ if ("warnings" in post) printDraftWarnings(post.warnings);
311
328
  } else if (post.status === "scheduled") {
312
329
  success(`Post scheduled for ${post.scheduled_at} (${post.id})`);
313
330
  } else {
@@ -353,7 +370,10 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
353
370
  ).option(
354
371
  "--config <json-or-file>",
355
372
  `Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "posts": [{ "text": "one" }, { "text": "two" }] } }. An account's posts replace the whole thread for that account`
356
- ).option("-s, --schedule <datetime>", "Schedule for later. Any ISO 8601 timestamp with a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00). Threads publish within ~5 minutes of the scheduled time.").option("--json", "Output raw JSON").action(async (posts, opts) => {
373
+ ).option("-s, --schedule <datetime>", "Schedule for later. Any ISO 8601 timestamp with a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00). Threads publish within ~5 minutes of the scheduled time.").option(
374
+ "--draft",
375
+ "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
376
+ ).option("--json", "Output raw JSON").action(async (posts, opts) => {
357
377
  const client = getClient();
358
378
  let postPayload;
359
379
  let scheduleAt = opts.schedule;
@@ -375,8 +395,10 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
375
395
  process.exit(1);
376
396
  }
377
397
  } else {
378
- if (posts.length < 2) {
379
- error("Thread must have at least 2 posts (or use --file)");
398
+ if (posts.length < (opts.draft ? 1 : 2)) {
399
+ error(
400
+ opts.draft ? "Pass at least one post (or use --file)" : "Thread must have at least 2 posts (or use --file)"
401
+ );
380
402
  process.exit(1);
381
403
  }
382
404
  postPayload = posts.map((text) => ({ text }));
@@ -426,13 +448,15 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
426
448
  account_ids: accountIds,
427
449
  posts: postPayload,
428
450
  ...configurations ? { account_configurations: configurations } : {},
429
- schedule_at: scheduleAt
451
+ schedule_at: scheduleAt,
452
+ ...opts.draft ? { draft: true } : {}
430
453
  });
431
454
  if (opts.json) {
432
455
  json(result);
433
456
  if (result.results.some((r) => !r.success)) process.exit(1);
434
457
  return;
435
458
  }
459
+ if ("warnings" in result) printDraftWarnings(result.warnings);
436
460
  const failed = printFanOut(
437
461
  result.group_id,
438
462
  result.results.map((r) => ({
@@ -451,10 +475,14 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
451
475
  const thread = await client.createThread({
452
476
  account_id: accountIds[0],
453
477
  posts: postPayload,
454
- schedule_at: scheduleAt
478
+ schedule_at: scheduleAt,
479
+ ...opts.draft ? { draft: true } : {}
455
480
  });
456
481
  if (opts.json) {
457
482
  json(thread);
483
+ } else if (thread.status === "draft") {
484
+ success(`Draft thread saved with ${thread.posts.length} posts (${thread.id})`);
485
+ if ("warnings" in thread) printDraftWarnings(thread.warnings);
458
486
  } else {
459
487
  success(`Thread created with ${thread.posts.length} posts (${thread.id})`);
460
488
  }
@@ -475,7 +503,10 @@ function wantsJson(opts, cmd) {
475
503
  }
476
504
 
477
505
  // src/commands/posts.ts
478
- var postsCommand = new Command5("posts").description("List, get, update, or delete posts").option("--status <status>", "Filter by status").option("--account <id>", "Filter by account").option(
506
+ var postsCommand = new Command5("posts").description("List, get, update, or delete posts").option(
507
+ "--status <status>",
508
+ "Filter by status: draft, scheduled, publishing, published, failed, deleted"
509
+ ).option("--account <id>", "Filter by account").option(
479
510
  "--group <id>",
480
511
  "Only the posts of one fan-out, from the group ID a multi-account post returned"
481
512
  ).option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts, cmd) => {
@@ -532,20 +563,25 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
532
563
  }
533
564
  });
534
565
  postsCommand.command("update <id>").description(
535
- "Edit a post that has not published yet. Leaving --schedule-at out keeps the time it already has"
566
+ "Edit a post that has not published yet, or finish a draft. Leaving --schedule-at out keeps the time a queued post already has"
536
567
  ).option("-t, --text <text>", "Replacement post text").option(
537
568
  "-m, --media <files...>",
538
569
  "Replacement image or video files on this machine, or public URLs, following the same per-platform rules as creating a post. Pass --clear-media to remove the media instead."
539
570
  ).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
540
571
  "-s, --schedule-at <datetime>",
541
- "New publish time. Any ISO 8601 timestamp with a timezone (2027-04-01T14:00:00Z or 2027-04-01T16:00:00+02:00). Omit it to keep the time the post already has. Rescheduling one part of a thread moves the whole thread."
572
+ "New publish time. Any ISO 8601 timestamp with a timezone (2027-04-01T14:00:00Z or 2027-04-01T16:00:00+02:00). Omit it to keep the time the post already has. Rescheduling one part of a thread moves the whole thread. On a draft this queues it, unless you also pass --keep-draft."
573
+ ).option(
574
+ "--keep-draft",
575
+ "Keep a draft a draft, so --schedule-at only changes the time it remembers"
542
576
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
543
577
  if (opts.media && opts.clearMedia) {
544
578
  error("Pass either --media or --clear-media, not both");
545
579
  process.exit(1);
546
580
  }
547
- if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt) {
548
- error("Nothing to change. Pass --text, --media, --clear-media or --schedule-at");
581
+ if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt && !opts.keepDraft) {
582
+ error(
583
+ "Nothing to change. Pass --text, --media, --clear-media, --schedule-at or --keep-draft"
584
+ );
549
585
  process.exit(1);
550
586
  }
551
587
  const client = getClient();
@@ -555,24 +591,44 @@ postsCommand.command("update <id>").description(
555
591
  ...opts.text !== void 0 ? { text: opts.text } : {},
556
592
  ...opts.clearMedia ? { media: [] } : {},
557
593
  ...media ? { media } : {},
558
- ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
594
+ ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {},
595
+ ...opts.keepDraft ? { draft: true } : {}
559
596
  });
560
597
  if (wantsJson(opts, cmd)) {
561
598
  json(post);
562
599
  } else {
563
600
  const moved = post.rescheduled_post_ids ?? [];
564
- success(`Post ${post.id} updated`);
601
+ const promoted = post.promoted_from_draft_id ?? post.promoted_from_draft_ids?.[0];
602
+ success(promoted ? `Draft ${promoted} is now ${post.status}` : `Post ${post.id} updated`);
565
603
  if (opts.scheduleAt) {
566
604
  success(
567
605
  moved.length > 1 ? `Thread rescheduled for ${post.scheduled_at} (${moved.length} posts)` : `Scheduled for ${post.scheduled_at}`
568
606
  );
569
607
  }
608
+ if (post.warnings?.length) printDraftWarnings(post.warnings);
570
609
  }
571
610
  } catch (err) {
572
611
  apiError(err, "Failed to update post");
573
612
  process.exit(1);
574
613
  }
575
614
  });
615
+ postsCommand.command("publish <id>").description("Publish a draft now. Every rule and the monthly quota apply").option("-t, --text <text>", "Final post text, saved before publishing").option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
616
+ const client = getClient();
617
+ try {
618
+ const post = await client.updatePost(id, {
619
+ publish: true,
620
+ ...opts.text !== void 0 ? { text: opts.text } : {}
621
+ });
622
+ if (wantsJson(opts, cmd)) {
623
+ json(post);
624
+ } else {
625
+ success(`Draft ${id} published (${post.id})`);
626
+ }
627
+ } catch (err) {
628
+ apiError(err, "Failed to publish draft");
629
+ process.exit(1);
630
+ }
631
+ });
576
632
  postsCommand.command("delete <id>").description(
577
633
  "Delete a post (deleting any post of a scheduled thread cancels the thread)"
578
634
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chirpie",
3
- "version": "1.0.45",
3
+ "version": "1.0.46",
4
4
  "description": "Chirpie CLI: post to social media from the command line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,7 +44,7 @@
44
44
  ],
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
- "@chirpie/sdk": "^1.0.42",
47
+ "@chirpie/sdk": "^1.0.43",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {