chirpie 1.0.48 → 1.0.50
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 +13 -6
- package/dist/index.js +136 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,13 +30,13 @@ 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, `--first-comment` a comment published under the post as soon as it goes out, `-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, `--first-comment` posts a comment under the last part, `-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, `--first-comment` a comment published under the post as soon as it goes out, `--instagram-placement` and `--facebook-placement` to publish it as a story or a reel instead of a feed post, `-s` schedule, `--timezone` the IANA zone a `-s` with no offset is read in, `--idempotency-key` to make a retry safe, `--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, `--first-comment` posts a comment under the last part, `--timezone` and `--idempotency-key` work as they do on a post, `-f` takes a JSON payload file |
|
|
35
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, or finish a draft. `--text`, `--media`, `--alt`, `--clear-media`, `--first-comment` (`""` removes it), `--schedule-at`, `--keep-draft`. 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`, `--first-comment` (`""` removes it), `--instagram-placement`, `--facebook-placement`, `--clear-placement`, `--schedule-at`, `--timezone`, `--keep-draft`. Leaving `--schedule-at` out keeps the time it already has |
|
|
38
38
|
| `chirpie posts publish <id>` | Send a saved draft now. `-t` sets the final text first |
|
|
39
|
-
| `chirpie posts first-comment <id>` | Post a first comment that failed, again. It re-sends the text the post already carries |
|
|
39
|
+
| `chirpie posts first-comment <id>` | Post a first comment that failed, again. It re-sends the text the post already carries. `--idempotency-key` makes a retry safe |
|
|
40
40
|
| `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` |
|
|
41
41
|
| `chirpie posts hide <id>` / `unhide <id>` | Hide a post from your Chirpie listings, or show it again. Nothing reaches the platform |
|
|
42
42
|
| `chirpie accounts` | List connected accounts, active and inactive |
|
|
@@ -49,8 +49,8 @@ export CHIRPIE_API_KEY=chirpie_sk_...
|
|
|
49
49
|
| `chirpie accounts connect-linkedin --pages` | Connect the LinkedIn Pages you administer, each as its own account (coming soon) |
|
|
50
50
|
| `chirpie accounts connect-telegram` | Connect a Telegram bot to a channel or group |
|
|
51
51
|
| `chirpie accounts x-keys set` / `status` / `remove` | Use your own X developer app |
|
|
52
|
-
| `chirpie analytics <post_id>` | Engagement metrics for a published post |
|
|
53
|
-
| `chirpie keys` / `keys create` / `keys revoke <id>` | Manage API keys |
|
|
52
|
+
| `chirpie analytics <post_id>` | Engagement metrics for a published post. `--refresh` asks the platform now instead of reading the stored snapshot, once per post every 30 minutes |
|
|
53
|
+
| `chirpie keys` / `keys create` / `keys revoke <id>` | Manage API keys. `keys create --scope posts:write --scope media:write` mints a key that can do less than yours; `chirpie keys` prints each key's scopes |
|
|
54
54
|
|
|
55
55
|
Run `chirpie <command> --help` for the full option list. Threads, Instagram, Facebook, Pinterest, TikTok, YouTube and Google Business Profile are coming soon. Accounts already connected keep posting and scheduling as normal.
|
|
56
56
|
|
|
@@ -66,6 +66,13 @@ chirpie post "Look at this" -a 550e8400-e29b-41d4-a716-446655440000 -m ./shot.pn
|
|
|
66
66
|
# Schedule. Any ISO 8601 timestamp that carries a timezone.
|
|
67
67
|
chirpie post "Later" -s 2026-04-01T14:00:00Z
|
|
68
68
|
|
|
69
|
+
# Or a local time, with the zone named separately. Daylight saving is worked
|
|
70
|
+
# out for the date, which an offset computed today gets wrong.
|
|
71
|
+
chirpie post "Half nine, my time" -s 2026-11-01T09:30 --timezone America/New_York
|
|
72
|
+
|
|
73
|
+
# Make a retry safe: the same key replays the first answer for 24 hours.
|
|
74
|
+
chirpie post "Shipped" --idempotency-key release-2026-11-01
|
|
75
|
+
|
|
69
76
|
# A thread
|
|
70
77
|
chirpie thread "One" "Two" "Three" -a 550e8400-e29b-41d4-a716-446655440000
|
|
71
78
|
|
package/dist/index.js
CHANGED
|
@@ -168,6 +168,15 @@ function getClient() {
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// src/lib/flags.ts
|
|
172
|
+
function wantsJson(opts, cmd) {
|
|
173
|
+
if (opts?.json) return true;
|
|
174
|
+
return Boolean(cmd?.optsWithGlobals?.().json);
|
|
175
|
+
}
|
|
176
|
+
function idempotency(opts) {
|
|
177
|
+
return opts?.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : void 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
// src/lib/media.ts
|
|
172
181
|
import { readFile } from "fs/promises";
|
|
173
182
|
import { basename, resolve } from "path";
|
|
@@ -269,17 +278,50 @@ function printFanOut(groupId, lines) {
|
|
|
269
278
|
return failed.length;
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
// src/lib/placement.ts
|
|
282
|
+
var INSTAGRAM_PLACEMENT_HELP = 'Where the post goes on Instagram: feed (the default), story or reel. A story takes exactly one image or video, no caption (pass "" as the text) and no first comment. A reel takes exactly one video. Only pass this when the post is going to an Instagram account';
|
|
283
|
+
var FACEBOOK_PLACEMENT_HELP = "Where the post goes on a Facebook Page: feed (the default) or story. A story takes exactly one image or video, no text and no first comment. Only pass this when the post is going to a Facebook Page";
|
|
284
|
+
var INSTAGRAM_PLACEMENTS = ["feed", "story", "reel"];
|
|
285
|
+
var FACEBOOK_PLACEMENTS = ["feed", "story"];
|
|
286
|
+
function placementConfiguration(opts) {
|
|
287
|
+
const instagram = opts.instagramPlacement;
|
|
288
|
+
const facebook = opts.facebookPlacement;
|
|
289
|
+
if (instagram !== void 0 && !INSTAGRAM_PLACEMENTS.includes(instagram)) {
|
|
290
|
+
error(
|
|
291
|
+
`--instagram-placement takes ${INSTAGRAM_PLACEMENTS.join(", ")}, not "${instagram}"`
|
|
292
|
+
);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
if (facebook !== void 0 && !FACEBOOK_PLACEMENTS.includes(facebook)) {
|
|
296
|
+
error(
|
|
297
|
+
`--facebook-placement takes ${FACEBOOK_PLACEMENTS.join(", ")}, not "${facebook}"`
|
|
298
|
+
);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
if (instagram === void 0 && facebook === void 0) return void 0;
|
|
302
|
+
return {
|
|
303
|
+
...instagram !== void 0 ? { instagram: { placement: instagram } } : {},
|
|
304
|
+
...facebook !== void 0 ? { facebook: { placement: facebook } } : {}
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
272
308
|
// src/commands/post.ts
|
|
273
309
|
var postCommand = new Command3("post").description("Create a post").argument("<text>", "Post text").option(
|
|
274
310
|
"-a, --account <id>",
|
|
275
311
|
"Account ID. Repeat it to publish the same post to several accounts at once (up to 25)",
|
|
276
312
|
collect
|
|
277
|
-
).option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--first-comment <text>", "Post this as a comment under the post, as soon as it goes out. X, Threads, Instagram and Facebook only. It counts as one post against your monthly quota").option("--alt <text...>", ALT_OPTION_HELP).option(
|
|
313
|
+
).option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--first-comment <text>", "Post this as a comment under the post, as soon as it goes out. X, Threads, Instagram and Facebook only. It counts as one post against your monthly quota").option("--alt <text...>", ALT_OPTION_HELP).option("--instagram-placement <placement>", INSTAGRAM_PLACEMENT_HELP).option("--facebook-placement <placement>", FACEBOOK_PLACEMENT_HELP).option(
|
|
278
314
|
"--config <json-or-file>",
|
|
279
|
-
'Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "text": "...", "media_urls": ["..."], "first_comment": "..." } }. Every key must be an account named with --account,
|
|
280
|
-
).option("-s, --schedule <datetime>", "Schedule for later.
|
|
315
|
+
'Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "text": "...", "media_urls": ["..."], "first_comment": "...", "configuration": { "instagram": { "placement": "reel" } } } }. Every key must be an account named with --account, "first_comment": "" publishes that account without the first comment the call is sending, and "configuration" replaces the placement flags for that account and is where the options a placement carries go (collaborators, user_tags, cover, video_cover_timestamp_ms, share_to_feed, trial_reel on Instagram, link on a Facebook Page)'
|
|
316
|
+
).option("-s, --schedule <datetime>", "Schedule for later. An ISO 8601 timestamp carrying a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00), or a local time with no offset (2026-11-01T09:30) read in --timezone or the timezone saved on your account. Posts publish within ~5 minutes of the scheduled time.").option(
|
|
281
317
|
"--draft",
|
|
282
318
|
"Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
|
|
319
|
+
).option(
|
|
320
|
+
"--timezone <iana>",
|
|
321
|
+
"The IANA timezone a --schedule with no offset is read in, such as America/New_York. Daylight saving is worked out for the date you name"
|
|
322
|
+
).option(
|
|
323
|
+
"--idempotency-key <key>",
|
|
324
|
+
"Make a retry safe. The same key with the same request replays the first answer instead of sending it again, for 24 hours. One is generated per call when you do not pass one"
|
|
283
325
|
).option("--json", "Output raw JSON").action(async (text, opts) => {
|
|
284
326
|
const client = getClient();
|
|
285
327
|
const named = opts.account ?? [];
|
|
@@ -289,6 +331,7 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
|
|
|
289
331
|
}
|
|
290
332
|
const accountIds = named.length > 0 ? named : [await resolveSingleAccountId(client)];
|
|
291
333
|
const fanOut = named.length > 1 || Boolean(opts.config);
|
|
334
|
+
const configuration = placementConfiguration(opts);
|
|
292
335
|
try {
|
|
293
336
|
const media = await resolveMediaArgs(client, opts.media, opts.alt);
|
|
294
337
|
if (fanOut) {
|
|
@@ -297,14 +340,16 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
|
|
|
297
340
|
text,
|
|
298
341
|
...media ? { media } : {},
|
|
299
342
|
...opts.firstComment ? { first_comment: opts.firstComment } : {},
|
|
343
|
+
...configuration ? { configuration } : {},
|
|
300
344
|
...opts.config ? {
|
|
301
345
|
account_configurations: parseAccountConfigurations(
|
|
302
346
|
opts.config
|
|
303
347
|
)
|
|
304
348
|
} : {},
|
|
305
349
|
schedule_at: opts.schedule,
|
|
350
|
+
...opts.timezone ? { timezone: opts.timezone } : {},
|
|
306
351
|
...opts.draft ? { draft: true } : {}
|
|
307
|
-
});
|
|
352
|
+
}, idempotency(opts));
|
|
308
353
|
if (opts.json) {
|
|
309
354
|
json(result);
|
|
310
355
|
if (result.results.some((r) => !r.success)) process.exit(1);
|
|
@@ -331,9 +376,11 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
|
|
|
331
376
|
text,
|
|
332
377
|
...media ? { media } : {},
|
|
333
378
|
...opts.firstComment ? { first_comment: opts.firstComment } : {},
|
|
379
|
+
...configuration ? { configuration } : {},
|
|
334
380
|
schedule_at: opts.schedule,
|
|
381
|
+
...opts.timezone ? { timezone: opts.timezone } : {},
|
|
335
382
|
...opts.draft ? { draft: true } : {}
|
|
336
|
-
});
|
|
383
|
+
}, idempotency(opts));
|
|
337
384
|
if (opts.json) {
|
|
338
385
|
json(post);
|
|
339
386
|
} else if (post.status === "draft") {
|
|
@@ -383,10 +430,16 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
383
430
|
"JSON file with a full thread payload: { posts: [{ text, media_urls? }, ...], schedule_at?: string }. It may also carry account_ids and account_configurations."
|
|
384
431
|
).option(
|
|
385
432
|
"--config <json-or-file>",
|
|
386
|
-
`Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "posts": [{ "text": "one" }, { "text": "two" }], "first_comment": "..." } }. An account's posts replace the whole thread for that account,
|
|
387
|
-
).option("-s, --schedule <datetime>", "Schedule for later.
|
|
433
|
+
`Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "posts": [{ "text": "one" }, { "text": "two" }], "first_comment": "...", "configuration": { "facebook": { "link": "https://example.com" } } } }. An account's posts replace the whole thread for that account, "first_comment": "" publishes that account without the first comment the call is sending, and "configuration" carries the publishing options for that account. A thread publishes to the feed: an Instagram story or reel, and a Facebook Page story, are each a single post, so a thread asking for one of those placements is refused`
|
|
434
|
+
).option("-s, --schedule <datetime>", "Schedule for later. An ISO 8601 timestamp carrying a timezone (2026-04-01T14:00:00Z or 2026-04-01T16:00:00+02:00), or a local time with no offset (2026-11-01T09:30) read in --timezone or the timezone saved on your account. Threads publish within ~5 minutes of the scheduled time.").option(
|
|
388
435
|
"--draft",
|
|
389
436
|
"Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
|
|
437
|
+
).option(
|
|
438
|
+
"--timezone <iana>",
|
|
439
|
+
"The IANA timezone a --schedule with no offset is read in, such as America/New_York. Daylight saving is worked out for the date you name"
|
|
440
|
+
).option(
|
|
441
|
+
"--idempotency-key <key>",
|
|
442
|
+
"Make a retry safe. The same key with the same request replays the first answer instead of sending it again, for 24 hours. One is generated per call when you do not pass one"
|
|
390
443
|
).option("--json", "Output raw JSON").action(async (posts, opts) => {
|
|
391
444
|
const client = getClient();
|
|
392
445
|
let postPayload;
|
|
@@ -464,8 +517,9 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
464
517
|
...opts.firstComment ? { first_comment: opts.firstComment } : {},
|
|
465
518
|
...configurations ? { account_configurations: configurations } : {},
|
|
466
519
|
schedule_at: scheduleAt,
|
|
520
|
+
...opts.timezone ? { timezone: opts.timezone } : {},
|
|
467
521
|
...opts.draft ? { draft: true } : {}
|
|
468
|
-
});
|
|
522
|
+
}, idempotency(opts));
|
|
469
523
|
if (opts.json) {
|
|
470
524
|
json(result);
|
|
471
525
|
if (result.results.some((r) => !r.success)) process.exit(1);
|
|
@@ -492,8 +546,9 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
492
546
|
posts: postPayload,
|
|
493
547
|
...opts.firstComment ? { first_comment: opts.firstComment } : {},
|
|
494
548
|
schedule_at: scheduleAt,
|
|
549
|
+
...opts.timezone ? { timezone: opts.timezone } : {},
|
|
495
550
|
...opts.draft ? { draft: true } : {}
|
|
496
|
-
});
|
|
551
|
+
}, idempotency(opts));
|
|
497
552
|
if (opts.json) {
|
|
498
553
|
json(thread);
|
|
499
554
|
} else if (thread.status === "draft") {
|
|
@@ -511,14 +566,6 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
511
566
|
// src/commands/posts.ts
|
|
512
567
|
import { Command as Command5 } from "commander";
|
|
513
568
|
import { postFailureReason } from "@chirpie/sdk";
|
|
514
|
-
|
|
515
|
-
// src/lib/flags.ts
|
|
516
|
-
function wantsJson(opts, cmd) {
|
|
517
|
-
if (opts?.json) return true;
|
|
518
|
-
return Boolean(cmd?.optsWithGlobals?.().json);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// src/commands/posts.ts
|
|
522
569
|
var postsCommand = new Command5("posts").description("List, get, update, delete, hide or unhide posts").option(
|
|
523
570
|
"--status <status>",
|
|
524
571
|
"Filter by status: draft, scheduled, publishing, published, failed, deleted"
|
|
@@ -587,10 +634,22 @@ postsCommand.command("update <id>").description(
|
|
|
587
634
|
"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."
|
|
588
635
|
).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
|
|
589
636
|
"-s, --schedule-at <datetime>",
|
|
590
|
-
"New publish time.
|
|
637
|
+
"New publish time. An ISO 8601 timestamp carrying a timezone (2027-04-01T14:00:00Z or 2027-04-01T16:00:00+02:00), or a local time with no offset (2027-04-01T09:30) read in --timezone or the timezone saved on your account. 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."
|
|
638
|
+
).option(
|
|
639
|
+
"--timezone <iana>",
|
|
640
|
+
"The IANA timezone a --schedule-at with no offset is read in, such as America/New_York"
|
|
591
641
|
).option(
|
|
592
642
|
"--first-comment <text>",
|
|
593
643
|
'The comment to post under this one once it publishes. Pass "" to remove the one it has'
|
|
644
|
+
).option(
|
|
645
|
+
"--instagram-placement <placement>",
|
|
646
|
+
`${INSTAGRAM_PLACEMENT_HELP}. On an update this replaces every publishing option the post carries, so a queued reel loses its collaborators, cover and trial-reel setting unless you send them again with --config`
|
|
647
|
+
).option(
|
|
648
|
+
"--facebook-placement <placement>",
|
|
649
|
+
`${FACEBOOK_PLACEMENT_HELP}. On an update this replaces every publishing option the post carries`
|
|
650
|
+
).option(
|
|
651
|
+
"--clear-placement",
|
|
652
|
+
"Put the post back to a plain feed post, dropping every publishing option it carries"
|
|
594
653
|
).option(
|
|
595
654
|
"--keep-draft",
|
|
596
655
|
"Keep a draft a draft, so --schedule-at only changes the time it remembers"
|
|
@@ -599,12 +658,19 @@ postsCommand.command("update <id>").description(
|
|
|
599
658
|
error("Pass either --media or --clear-media, not both");
|
|
600
659
|
process.exit(1);
|
|
601
660
|
}
|
|
602
|
-
if (
|
|
661
|
+
if (opts.clearPlacement && (opts.instagramPlacement !== void 0 || opts.facebookPlacement !== void 0)) {
|
|
662
|
+
error(
|
|
663
|
+
"Pass either --clear-placement or a placement flag, not both"
|
|
664
|
+
);
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
if (!opts.text && !opts.media && !opts.clearMedia && opts.firstComment === void 0 && opts.instagramPlacement === void 0 && opts.facebookPlacement === void 0 && !opts.clearPlacement && !opts.scheduleAt && !opts.keepDraft) {
|
|
603
668
|
error(
|
|
604
|
-
"Nothing to change. Pass --text, --media, --clear-media, --first-comment, --schedule-at or --keep-draft"
|
|
669
|
+
"Nothing to change. Pass --text, --media, --clear-media, --first-comment, --instagram-placement, --facebook-placement, --clear-placement, --schedule-at or --keep-draft"
|
|
605
670
|
);
|
|
606
671
|
process.exit(1);
|
|
607
672
|
}
|
|
673
|
+
const configuration = opts.clearPlacement ? {} : placementConfiguration(opts);
|
|
608
674
|
const client = getClient();
|
|
609
675
|
try {
|
|
610
676
|
const media = await resolveMediaArgs(client, opts.media, opts.alt);
|
|
@@ -613,7 +679,9 @@ postsCommand.command("update <id>").description(
|
|
|
613
679
|
...opts.clearMedia ? { media: [] } : {},
|
|
614
680
|
...media ? { media } : {},
|
|
615
681
|
...opts.firstComment !== void 0 ? { first_comment: opts.firstComment } : {},
|
|
682
|
+
...configuration ? { configuration } : {},
|
|
616
683
|
...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {},
|
|
684
|
+
...opts.timezone ? { timezone: opts.timezone } : {},
|
|
617
685
|
...opts.keepDraft ? { draft: true } : {}
|
|
618
686
|
});
|
|
619
687
|
if (wantsJson(opts, cmd)) {
|
|
@@ -674,10 +742,13 @@ postsCommand.command("delete <id>").description(
|
|
|
674
742
|
});
|
|
675
743
|
postsCommand.command("first-comment <id>").description(
|
|
676
744
|
"Post a first comment that failed, again. Re-sends the text the post already carries"
|
|
745
|
+
).option(
|
|
746
|
+
"--idempotency-key <key>",
|
|
747
|
+
"Make a retry safe. The same key replays the first answer instead of sending the comment again, for 24 hours"
|
|
677
748
|
).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
|
678
749
|
const client = getClient();
|
|
679
750
|
try {
|
|
680
|
-
const post = await client.retryFirstComment(id);
|
|
751
|
+
const post = await client.retryFirstComment(id, idempotency(opts));
|
|
681
752
|
if (wantsJson(opts, cmd)) {
|
|
682
753
|
json(post);
|
|
683
754
|
} else {
|
|
@@ -1220,10 +1291,18 @@ commentsCommand.command("list").description("List the comments on a post you pub
|
|
|
1220
1291
|
process.exit(1);
|
|
1221
1292
|
}
|
|
1222
1293
|
});
|
|
1223
|
-
commentsCommand.command("reply").description("Reply to a comment (counts as one post against your quota)").argument("<post_id>", "Post ID").argument("<comment_id>", "Comment ID from `chirpie comments list`").requiredOption("--text <text>", "The reply text").option(
|
|
1294
|
+
commentsCommand.command("reply").description("Reply to a comment (counts as one post against your quota)").argument("<post_id>", "Post ID").argument("<comment_id>", "Comment ID from `chirpie comments list`").requiredOption("--text <text>", "The reply text").option(
|
|
1295
|
+
"--idempotency-key <key>",
|
|
1296
|
+
"Make a retry safe. The same key with the same reply replays the first answer instead of sending it again, for 24 hours"
|
|
1297
|
+
).option("--json", "Output raw JSON").action(async (postId, commentId, opts, cmd) => {
|
|
1224
1298
|
const client = getClient();
|
|
1225
1299
|
try {
|
|
1226
|
-
const reply = await client.replyToComment(
|
|
1300
|
+
const reply = await client.replyToComment(
|
|
1301
|
+
postId,
|
|
1302
|
+
commentId,
|
|
1303
|
+
opts.text,
|
|
1304
|
+
idempotency(opts)
|
|
1305
|
+
);
|
|
1227
1306
|
if (wantsJson(opts, cmd)) {
|
|
1228
1307
|
json(reply);
|
|
1229
1308
|
} else {
|
|
@@ -1283,10 +1362,15 @@ async function confirm2(question) {
|
|
|
1283
1362
|
|
|
1284
1363
|
// src/commands/analytics.ts
|
|
1285
1364
|
import { Command as Command8 } from "commander";
|
|
1286
|
-
var analyticsCommand = new Command8("analytics").description("Get analytics for a post").argument("<post_id>", "Post ID").option(
|
|
1365
|
+
var analyticsCommand = new Command8("analytics").description("Get analytics for a post").argument("<post_id>", "Post ID").option(
|
|
1366
|
+
"--refresh",
|
|
1367
|
+
"Ask the platform now instead of reading the stored numbers. Allowed once per post every 30 minutes"
|
|
1368
|
+
).option("--json", "Output raw JSON").action(async (postId, opts) => {
|
|
1287
1369
|
const client = getClient();
|
|
1288
1370
|
try {
|
|
1289
|
-
const analytics = await client.getPostAnalytics(postId
|
|
1371
|
+
const analytics = await client.getPostAnalytics(postId, {
|
|
1372
|
+
refresh: Boolean(opts.refresh)
|
|
1373
|
+
});
|
|
1290
1374
|
if (opts.json) {
|
|
1291
1375
|
json(analytics);
|
|
1292
1376
|
} else {
|
|
@@ -1308,6 +1392,7 @@ var analyticsCommand = new Command8("analytics").description("Get analytics for
|
|
|
1308
1392
|
|
|
1309
1393
|
// src/commands/keys.ts
|
|
1310
1394
|
import { Command as Command9 } from "commander";
|
|
1395
|
+
import { API_SCOPES } from "@chirpie/sdk";
|
|
1311
1396
|
var keysCommand = new Command9("keys").description("Manage API keys").option("--json", "Output raw JSON").action(async (opts, cmd) => {
|
|
1312
1397
|
const client = getClient();
|
|
1313
1398
|
try {
|
|
@@ -1320,6 +1405,9 @@ var keysCommand = new Command9("keys").description("Manage API keys").option("--
|
|
|
1320
1405
|
id: k.id.slice(0, 8),
|
|
1321
1406
|
name: k.name,
|
|
1322
1407
|
prefix: k.key_prefix,
|
|
1408
|
+
// Null scopes mean full access, which is not the same as every
|
|
1409
|
+
// scope listed: it also covers anything added later.
|
|
1410
|
+
scopes: k.scopes === null ? "all" : k.scopes.join(" "),
|
|
1323
1411
|
// GET /v1/keys only ever returns live keys, so there is no
|
|
1324
1412
|
// revoked column to show.
|
|
1325
1413
|
expires: k.expires_at?.slice(0, 10) ?? "never",
|
|
@@ -1332,10 +1420,27 @@ var keysCommand = new Command9("keys").description("Manage API keys").option("--
|
|
|
1332
1420
|
process.exit(1);
|
|
1333
1421
|
}
|
|
1334
1422
|
});
|
|
1335
|
-
keysCommand.command("create").description("Create a new API key").option("-n, --name <name>", "Key name").option(
|
|
1423
|
+
keysCommand.command("create").description("Create a new API key").option("-n, --name <name>", "Key name").option(
|
|
1424
|
+
"--scope <scope>",
|
|
1425
|
+
`Limit what the new key can do. Repeat it for each one: ${API_SCOPES.join(", ")}. Leave it out and the key can do everything this one can`,
|
|
1426
|
+
collect
|
|
1427
|
+
).option("--json", "Output raw JSON").action(async (opts, cmd) => {
|
|
1336
1428
|
const client = getClient();
|
|
1429
|
+
const scopes = opts.scope;
|
|
1430
|
+
const unknown = (scopes ?? []).filter(
|
|
1431
|
+
(scope) => !API_SCOPES.includes(scope)
|
|
1432
|
+
);
|
|
1433
|
+
if (unknown.length > 0) {
|
|
1434
|
+
error(
|
|
1435
|
+
`Not a scope: ${unknown.join(", ")}. Allowed values: ${API_SCOPES.join(", ")}`
|
|
1436
|
+
);
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1337
1439
|
try {
|
|
1338
|
-
const key = await client.createKey(
|
|
1440
|
+
const key = await client.createKey({
|
|
1441
|
+
...opts.name ? { name: opts.name } : {},
|
|
1442
|
+
...scopes ? { scopes } : {}
|
|
1443
|
+
});
|
|
1339
1444
|
if (wantsJson(opts, cmd)) {
|
|
1340
1445
|
json(key);
|
|
1341
1446
|
} else {
|
|
@@ -1343,6 +1448,10 @@ keysCommand.command("create").description("Create a new API key").option("-n, --
|
|
|
1343
1448
|
console.log(`
|
|
1344
1449
|
${key.key}
|
|
1345
1450
|
`);
|
|
1451
|
+
console.log(
|
|
1452
|
+
` Scopes: ${key.scopes === null ? "all" : key.scopes.join(" ")}
|
|
1453
|
+
`
|
|
1454
|
+
);
|
|
1346
1455
|
}
|
|
1347
1456
|
} catch (err) {
|
|
1348
1457
|
error(err instanceof Error ? err.message : "Failed to create key");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chirpie",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
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.
|
|
47
|
+
"@chirpie/sdk": "^1.0.47",
|
|
48
48
|
"commander": "^13"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|