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.
- package/README.md +4 -4
- package/dist/index.js +95 -25
- 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
|
}
|
|
@@ -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 <
|
|
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 ? {
|
|
357
|
-
...
|
|
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((
|
|
469
|
+
return new Promise((resolve2) => {
|
|
407
470
|
rl.on("line", (line) => {
|
|
408
471
|
rl.close();
|
|
409
|
-
|
|
472
|
+
resolve2(line.trim());
|
|
410
473
|
});
|
|
411
474
|
});
|
|
412
475
|
}
|
|
413
|
-
return new Promise((
|
|
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
|
-
|
|
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
|
|
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((
|
|
621
|
+
return new Promise((resolve2) => {
|
|
552
622
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
553
623
|
rl.close();
|
|
554
|
-
|
|
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((
|
|
797
|
+
chatId = await new Promise((resolve2) => {
|
|
728
798
|
process.stderr.write("Chat ID: ");
|
|
729
799
|
rl.question("", (answer) => {
|
|
730
800
|
rl.close();
|
|
731
|
-
|
|
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((
|
|
1009
|
+
return new Promise((resolve2) => {
|
|
940
1010
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
941
1011
|
rl.close();
|
|
942
|
-
|
|
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.
|
|
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.
|
|
47
|
+
"@chirpie/sdk": "^1.0.41",
|
|
48
48
|
"commander": "^13"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|