chirpie 1.0.41 → 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 -3
- package/dist/index.js +115 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,10 +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`, `--alt`, `--clear-media`, `--schedule-at`. Leaving `--schedule-at` out keeps the time it already has |
|
|
37
38
|
| `chirpie posts delete <id>` | Delete a post. Deleting any post of a scheduled thread cancels the thread |
|
|
38
39
|
| `chirpie accounts` | List connected accounts, active and inactive |
|
|
39
40
|
| `chirpie accounts activate <id>` / `deactivate <id>` | Choose which accounts publish. Deactivating cancels that account's scheduled posts |
|
|
@@ -56,8 +57,8 @@ Run `chirpie <command> --help` for the full option list. Threads, Instagram, Fac
|
|
|
56
57
|
# Post now, picking the account interactively
|
|
57
58
|
chirpie post "Shipped a new release."
|
|
58
59
|
|
|
59
|
-
# Post with media to a specific account
|
|
60
|
-
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"
|
|
61
62
|
|
|
62
63
|
# Schedule. Any ISO 8601 timestamp that carries a timezone.
|
|
63
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
|
}
|
|
@@ -287,7 +341,7 @@ function wantsJson(opts, cmd) {
|
|
|
287
341
|
}
|
|
288
342
|
|
|
289
343
|
// src/commands/posts.ts
|
|
290
|
-
var postsCommand = new Command5("posts").description("List, get, or delete posts").option("--status <status>", "Filter by status").option("--account <id>", "Filter by account").option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts, cmd) => {
|
|
344
|
+
var postsCommand = new Command5("posts").description("List, get, update, or delete posts").option("--status <status>", "Filter by status").option("--account <id>", "Filter by account").option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts, cmd) => {
|
|
291
345
|
const client = getClient();
|
|
292
346
|
try {
|
|
293
347
|
const posts = await client.listPosts({
|
|
@@ -332,6 +386,48 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
|
|
|
332
386
|
process.exit(1);
|
|
333
387
|
}
|
|
334
388
|
});
|
|
389
|
+
postsCommand.command("update <id>").description(
|
|
390
|
+
"Edit a post that has not published yet. Leaving --schedule-at out keeps the time it already has"
|
|
391
|
+
).option("-t, --text <text>", "Replacement post text").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(
|
|
395
|
+
"-s, --schedule-at <datetime>",
|
|
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."
|
|
397
|
+
).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
|
398
|
+
if (opts.media && opts.clearMedia) {
|
|
399
|
+
error("Pass either --media or --clear-media, not both");
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt) {
|
|
403
|
+
error("Nothing to change. Pass --text, --media, --clear-media or --schedule-at");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const client = getClient();
|
|
407
|
+
try {
|
|
408
|
+
const media = await resolveMediaArgs(client, opts.media, opts.alt);
|
|
409
|
+
const post = await client.updatePost(id, {
|
|
410
|
+
...opts.text !== void 0 ? { text: opts.text } : {},
|
|
411
|
+
...opts.clearMedia ? { media: [] } : {},
|
|
412
|
+
...media ? { media } : {},
|
|
413
|
+
...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
|
|
414
|
+
});
|
|
415
|
+
if (wantsJson(opts, cmd)) {
|
|
416
|
+
json(post);
|
|
417
|
+
} else {
|
|
418
|
+
const moved = post.rescheduled_post_ids ?? [];
|
|
419
|
+
success(`Post ${post.id} updated`);
|
|
420
|
+
if (opts.scheduleAt) {
|
|
421
|
+
success(
|
|
422
|
+
moved.length > 1 ? `Thread rescheduled for ${post.scheduled_at} (${moved.length} posts)` : `Scheduled for ${post.scheduled_at}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
apiError(err, "Failed to update post");
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
335
431
|
postsCommand.command("delete <id>").description(
|
|
336
432
|
"Delete a post (deleting any post of a scheduled thread cancels the thread)"
|
|
337
433
|
).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
|
@@ -362,14 +458,14 @@ async function readSecretFromStdin(prompt) {
|
|
|
362
458
|
if (!stdin.isTTY) {
|
|
363
459
|
const readline = await import("readline");
|
|
364
460
|
const rl = readline.createInterface({ input: stdin });
|
|
365
|
-
return new Promise((
|
|
461
|
+
return new Promise((resolve2) => {
|
|
366
462
|
rl.on("line", (line) => {
|
|
367
463
|
rl.close();
|
|
368
|
-
|
|
464
|
+
resolve2(line.trim());
|
|
369
465
|
});
|
|
370
466
|
});
|
|
371
467
|
}
|
|
372
|
-
return new Promise((
|
|
468
|
+
return new Promise((resolve2) => {
|
|
373
469
|
const chars = [];
|
|
374
470
|
stdin.setRawMode(true);
|
|
375
471
|
stdin.resume();
|
|
@@ -383,7 +479,7 @@ async function readSecretFromStdin(prompt) {
|
|
|
383
479
|
stdin.pause();
|
|
384
480
|
stdin.off("data", onData);
|
|
385
481
|
process.stderr.write("\n");
|
|
386
|
-
|
|
482
|
+
resolve2(chars.join("").trim());
|
|
387
483
|
break;
|
|
388
484
|
case "":
|
|
389
485
|
stdin.setRawMode(false);
|
|
@@ -507,10 +603,10 @@ async function confirm(question) {
|
|
|
507
603
|
input: process.stdin,
|
|
508
604
|
output: process.stderr
|
|
509
605
|
});
|
|
510
|
-
return new Promise((
|
|
606
|
+
return new Promise((resolve2) => {
|
|
511
607
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
512
608
|
rl.close();
|
|
513
|
-
|
|
609
|
+
resolve2(answer.trim().toLowerCase() === "y");
|
|
514
610
|
});
|
|
515
611
|
});
|
|
516
612
|
}
|
|
@@ -683,11 +779,11 @@ accountsCommand.command("connect-telegram").description("Connect a Telegram bot
|
|
|
683
779
|
if (!chatId) {
|
|
684
780
|
const readline = await import("readline");
|
|
685
781
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
686
|
-
chatId = await new Promise((
|
|
782
|
+
chatId = await new Promise((resolve2) => {
|
|
687
783
|
process.stderr.write("Chat ID: ");
|
|
688
784
|
rl.question("", (answer) => {
|
|
689
785
|
rl.close();
|
|
690
|
-
|
|
786
|
+
resolve2(answer.trim());
|
|
691
787
|
});
|
|
692
788
|
});
|
|
693
789
|
if (!chatId) {
|
|
@@ -895,10 +991,10 @@ async function confirm2(question) {
|
|
|
895
991
|
input: process.stdin,
|
|
896
992
|
output: process.stderr
|
|
897
993
|
});
|
|
898
|
-
return new Promise((
|
|
994
|
+
return new Promise((resolve2) => {
|
|
899
995
|
rl.question(`${question} [y/N] `, (answer) => {
|
|
900
996
|
rl.close();
|
|
901
|
-
|
|
997
|
+
resolve2(answer.trim().toLowerCase() === "y");
|
|
902
998
|
});
|
|
903
999
|
});
|
|
904
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": {
|