chirpie 1.0.45 → 1.0.47

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 +26 -5
  2. package/dist/index.js +124 -18
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,12 +30,14 @@ 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 |
35
- | `chirpie posts` | List posts. `--status`, `--account`, `--group`, `--limit` |
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
+ | `chirpie posts` | List posts. `--status`, `--account`, `--group`, `--limit`, `--include-hidden` |
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 |
38
- | `chirpie posts delete <id>` | Delete a post. Deleting any post of a scheduled thread cancels the thread |
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 |
39
+ | `chirpie posts delete <id>` | Take a post down from the platform. Chirpie keeps it, marked deleted. Deleting any post of a scheduled thread cancels the thread. Instagram and TikTok offer no delete API and refuse with `delete_unsupported` |
40
+ | `chirpie posts hide <id>` / `unhide <id>` | Hide a post from your Chirpie listings, or show it again. Nothing reaches the platform |
39
41
  | `chirpie accounts` | List connected accounts, active and inactive |
40
42
  | `chirpie accounts activate <id>` / `deactivate <id>` | Choose which accounts publish. Deactivating cancels that account's scheduled posts |
41
43
  | `chirpie accounts disconnect <id>` | End a connection. Cancels that account's scheduled posts, frees a plan slot, and removes the stored credential, so reconnecting means authorizing again. Asks first; `-y` skips the prompt |
@@ -70,6 +72,25 @@ chirpie thread "One" "Two" "Three" -a 550e8400-e29b-41d4-a716-446655440000
70
72
  chirpie posts --status scheduled
71
73
  ```
72
74
 
75
+ ## Drafts
76
+
77
+ `--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.
78
+
79
+ ```bash
80
+ chirpie post "Half an idea" --draft
81
+ chirpie thread "Opening line" --draft # a draft thread may be one post
82
+ chirpie posts --status draft # what is saved
83
+
84
+ # Change only the time it remembers
85
+ chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z --keep-draft
86
+
87
+ # Promote it: queue it, or send it now
88
+ chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z
89
+ chirpie posts publish "$POST_ID"
90
+ ```
91
+
92
+ 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.
93
+
73
94
  ## Several accounts at once
74
95
 
75
96
  Repeat `-a` to publish the same post to up to 25 accounts in one call.
package/dist/index.js CHANGED
@@ -31,10 +31,22 @@ function apiError(err, fallback) {
31
31
  const maybe = err;
32
32
  if (typeof maybe?.code === "string" && typeof maybe?.message === "string") {
33
33
  error(`${maybe.code}: ${maybe.message}`);
34
+ printThreadRollback(err);
34
35
  return;
35
36
  }
36
37
  error(err instanceof Error ? err.message : fallback);
37
38
  }
39
+ function printThreadRollback(err) {
40
+ const rollback = err?.details?.thread_rollback;
41
+ const live = rollback?.still_live ?? [];
42
+ if (live.length === 0) return;
43
+ error("Still live on the platform, delete these yourself:");
44
+ for (const part of live) {
45
+ error(
46
+ ` post ${part.thread_order + 1}: ${part.platform_post_url ?? part.platform_post_id}`
47
+ );
48
+ }
49
+ }
38
50
  function info(msg) {
39
51
  console.log(`\x1B[36m\u2139\x1B[0m ${msg}`);
40
52
  }
@@ -227,6 +239,14 @@ function parseAccountConfigurations(value) {
227
239
  }
228
240
  return parsed;
229
241
  }
242
+ function printDraftWarnings(warnings) {
243
+ if (!Array.isArray(warnings)) return;
244
+ for (const warning of warnings) {
245
+ const entry = warning;
246
+ if (typeof entry?.message !== "string") continue;
247
+ info(`${entry.platform} ${entry.account_id}: ${entry.message}`);
248
+ }
249
+ }
230
250
  function printFanOut(groupId, lines) {
231
251
  const failed = lines.filter((line) => !line.success);
232
252
  console.log(`Group ${groupId}`);
@@ -257,7 +277,10 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
257
277
  ).option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--alt <text...>", ALT_OPTION_HELP).option(
258
278
  "--config <json-or-file>",
259
279
  '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) => {
280
+ ).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(
281
+ "--draft",
282
+ "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
283
+ ).option("--json", "Output raw JSON").action(async (text, opts) => {
261
284
  const client = getClient();
262
285
  const named = opts.account ?? [];
263
286
  if (opts.config && named.length === 0) {
@@ -278,13 +301,15 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
278
301
  opts.config
279
302
  )
280
303
  } : {},
281
- schedule_at: opts.schedule
304
+ schedule_at: opts.schedule,
305
+ ...opts.draft ? { draft: true } : {}
282
306
  });
283
307
  if (opts.json) {
284
308
  json(result);
285
309
  if (result.results.some((r) => !r.success)) process.exit(1);
286
310
  return;
287
311
  }
312
+ if ("warnings" in result) printDraftWarnings(result.warnings);
288
313
  const failed = printFanOut(
289
314
  result.group_id,
290
315
  result.results.map((r) => ({
@@ -304,10 +329,14 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
304
329
  account_id: accountIds[0],
305
330
  text,
306
331
  ...media ? { media } : {},
307
- schedule_at: opts.schedule
332
+ schedule_at: opts.schedule,
333
+ ...opts.draft ? { draft: true } : {}
308
334
  });
309
335
  if (opts.json) {
310
336
  json(post);
337
+ } else if (post.status === "draft") {
338
+ success(`Draft saved (${post.id})`);
339
+ if ("warnings" in post) printDraftWarnings(post.warnings);
311
340
  } else if (post.status === "scheduled") {
312
341
  success(`Post scheduled for ${post.scheduled_at} (${post.id})`);
313
342
  } else {
@@ -353,7 +382,10 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
353
382
  ).option(
354
383
  "--config <json-or-file>",
355
384
  `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) => {
385
+ ).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(
386
+ "--draft",
387
+ "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
388
+ ).option("--json", "Output raw JSON").action(async (posts, opts) => {
357
389
  const client = getClient();
358
390
  let postPayload;
359
391
  let scheduleAt = opts.schedule;
@@ -375,8 +407,10 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
375
407
  process.exit(1);
376
408
  }
377
409
  } else {
378
- if (posts.length < 2) {
379
- error("Thread must have at least 2 posts (or use --file)");
410
+ if (posts.length < (opts.draft ? 1 : 2)) {
411
+ error(
412
+ opts.draft ? "Pass at least one post (or use --file)" : "Thread must have at least 2 posts (or use --file)"
413
+ );
380
414
  process.exit(1);
381
415
  }
382
416
  postPayload = posts.map((text) => ({ text }));
@@ -426,13 +460,15 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
426
460
  account_ids: accountIds,
427
461
  posts: postPayload,
428
462
  ...configurations ? { account_configurations: configurations } : {},
429
- schedule_at: scheduleAt
463
+ schedule_at: scheduleAt,
464
+ ...opts.draft ? { draft: true } : {}
430
465
  });
431
466
  if (opts.json) {
432
467
  json(result);
433
468
  if (result.results.some((r) => !r.success)) process.exit(1);
434
469
  return;
435
470
  }
471
+ if ("warnings" in result) printDraftWarnings(result.warnings);
436
472
  const failed = printFanOut(
437
473
  result.group_id,
438
474
  result.results.map((r) => ({
@@ -451,10 +487,14 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
451
487
  const thread = await client.createThread({
452
488
  account_id: accountIds[0],
453
489
  posts: postPayload,
454
- schedule_at: scheduleAt
490
+ schedule_at: scheduleAt,
491
+ ...opts.draft ? { draft: true } : {}
455
492
  });
456
493
  if (opts.json) {
457
494
  json(thread);
495
+ } else if (thread.status === "draft") {
496
+ success(`Draft thread saved with ${thread.posts.length} posts (${thread.id})`);
497
+ if ("warnings" in thread) printDraftWarnings(thread.warnings);
458
498
  } else {
459
499
  success(`Thread created with ${thread.posts.length} posts (${thread.id})`);
460
500
  }
@@ -475,17 +515,21 @@ function wantsJson(opts, cmd) {
475
515
  }
476
516
 
477
517
  // 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(
518
+ var postsCommand = new Command5("posts").description("List, get, update, delete, hide or unhide posts").option(
519
+ "--status <status>",
520
+ "Filter by status: draft, scheduled, publishing, published, failed, deleted"
521
+ ).option("--account <id>", "Filter by account").option(
479
522
  "--group <id>",
480
523
  "Only the posts of one fan-out, from the group ID a multi-account post returned"
481
- ).option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts, cmd) => {
524
+ ).option("--limit <n>", "Limit results", "20").option("--include-hidden", "Include posts you have hidden").option("--json", "Output raw JSON").action(async (opts, cmd) => {
482
525
  const client = getClient();
483
526
  try {
484
527
  const posts = await client.listPosts({
485
528
  status: opts.status,
486
529
  account_id: opts.account,
487
530
  group_id: opts.group,
488
- limit: parseInt(opts.limit)
531
+ limit: parseInt(opts.limit),
532
+ include_hidden: opts.includeHidden
489
533
  });
490
534
  if (wantsJson(opts, cmd)) {
491
535
  json(posts);
@@ -496,6 +540,7 @@ var postsCommand = new Command5("posts").description("List, get, update, or dele
496
540
  status: p.status,
497
541
  text: p.text.length > 50 ? p.text.slice(0, 50) + "..." : p.text,
498
542
  created: p.created_at.slice(0, 10),
543
+ hidden: p.hidden ? "yes" : "",
499
544
  // Why a post is not where you expected it. Empty for the ones
500
545
  // that are exactly where you left them.
501
546
  issue: postFailureReason(p) ?? ""
@@ -532,20 +577,25 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
532
577
  }
533
578
  });
534
579
  postsCommand.command("update <id>").description(
535
- "Edit a post that has not published yet. Leaving --schedule-at out keeps the time it already has"
580
+ "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
581
  ).option("-t, --text <text>", "Replacement post text").option(
537
582
  "-m, --media <files...>",
538
583
  "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
584
  ).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
540
585
  "-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."
586
+ "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."
587
+ ).option(
588
+ "--keep-draft",
589
+ "Keep a draft a draft, so --schedule-at only changes the time it remembers"
542
590
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
543
591
  if (opts.media && opts.clearMedia) {
544
592
  error("Pass either --media or --clear-media, not both");
545
593
  process.exit(1);
546
594
  }
547
- if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt) {
548
- error("Nothing to change. Pass --text, --media, --clear-media or --schedule-at");
595
+ if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt && !opts.keepDraft) {
596
+ error(
597
+ "Nothing to change. Pass --text, --media, --clear-media, --schedule-at or --keep-draft"
598
+ );
549
599
  process.exit(1);
550
600
  }
551
601
  const client = getClient();
@@ -555,26 +605,46 @@ postsCommand.command("update <id>").description(
555
605
  ...opts.text !== void 0 ? { text: opts.text } : {},
556
606
  ...opts.clearMedia ? { media: [] } : {},
557
607
  ...media ? { media } : {},
558
- ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
608
+ ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {},
609
+ ...opts.keepDraft ? { draft: true } : {}
559
610
  });
560
611
  if (wantsJson(opts, cmd)) {
561
612
  json(post);
562
613
  } else {
563
614
  const moved = post.rescheduled_post_ids ?? [];
564
- success(`Post ${post.id} updated`);
615
+ const promoted = post.promoted_from_draft_id ?? post.promoted_from_draft_ids?.[0];
616
+ success(promoted ? `Draft ${promoted} is now ${post.status}` : `Post ${post.id} updated`);
565
617
  if (opts.scheduleAt) {
566
618
  success(
567
619
  moved.length > 1 ? `Thread rescheduled for ${post.scheduled_at} (${moved.length} posts)` : `Scheduled for ${post.scheduled_at}`
568
620
  );
569
621
  }
622
+ if (post.warnings?.length) printDraftWarnings(post.warnings);
570
623
  }
571
624
  } catch (err) {
572
625
  apiError(err, "Failed to update post");
573
626
  process.exit(1);
574
627
  }
575
628
  });
629
+ 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) => {
630
+ const client = getClient();
631
+ try {
632
+ const post = await client.updatePost(id, {
633
+ publish: true,
634
+ ...opts.text !== void 0 ? { text: opts.text } : {}
635
+ });
636
+ if (wantsJson(opts, cmd)) {
637
+ json(post);
638
+ } else {
639
+ success(`Draft ${id} published (${post.id})`);
640
+ }
641
+ } catch (err) {
642
+ apiError(err, "Failed to publish draft");
643
+ process.exit(1);
644
+ }
645
+ });
576
646
  postsCommand.command("delete <id>").description(
577
- "Delete a post (deleting any post of a scheduled thread cancels the thread)"
647
+ "Delete a post from the platform (a scheduled thread is cancelled whole; the post stays in Chirpie marked deleted)"
578
648
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
579
649
  const client = getClient();
580
650
  try {
@@ -594,6 +664,42 @@ postsCommand.command("delete <id>").description(
594
664
  process.exit(1);
595
665
  }
596
666
  });
667
+ postsCommand.command("hide <id>").description(
668
+ "Hide a post from your Chirpie listings (nothing reaches the platform)"
669
+ ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
670
+ const client = getClient();
671
+ try {
672
+ const result = await client.hidePost(id);
673
+ if (wantsJson(opts, cmd)) {
674
+ json(result);
675
+ } else {
676
+ const n = result.hidden_ids.length;
677
+ success(
678
+ n > 1 ? `Hidden from Chirpie (${n} posts): ${result.hidden_ids.join(", ")}` : `Post ${id} hidden from Chirpie. It is unchanged on the platform.`
679
+ );
680
+ }
681
+ } catch (err) {
682
+ error(err instanceof Error ? err.message : "Failed to hide post");
683
+ process.exit(1);
684
+ }
685
+ });
686
+ postsCommand.command("unhide <id>").description("Put a hidden post back in your Chirpie listings").option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
687
+ const client = getClient();
688
+ try {
689
+ const result = await client.unhidePost(id);
690
+ if (wantsJson(opts, cmd)) {
691
+ json(result);
692
+ } else {
693
+ const n = result.hidden_ids.length;
694
+ success(
695
+ n > 1 ? `Showing again (${n} posts): ${result.hidden_ids.join(", ")}` : `Post ${id} is showing again`
696
+ );
697
+ }
698
+ } catch (err) {
699
+ error(err instanceof Error ? err.message : "Failed to unhide post");
700
+ process.exit(1);
701
+ }
702
+ });
597
703
 
598
704
  // src/commands/accounts.ts
599
705
  import { Command as Command6 } from "commander";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chirpie",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
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.44",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {