chirpie 1.0.48 → 1.0.49

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 +13 -6
  2. package/dist/index.js +84 -22
  3. 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, `-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), `--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";
@@ -277,9 +286,15 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
277
286
  ).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(
278
287
  "--config <json-or-file>",
279
288
  '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, and "first_comment": "" publishes that account without the first comment the call is sending'
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(
289
+ ).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
290
  "--draft",
282
291
  "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
292
+ ).option(
293
+ "--timezone <iana>",
294
+ "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"
295
+ ).option(
296
+ "--idempotency-key <key>",
297
+ "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
298
  ).option("--json", "Output raw JSON").action(async (text, opts) => {
284
299
  const client = getClient();
285
300
  const named = opts.account ?? [];
@@ -303,8 +318,9 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
303
318
  )
304
319
  } : {},
305
320
  schedule_at: opts.schedule,
321
+ ...opts.timezone ? { timezone: opts.timezone } : {},
306
322
  ...opts.draft ? { draft: true } : {}
307
- });
323
+ }, idempotency(opts));
308
324
  if (opts.json) {
309
325
  json(result);
310
326
  if (result.results.some((r) => !r.success)) process.exit(1);
@@ -332,8 +348,9 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
332
348
  ...media ? { media } : {},
333
349
  ...opts.firstComment ? { first_comment: opts.firstComment } : {},
334
350
  schedule_at: opts.schedule,
351
+ ...opts.timezone ? { timezone: opts.timezone } : {},
335
352
  ...opts.draft ? { draft: true } : {}
336
- });
353
+ }, idempotency(opts));
337
354
  if (opts.json) {
338
355
  json(post);
339
356
  } else if (post.status === "draft") {
@@ -384,9 +401,15 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
384
401
  ).option(
385
402
  "--config <json-or-file>",
386
403
  `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, and "first_comment": "" publishes that account without the first comment the call is sending`
387
- ).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(
404
+ ).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
405
  "--draft",
389
406
  "Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
407
+ ).option(
408
+ "--timezone <iana>",
409
+ "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"
410
+ ).option(
411
+ "--idempotency-key <key>",
412
+ "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
413
  ).option("--json", "Output raw JSON").action(async (posts, opts) => {
391
414
  const client = getClient();
392
415
  let postPayload;
@@ -464,8 +487,9 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
464
487
  ...opts.firstComment ? { first_comment: opts.firstComment } : {},
465
488
  ...configurations ? { account_configurations: configurations } : {},
466
489
  schedule_at: scheduleAt,
490
+ ...opts.timezone ? { timezone: opts.timezone } : {},
467
491
  ...opts.draft ? { draft: true } : {}
468
- });
492
+ }, idempotency(opts));
469
493
  if (opts.json) {
470
494
  json(result);
471
495
  if (result.results.some((r) => !r.success)) process.exit(1);
@@ -492,8 +516,9 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
492
516
  posts: postPayload,
493
517
  ...opts.firstComment ? { first_comment: opts.firstComment } : {},
494
518
  schedule_at: scheduleAt,
519
+ ...opts.timezone ? { timezone: opts.timezone } : {},
495
520
  ...opts.draft ? { draft: true } : {}
496
- });
521
+ }, idempotency(opts));
497
522
  if (opts.json) {
498
523
  json(thread);
499
524
  } else if (thread.status === "draft") {
@@ -511,14 +536,6 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
511
536
  // src/commands/posts.ts
512
537
  import { Command as Command5 } from "commander";
513
538
  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
539
  var postsCommand = new Command5("posts").description("List, get, update, delete, hide or unhide posts").option(
523
540
  "--status <status>",
524
541
  "Filter by status: draft, scheduled, publishing, published, failed, deleted"
@@ -587,7 +604,10 @@ postsCommand.command("update <id>").description(
587
604
  "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
605
  ).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
589
606
  "-s, --schedule-at <datetime>",
590
- "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."
607
+ "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."
608
+ ).option(
609
+ "--timezone <iana>",
610
+ "The IANA timezone a --schedule-at with no offset is read in, such as America/New_York"
591
611
  ).option(
592
612
  "--first-comment <text>",
593
613
  'The comment to post under this one once it publishes. Pass "" to remove the one it has'
@@ -614,6 +634,7 @@ postsCommand.command("update <id>").description(
614
634
  ...media ? { media } : {},
615
635
  ...opts.firstComment !== void 0 ? { first_comment: opts.firstComment } : {},
616
636
  ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {},
637
+ ...opts.timezone ? { timezone: opts.timezone } : {},
617
638
  ...opts.keepDraft ? { draft: true } : {}
618
639
  });
619
640
  if (wantsJson(opts, cmd)) {
@@ -674,10 +695,13 @@ postsCommand.command("delete <id>").description(
674
695
  });
675
696
  postsCommand.command("first-comment <id>").description(
676
697
  "Post a first comment that failed, again. Re-sends the text the post already carries"
698
+ ).option(
699
+ "--idempotency-key <key>",
700
+ "Make a retry safe. The same key replays the first answer instead of sending the comment again, for 24 hours"
677
701
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
678
702
  const client = getClient();
679
703
  try {
680
- const post = await client.retryFirstComment(id);
704
+ const post = await client.retryFirstComment(id, idempotency(opts));
681
705
  if (wantsJson(opts, cmd)) {
682
706
  json(post);
683
707
  } else {
@@ -1220,10 +1244,18 @@ commentsCommand.command("list").description("List the comments on a post you pub
1220
1244
  process.exit(1);
1221
1245
  }
1222
1246
  });
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("--json", "Output raw JSON").action(async (postId, commentId, opts, cmd) => {
1247
+ 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(
1248
+ "--idempotency-key <key>",
1249
+ "Make a retry safe. The same key with the same reply replays the first answer instead of sending it again, for 24 hours"
1250
+ ).option("--json", "Output raw JSON").action(async (postId, commentId, opts, cmd) => {
1224
1251
  const client = getClient();
1225
1252
  try {
1226
- const reply = await client.replyToComment(postId, commentId, opts.text);
1253
+ const reply = await client.replyToComment(
1254
+ postId,
1255
+ commentId,
1256
+ opts.text,
1257
+ idempotency(opts)
1258
+ );
1227
1259
  if (wantsJson(opts, cmd)) {
1228
1260
  json(reply);
1229
1261
  } else {
@@ -1283,10 +1315,15 @@ async function confirm2(question) {
1283
1315
 
1284
1316
  // src/commands/analytics.ts
1285
1317
  import { Command as Command8 } from "commander";
1286
- var analyticsCommand = new Command8("analytics").description("Get analytics for a post").argument("<post_id>", "Post ID").option("--json", "Output raw JSON").action(async (postId, opts) => {
1318
+ var analyticsCommand = new Command8("analytics").description("Get analytics for a post").argument("<post_id>", "Post ID").option(
1319
+ "--refresh",
1320
+ "Ask the platform now instead of reading the stored numbers. Allowed once per post every 30 minutes"
1321
+ ).option("--json", "Output raw JSON").action(async (postId, opts) => {
1287
1322
  const client = getClient();
1288
1323
  try {
1289
- const analytics = await client.getPostAnalytics(postId);
1324
+ const analytics = await client.getPostAnalytics(postId, {
1325
+ refresh: Boolean(opts.refresh)
1326
+ });
1290
1327
  if (opts.json) {
1291
1328
  json(analytics);
1292
1329
  } else {
@@ -1308,6 +1345,7 @@ var analyticsCommand = new Command8("analytics").description("Get analytics for
1308
1345
 
1309
1346
  // src/commands/keys.ts
1310
1347
  import { Command as Command9 } from "commander";
1348
+ import { API_SCOPES } from "@chirpie/sdk";
1311
1349
  var keysCommand = new Command9("keys").description("Manage API keys").option("--json", "Output raw JSON").action(async (opts, cmd) => {
1312
1350
  const client = getClient();
1313
1351
  try {
@@ -1320,6 +1358,9 @@ var keysCommand = new Command9("keys").description("Manage API keys").option("--
1320
1358
  id: k.id.slice(0, 8),
1321
1359
  name: k.name,
1322
1360
  prefix: k.key_prefix,
1361
+ // Null scopes mean full access, which is not the same as every
1362
+ // scope listed: it also covers anything added later.
1363
+ scopes: k.scopes === null ? "all" : k.scopes.join(" "),
1323
1364
  // GET /v1/keys only ever returns live keys, so there is no
1324
1365
  // revoked column to show.
1325
1366
  expires: k.expires_at?.slice(0, 10) ?? "never",
@@ -1332,10 +1373,27 @@ var keysCommand = new Command9("keys").description("Manage API keys").option("--
1332
1373
  process.exit(1);
1333
1374
  }
1334
1375
  });
1335
- keysCommand.command("create").description("Create a new API key").option("-n, --name <name>", "Key name").option("--json", "Output raw JSON").action(async (opts, cmd) => {
1376
+ keysCommand.command("create").description("Create a new API key").option("-n, --name <name>", "Key name").option(
1377
+ "--scope <scope>",
1378
+ `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`,
1379
+ collect
1380
+ ).option("--json", "Output raw JSON").action(async (opts, cmd) => {
1336
1381
  const client = getClient();
1382
+ const scopes = opts.scope;
1383
+ const unknown = (scopes ?? []).filter(
1384
+ (scope) => !API_SCOPES.includes(scope)
1385
+ );
1386
+ if (unknown.length > 0) {
1387
+ error(
1388
+ `Not a scope: ${unknown.join(", ")}. Allowed values: ${API_SCOPES.join(", ")}`
1389
+ );
1390
+ process.exit(1);
1391
+ }
1337
1392
  try {
1338
- const key = await client.createKey(opts.name);
1393
+ const key = await client.createKey({
1394
+ ...opts.name ? { name: opts.name } : {},
1395
+ ...scopes ? { scopes } : {}
1396
+ });
1339
1397
  if (wantsJson(opts, cmd)) {
1340
1398
  json(key);
1341
1399
  } else {
@@ -1343,6 +1401,10 @@ keysCommand.command("create").description("Create a new API key").option("-n, --
1343
1401
  console.log(`
1344
1402
  ${key.key}
1345
1403
  `);
1404
+ console.log(
1405
+ ` Scopes: ${key.scopes === null ? "all" : key.scopes.join(" ")}
1406
+ `
1407
+ );
1346
1408
  }
1347
1409
  } catch (err) {
1348
1410
  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.48",
3
+ "version": "1.0.49",
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.45",
47
+ "@chirpie/sdk": "^1.0.46",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {