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.
- package/README.md +4 -4
- package/dist/index.js +78 -23
- 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
|
|
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((
|
|
48
|
+
apiKey = await new Promise((resolve2) => {
|
|
49
49
|
rl.question("Enter your Chirpie API key: ", (answer) => {
|
|
50
50
|
rl.close();
|
|
51
|
-
|
|
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 <
|
|
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
|
-
|
|
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
|
|
205
|
-
return { index,
|
|
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:
|
|
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].
|
|
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 <
|
|
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 ? {
|
|
357
|
-
...
|
|
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((
|
|
461
|
+
return new Promise((resolve2) => {
|
|
407
462
|
rl.on("line", (line) => {
|
|
408
463
|
rl.close();
|
|
409
|
-
|
|
464
|
+
resolve2(line.trim());
|
|
410
465
|
});
|
|
411
466
|
});
|
|
412
467
|
}
|
|
413
|
-
return new Promise((
|
|
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
|
-
|
|
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((
|
|
606
|
+
return new Promise((resolve2) => {
|
|
552
607
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
553
608
|
rl.close();
|
|
554
|
-
|
|
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((
|
|
782
|
+
chatId = await new Promise((resolve2) => {
|
|
728
783
|
process.stderr.write("Chat ID: ");
|
|
729
784
|
rl.question("", (answer) => {
|
|
730
785
|
rl.close();
|
|
731
|
-
|
|
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((
|
|
994
|
+
return new Promise((resolve2) => {
|
|
940
995
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
941
996
|
rl.close();
|
|
942
|
-
|
|
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.
|
|
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.
|
|
47
|
+
"@chirpie/sdk": "^1.0.40",
|
|
48
48
|
"commander": "^13"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|