chirpie 1.0.42 → 1.0.44

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 +4 -4
  2. package/dist/index.js +95 -25
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,11 +30,11 @@ 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, `-m` media URLs, `-s` schedule |
33
+ | `chirpie post <text>` | Create a post. `-a` account, `-m` media files or URLs, `--alt` descriptions, `-s` schedule |
34
34
  | `chirpie thread <post> <post> ...` | Create a 2 to 25 post thread. `-f` takes a JSON payload file |
35
35
  | `chirpie posts` | List posts. `--status`, `--account`, `--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`, `--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. `--text`, `--media`, `--alt`, `--clear-media`, `--schedule-at`. Leaving `--schedule-at` out keeps the time it already has |
38
38
  | `chirpie posts delete <id>` | Delete a post. Deleting any post of a scheduled thread cancels the thread |
39
39
  | `chirpie accounts` | List connected accounts, active and inactive |
40
40
  | `chirpie accounts activate <id>` / `deactivate <id>` | Choose which accounts publish. Deactivating cancels that account's scheduled posts |
@@ -57,8 +57,8 @@ Run `chirpie <command> --help` for the full option list. Threads, Instagram, Fac
57
57
  # Post now, picking the account interactively
58
58
  chirpie post "Shipped a new release."
59
59
 
60
- # Post with media to a specific account
61
- chirpie post "Look at this" -a 550e8400-e29b-41d4-a716-446655440000 -m https://example.com/a.png
60
+ # Post with media to a specific account. A local file is uploaded first.
61
+ chirpie post "Look at this" -a 550e8400-e29b-41d4-a716-446655440000 -m ./shot.png --alt "The new dashboard"
62
62
 
63
63
  # Schedule. Any ISO 8601 timestamp that carries a timezone.
64
64
  chirpie post "Later" -s 2026-04-01T14:00:00Z
package/dist/index.js CHANGED
@@ -45,10 +45,10 @@ var authCommand = new Command("auth").description("Configure your Chirpie API ke
45
45
  let apiKey = opts.key;
46
46
  if (!apiKey) {
47
47
  const rl = createInterface({ input: process.stdin, output: process.stdout });
48
- apiKey = await new Promise((resolve) => {
48
+ apiKey = await new Promise((resolve2) => {
49
49
  rl.question("Enter your Chirpie API key: ", (answer) => {
50
50
  rl.close();
51
- resolve(answer.trim());
51
+ resolve2(answer.trim());
52
52
  });
53
53
  });
54
54
  }
@@ -156,8 +156,33 @@ function getClient() {
156
156
  });
157
157
  }
158
158
 
159
+ // src/lib/media.ts
160
+ import { readFile } from "fs/promises";
161
+ import { basename, resolve } from "path";
162
+ async function resolveMediaArgs(client, media, alt) {
163
+ if (!media?.length) return void 0;
164
+ const items = [];
165
+ for (const [index, value] of media.entries()) {
166
+ const description = alt?.[index];
167
+ if (/^https?:\/\//i.test(value)) {
168
+ items.push({ url: value, ...description ? { alt: description } : {} });
169
+ continue;
170
+ }
171
+ const path = resolve(value);
172
+ const bytes = await readFile(path);
173
+ const uploaded = await client.uploadMedia({
174
+ data: new Uint8Array(bytes),
175
+ filename: basename(path)
176
+ });
177
+ items.push({ id: uploaded.id, ...description ? { alt: description } : {} });
178
+ }
179
+ return items;
180
+ }
181
+ var MEDIA_OPTION_HELP = "Image or video files on this machine, or public URLs. Files are uploaded first. Image caps: X, Bluesky, LinkedIn and Mastodon 4, Threads 1, Instagram, Facebook and Telegram 10. Video: X, Mastodon and Telegram only, one per post, never with images. Anything a platform cannot take is refused rather than dropped.";
182
+ var ALT_OPTION_HELP = "Describe each item for people using a screen reader, in the same order as --media. Carried to X, Bluesky, LinkedIn, Mastodon, Instagram and Facebook.";
183
+
159
184
  // src/commands/post.ts
160
- var postCommand = new Command3("post").description("Create a post").argument("<text>", "Post text").option("-a, --account <id>", "Account ID").option("-m, --media <urls...>", "Image or video URLs. Image caps: X, Bluesky, LinkedIn and Mastodon 4, Threads 1, Instagram, Facebook and Telegram 10. Video: X, Mastodon and Telegram only, one per post, never with images. Anything a platform cannot take is refused rather than dropped.").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) => {
185
+ var postCommand = new Command3("post").description("Create a post").argument("<text>", "Post text").option("-a, --account <id>", "Account ID").option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--alt <text...>", ALT_OPTION_HELP).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) => {
161
186
  const client = getClient();
162
187
  let accountId = opts.account;
163
188
  if (!accountId) {
@@ -175,10 +200,11 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
175
200
  accountId = active[0].id;
176
201
  }
177
202
  try {
203
+ const media = await resolveMediaArgs(client, opts.media, opts.alt);
178
204
  const post = await client.createPost({
179
205
  account_id: accountId,
180
206
  text,
181
- media_urls: opts.media,
207
+ ...media ? { media } : {},
182
208
  schedule_at: opts.schedule
183
209
  });
184
210
  if (opts.json) {
@@ -201,12 +227,23 @@ function parseMediaSpec(spec) {
201
227
  const m = spec.match(/^(\d+):(.+)$/);
202
228
  if (!m) return null;
203
229
  const index = parseInt(m[1], 10);
204
- const urls = m[2].split(",").map((u) => u.trim()).filter(Boolean);
205
- return { index, urls };
230
+ const files = m[2].split(",").map((u) => u.trim()).filter(Boolean);
231
+ return { index, files };
232
+ }
233
+ function parseAltSpec(spec) {
234
+ const m = spec.match(/^(\d+):(.+)$/);
235
+ if (!m) return null;
236
+ return {
237
+ index: parseInt(m[1], 10),
238
+ alts: m[2].split(",").map((a) => a.trim())
239
+ };
206
240
  }
207
241
  var threadCommand = new Command4("thread").description("Create a thread").argument("[posts...]", "Thread posts (each argument is one post). Omit when using --file.").option("-a, --account <id>", "Account ID").option(
208
242
  "-m, --media <specs...>",
209
- 'Per-post media as "INDEX:url1,url2" (repeatable). Example: -m 0:https://a.png 2:https://b.png,https://c.png'
243
+ 'Per-post media as "INDEX:file1,file2" (repeatable). Files on this machine are uploaded first; public URLs are used as they are. Example: -m 0:./shot.png 2:https://b.png,https://c.png'
244
+ ).option(
245
+ "--alt <specs...>",
246
+ 'Per-post alt text as "INDEX:first,second", lining up with --media. Carried to X, Bluesky, LinkedIn, Mastodon, Instagram and Facebook.'
210
247
  ).option(
211
248
  "-f, --file <path>",
212
249
  "JSON file with a full thread payload: { posts: [{ text, media_urls? }, ...], schedule_at?: string }"
@@ -245,6 +282,19 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
245
282
  process.exit(1);
246
283
  }
247
284
  postPayload = posts.map((text) => ({ text }));
285
+ if (opts.alt && !opts.media) {
286
+ error("--alt describes the items in --media, so pass --media too");
287
+ process.exit(1);
288
+ }
289
+ const altByIndex = /* @__PURE__ */ new Map();
290
+ for (const spec of opts.alt ?? []) {
291
+ const parsed = parseAltSpec(spec);
292
+ if (!parsed) {
293
+ error(`Invalid --alt spec: "${spec}". Expected "INDEX:first,second".`);
294
+ process.exit(1);
295
+ }
296
+ altByIndex.set(parsed.index, parsed.alts);
297
+ }
248
298
  if (opts.media) {
249
299
  for (const spec of opts.media) {
250
300
  const parsed = parseMediaSpec(spec);
@@ -256,7 +306,11 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
256
306
  error(`--media index ${parsed.index} out of range (thread has ${postPayload.length} posts)`);
257
307
  process.exit(1);
258
308
  }
259
- postPayload[parsed.index].media_urls = parsed.urls;
309
+ postPayload[parsed.index].media = await resolveMediaArgs(
310
+ client,
311
+ parsed.files,
312
+ altByIndex.get(parsed.index)
313
+ );
260
314
  }
261
315
  }
262
316
  }
@@ -279,6 +333,7 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
279
333
 
280
334
  // src/commands/posts.ts
281
335
  import { Command as Command5 } from "commander";
336
+ import { postFailureReason } from "@chirpie/sdk";
282
337
 
283
338
  // src/lib/flags.ts
284
339
  function wantsJson(opts, cmd) {
@@ -303,7 +358,10 @@ var postsCommand = new Command5("posts").description("List, get, update, or dele
303
358
  id: p.id.slice(0, 8),
304
359
  status: p.status,
305
360
  text: p.text.length > 50 ? p.text.slice(0, 50) + "..." : p.text,
306
- created: p.created_at.slice(0, 10)
361
+ created: p.created_at.slice(0, 10),
362
+ // Why a post is not where you expected it. Empty for the ones
363
+ // that are exactly where you left them.
364
+ issue: postFailureReason(p) ?? ""
307
365
  }))
308
366
  );
309
367
  }
@@ -326,6 +384,10 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
326
384
  if (post.scheduled_at) console.log(`Scheduled: ${post.scheduled_at}`);
327
385
  if (post.published_at) console.log(`Published: ${post.published_at}`);
328
386
  if (post.platform_post_id) console.log(`Platform Post ID: ${post.platform_post_id}`);
387
+ if (post.platform_post_url) console.log(`URL: ${post.platform_post_url}`);
388
+ if (post.retry_count > 0) console.log(`Attempts: ${post.retry_count}`);
389
+ const issue = postFailureReason(post);
390
+ if (issue) console.log(`Issue: ${issue}`);
329
391
  }
330
392
  } catch (err) {
331
393
  error(err instanceof Error ? err.message : "Failed to get post");
@@ -335,9 +397,9 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
335
397
  postsCommand.command("update <id>").description(
336
398
  "Edit a post that has not published yet. Leaving --schedule-at out keeps the time it already has"
337
399
  ).option("-t, --text <text>", "Replacement post text").option(
338
- "-m, --media <urls...>",
339
- "Replacement image or video URLs, following the same per-platform rules as creating a post. Pass --clear-media to remove the media instead."
340
- ).option("--clear-media", "Remove the media from the post").option(
400
+ "-m, --media <files...>",
401
+ "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."
402
+ ).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
341
403
  "-s, --schedule-at <datetime>",
342
404
  "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."
343
405
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
@@ -351,10 +413,11 @@ postsCommand.command("update <id>").description(
351
413
  }
352
414
  const client = getClient();
353
415
  try {
416
+ const media = await resolveMediaArgs(client, opts.media, opts.alt);
354
417
  const post = await client.updatePost(id, {
355
418
  ...opts.text !== void 0 ? { text: opts.text } : {},
356
- ...opts.clearMedia ? { media_urls: [] } : {},
357
- ...opts.media ? { media_urls: opts.media } : {},
419
+ ...opts.clearMedia ? { media: [] } : {},
420
+ ...media ? { media } : {},
358
421
  ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
359
422
  });
360
423
  if (wantsJson(opts, cmd)) {
@@ -403,14 +466,14 @@ async function readSecretFromStdin(prompt) {
403
466
  if (!stdin.isTTY) {
404
467
  const readline = await import("readline");
405
468
  const rl = readline.createInterface({ input: stdin });
406
- return new Promise((resolve) => {
469
+ return new Promise((resolve2) => {
407
470
  rl.on("line", (line) => {
408
471
  rl.close();
409
- resolve(line.trim());
472
+ resolve2(line.trim());
410
473
  });
411
474
  });
412
475
  }
413
- return new Promise((resolve) => {
476
+ return new Promise((resolve2) => {
414
477
  const chars = [];
415
478
  stdin.setRawMode(true);
416
479
  stdin.resume();
@@ -424,7 +487,7 @@ async function readSecretFromStdin(prompt) {
424
487
  stdin.pause();
425
488
  stdin.off("data", onData);
426
489
  process.stderr.write("\n");
427
- resolve(chars.join("").trim());
490
+ resolve2(chars.join("").trim());
428
491
  break;
429
492
  case "":
430
493
  stdin.setRawMode(false);
@@ -470,7 +533,7 @@ var accountsCommand = new Command6("accounts").description("List connected socia
470
533
  // Inactive accounts are listed too, so a Facebook Page that was
471
534
  // connected past the plan's account limit is visible and can be
472
535
  // switched on with `chirpie accounts activate <id>`.
473
- note: a.is_active ? "" : a.inactive_reason === "plan_limit" ? "over plan limit" : ""
536
+ note: a.is_active ? "" : inactiveNote(a.inactive_reason)
474
537
  }))
475
538
  );
476
539
  }
@@ -479,6 +542,13 @@ var accountsCommand = new Command6("accounts").description("List connected socia
479
542
  process.exit(1);
480
543
  }
481
544
  });
545
+ function inactiveNote(reason) {
546
+ if (reason === "plan_limit") return "over plan limit";
547
+ if (reason === "token_revoked" || reason === "token_expired" || reason === "reauth_required") {
548
+ return "needs reconnecting";
549
+ }
550
+ return "";
551
+ }
482
552
  accountsCommand.command("activate").argument("<id>", "Account ID (from `chirpie accounts`)").description("Activate a connected account so it can publish").option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
483
553
  const client = getClient();
484
554
  try {
@@ -548,10 +618,10 @@ async function confirm(question) {
548
618
  input: process.stdin,
549
619
  output: process.stderr
550
620
  });
551
- return new Promise((resolve) => {
621
+ return new Promise((resolve2) => {
552
622
  rl.question(`${question} [y/N] `, (answer) => {
553
623
  rl.close();
554
- resolve(answer.trim().toLowerCase() === "y");
624
+ resolve2(answer.trim().toLowerCase() === "y");
555
625
  });
556
626
  });
557
627
  }
@@ -724,11 +794,11 @@ accountsCommand.command("connect-telegram").description("Connect a Telegram bot
724
794
  if (!chatId) {
725
795
  const readline = await import("readline");
726
796
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
727
- chatId = await new Promise((resolve) => {
797
+ chatId = await new Promise((resolve2) => {
728
798
  process.stderr.write("Chat ID: ");
729
799
  rl.question("", (answer) => {
730
800
  rl.close();
731
- resolve(answer.trim());
801
+ resolve2(answer.trim());
732
802
  });
733
803
  });
734
804
  if (!chatId) {
@@ -936,10 +1006,10 @@ async function confirm2(question) {
936
1006
  input: process.stdin,
937
1007
  output: process.stderr
938
1008
  });
939
- return new Promise((resolve) => {
1009
+ return new Promise((resolve2) => {
940
1010
  rl.question(`${question} [y/N] `, (answer) => {
941
1011
  rl.close();
942
- resolve(answer.trim().toLowerCase() === "y");
1012
+ resolve2(answer.trim().toLowerCase() === "y");
943
1013
  });
944
1014
  });
945
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chirpie",
3
- "version": "1.0.42",
3
+ "version": "1.0.44",
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.39",
47
+ "@chirpie/sdk": "^1.0.41",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {