chirpie 1.0.42 → 1.0.43

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 +78 -23
  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
  }
@@ -335,9 +389,9 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
335
389
  postsCommand.command("update <id>").description(
336
390
  "Edit a post that has not published yet. Leaving --schedule-at out keeps the time it already has"
337
391
  ).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(
392
+ "-m, --media <files...>",
393
+ "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."
394
+ ).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
341
395
  "-s, --schedule-at <datetime>",
342
396
  "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
397
  ).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
@@ -351,10 +405,11 @@ postsCommand.command("update <id>").description(
351
405
  }
352
406
  const client = getClient();
353
407
  try {
408
+ const media = await resolveMediaArgs(client, opts.media, opts.alt);
354
409
  const post = await client.updatePost(id, {
355
410
  ...opts.text !== void 0 ? { text: opts.text } : {},
356
- ...opts.clearMedia ? { media_urls: [] } : {},
357
- ...opts.media ? { media_urls: opts.media } : {},
411
+ ...opts.clearMedia ? { media: [] } : {},
412
+ ...media ? { media } : {},
358
413
  ...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
359
414
  });
360
415
  if (wantsJson(opts, cmd)) {
@@ -403,14 +458,14 @@ async function readSecretFromStdin(prompt) {
403
458
  if (!stdin.isTTY) {
404
459
  const readline = await import("readline");
405
460
  const rl = readline.createInterface({ input: stdin });
406
- return new Promise((resolve) => {
461
+ return new Promise((resolve2) => {
407
462
  rl.on("line", (line) => {
408
463
  rl.close();
409
- resolve(line.trim());
464
+ resolve2(line.trim());
410
465
  });
411
466
  });
412
467
  }
413
- return new Promise((resolve) => {
468
+ return new Promise((resolve2) => {
414
469
  const chars = [];
415
470
  stdin.setRawMode(true);
416
471
  stdin.resume();
@@ -424,7 +479,7 @@ async function readSecretFromStdin(prompt) {
424
479
  stdin.pause();
425
480
  stdin.off("data", onData);
426
481
  process.stderr.write("\n");
427
- resolve(chars.join("").trim());
482
+ resolve2(chars.join("").trim());
428
483
  break;
429
484
  case "":
430
485
  stdin.setRawMode(false);
@@ -548,10 +603,10 @@ async function confirm(question) {
548
603
  input: process.stdin,
549
604
  output: process.stderr
550
605
  });
551
- return new Promise((resolve) => {
606
+ return new Promise((resolve2) => {
552
607
  rl.question(`${question} [y/N] `, (answer) => {
553
608
  rl.close();
554
- resolve(answer.trim().toLowerCase() === "y");
609
+ resolve2(answer.trim().toLowerCase() === "y");
555
610
  });
556
611
  });
557
612
  }
@@ -724,11 +779,11 @@ accountsCommand.command("connect-telegram").description("Connect a Telegram bot
724
779
  if (!chatId) {
725
780
  const readline = await import("readline");
726
781
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
727
- chatId = await new Promise((resolve) => {
782
+ chatId = await new Promise((resolve2) => {
728
783
  process.stderr.write("Chat ID: ");
729
784
  rl.question("", (answer) => {
730
785
  rl.close();
731
- resolve(answer.trim());
786
+ resolve2(answer.trim());
732
787
  });
733
788
  });
734
789
  if (!chatId) {
@@ -936,10 +991,10 @@ async function confirm2(question) {
936
991
  input: process.stdin,
937
992
  output: process.stderr
938
993
  });
939
- return new Promise((resolve) => {
994
+ return new Promise((resolve2) => {
940
995
  rl.question(`${question} [y/N] `, (answer) => {
941
996
  rl.close();
942
- resolve(answer.trim().toLowerCase() === "y");
997
+ resolve2(answer.trim().toLowerCase() === "y");
943
998
  });
944
999
  });
945
1000
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chirpie",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
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.40",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {