chirpie 1.0.44 → 1.0.46
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 +46 -4
- package/dist/index.js +240 -47
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,11 +30,12 @@ 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 files or URLs, `--alt` descriptions, `-s` schedule |
|
|
34
|
-
| `chirpie thread <post> <post> ...` | Create a 2 to 25 post thread
|
|
35
|
-
| `chirpie posts` | List posts. `--status`, `--account`, `--limit` |
|
|
33
|
+
| `chirpie post <text>` | Create a post. `-a` account (repeat it for several accounts), `-m` media files or URLs, `--alt` descriptions, `--config` per-account overrides, `-s` schedule, `--draft` save without sending |
|
|
34
|
+
| `chirpie thread <post> <post> ...` | Create a 2 to 25 post thread, or 1 to 25 with `--draft`. `-a` is repeatable, `--config` takes per-account posts, `-f` takes a JSON payload file |
|
|
35
|
+
| `chirpie posts` | List posts. `--status`, `--account`, `--group`, `--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
|
+
| `chirpie posts update <id>` | Edit a post that has not published yet, or finish a draft. `--text`, `--media`, `--alt`, `--clear-media`, `--schedule-at`, `--keep-draft`. Leaving `--schedule-at` out keeps the time it already has |
|
|
38
|
+
| `chirpie posts publish <id>` | Send a saved draft now. `-t` sets the final text first |
|
|
38
39
|
| `chirpie posts delete <id>` | Delete a post. Deleting any post of a scheduled thread cancels the thread |
|
|
39
40
|
| `chirpie accounts` | List connected accounts, active and inactive |
|
|
40
41
|
| `chirpie accounts activate <id>` / `deactivate <id>` | Choose which accounts publish. Deactivating cancels that account's scheduled posts |
|
|
@@ -70,6 +71,47 @@ chirpie thread "One" "Two" "Three" -a 550e8400-e29b-41d4-a716-446655440000
|
|
|
70
71
|
chirpie posts --status scheduled
|
|
71
72
|
```
|
|
72
73
|
|
|
74
|
+
## Drafts
|
|
75
|
+
|
|
76
|
+
`--draft` saves a post or a thread without sending it. Nothing reaches the platform and nothing counts against your quota until you promote it. Anything that would go wrong is printed back, one warning per line.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
chirpie post "Half an idea" --draft
|
|
80
|
+
chirpie thread "Opening line" --draft # a draft thread may be one post
|
|
81
|
+
chirpie posts --status draft # what is saved
|
|
82
|
+
|
|
83
|
+
# Change only the time it remembers
|
|
84
|
+
chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z --keep-draft
|
|
85
|
+
|
|
86
|
+
# Promote it: queue it, or send it now
|
|
87
|
+
chirpie posts update "$POST_ID" --schedule-at 2027-04-02T09:00:00Z
|
|
88
|
+
chirpie posts publish "$POST_ID"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Promoting runs every rule a normal post runs and takes the quota, so a draft that would be refused stays a draft, unchanged. A draft thread is promoted whole.
|
|
92
|
+
|
|
93
|
+
## Several accounts at once
|
|
94
|
+
|
|
95
|
+
Repeat `-a` to publish the same post to up to 25 accounts in one call.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
chirpie post "Shipped a new release." -a "$X_ACCOUNT" -a "$BSKY_ACCOUNT"
|
|
99
|
+
|
|
100
|
+
# Give one account its own text, inline or from a file
|
|
101
|
+
chirpie post "Shipped a new release." -a "$X_ACCOUNT" -a "$BSKY_ACCOUNT" \
|
|
102
|
+
--config "{\"$BSKY_ACCOUNT\":{\"text\":\"Shorter, for Bluesky\"}}"
|
|
103
|
+
chirpie post "Shipped a new release." -a "$X_ACCOUNT" -a "$BSKY_ACCOUNT" --config ./overrides.json
|
|
104
|
+
|
|
105
|
+
# A thread, with a thread of its own for one account
|
|
106
|
+
chirpie thread "One" "Two" -a "$X_ACCOUNT" -a "$BSKY_ACCOUNT" \
|
|
107
|
+
--config "{\"$BSKY_ACCOUNT\":{\"posts\":[{\"text\":\"A\"},{\"text\":\"B\"}]}}"
|
|
108
|
+
|
|
109
|
+
# Read the whole group back
|
|
110
|
+
chirpie posts --group 550e8400-e29b-41d4-a716-446655440000
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One line is printed per account, with a group ID above them. An account the platform refused is shown with its error while the others stay published, and the command leaves with a non-zero status when any account failed. An override says only what differs, and naming media replaces the shared media for that account.
|
|
114
|
+
|
|
73
115
|
## JSON output
|
|
74
116
|
|
|
75
117
|
Every command accepts `--json` and prints the raw API response, which is what you want in a script:
|
package/dist/index.js
CHANGED
|
@@ -181,34 +181,150 @@ async function resolveMediaArgs(client, media, alt) {
|
|
|
181
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
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
183
|
|
|
184
|
-
// src/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
// src/lib/accounts.ts
|
|
185
|
+
import { readFileSync } from "fs";
|
|
186
|
+
function collect(value, previous = []) {
|
|
187
|
+
return [...previous, value];
|
|
188
|
+
}
|
|
189
|
+
async function resolveSingleAccountId(client) {
|
|
190
|
+
const accounts = await client.listAccounts();
|
|
191
|
+
const active = accounts.filter((a) => a.is_active);
|
|
192
|
+
if (active.length === 0) {
|
|
193
|
+
error("No connected accounts. Run: chirpie accounts connect");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (active.length > 1) {
|
|
197
|
+
error("Multiple accounts found. Use --account <id> to specify:");
|
|
198
|
+
active.forEach((a) => console.log(` ${a.id} @${a.username}`));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
return active[0].id;
|
|
202
|
+
}
|
|
203
|
+
function parseAccountConfigurations(value) {
|
|
204
|
+
if (!value) return void 0;
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
let raw;
|
|
207
|
+
if (trimmed.startsWith("{")) {
|
|
208
|
+
raw = trimmed;
|
|
209
|
+
} else {
|
|
210
|
+
try {
|
|
211
|
+
raw = readFileSync(trimmed, "utf8");
|
|
212
|
+
} catch {
|
|
213
|
+
error(`Could not read --config file: ${trimmed}`);
|
|
193
214
|
process.exit(1);
|
|
194
215
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(raw);
|
|
220
|
+
} catch {
|
|
221
|
+
error("--config must be a JSON object, or a path to a file holding one");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
225
|
+
error("--config must be a JSON object keyed by account ID");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
return parsed;
|
|
229
|
+
}
|
|
230
|
+
function printDraftWarnings(warnings) {
|
|
231
|
+
if (!Array.isArray(warnings)) return;
|
|
232
|
+
for (const warning of warnings) {
|
|
233
|
+
const entry = warning;
|
|
234
|
+
if (typeof entry?.message !== "string") continue;
|
|
235
|
+
info(`${entry.platform} ${entry.account_id}: ${entry.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function printFanOut(groupId, lines) {
|
|
239
|
+
const failed = lines.filter((line) => !line.success);
|
|
240
|
+
console.log(`Group ${groupId}`);
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
if (line.success) {
|
|
243
|
+
const where = line.platform_post_url ? ` ${line.platform_post_url}` : "";
|
|
244
|
+
console.log(
|
|
245
|
+
`\x1B[32m\u2713\x1B[0m ${line.platform} ${line.account_id} ${line.status ?? ""} ${line.id ?? ""}${where}`.trimEnd()
|
|
246
|
+
);
|
|
247
|
+
} else {
|
|
248
|
+
const reason = line.error ? `${line.error.code}: ${line.error.message}` : "Failed";
|
|
249
|
+
console.log(
|
|
250
|
+
`\x1B[31m\u2717\x1B[0m ${line.platform} ${line.account_id} ${reason}`
|
|
251
|
+
);
|
|
199
252
|
}
|
|
200
|
-
accountId = active[0].id;
|
|
201
253
|
}
|
|
254
|
+
console.log(
|
|
255
|
+
`${lines.length - failed.length} of ${lines.length} accounts succeeded`
|
|
256
|
+
);
|
|
257
|
+
return failed.length;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/commands/post.ts
|
|
261
|
+
var postCommand = new Command3("post").description("Create a post").argument("<text>", "Post text").option(
|
|
262
|
+
"-a, --account <id>",
|
|
263
|
+
"Account ID. Repeat it to publish the same post to several accounts at once (up to 25)",
|
|
264
|
+
collect
|
|
265
|
+
).option("-m, --media <files...>", MEDIA_OPTION_HELP).option("--alt <text...>", ALT_OPTION_HELP).option(
|
|
266
|
+
"--config <json-or-file>",
|
|
267
|
+
'Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "text": "...", "media_urls": ["..."] } }. Every key must be an account named with --account'
|
|
268
|
+
).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(
|
|
269
|
+
"--draft",
|
|
270
|
+
"Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
|
|
271
|
+
).option("--json", "Output raw JSON").action(async (text, opts) => {
|
|
272
|
+
const client = getClient();
|
|
273
|
+
const named = opts.account ?? [];
|
|
274
|
+
if (opts.config && named.length === 0) {
|
|
275
|
+
error("--config names accounts, so pass --account <id> for each of them");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
const accountIds = named.length > 0 ? named : [await resolveSingleAccountId(client)];
|
|
279
|
+
const fanOut = named.length > 1 || Boolean(opts.config);
|
|
202
280
|
try {
|
|
203
281
|
const media = await resolveMediaArgs(client, opts.media, opts.alt);
|
|
282
|
+
if (fanOut) {
|
|
283
|
+
const result = await client.createPost({
|
|
284
|
+
account_ids: accountIds,
|
|
285
|
+
text,
|
|
286
|
+
...media ? { media } : {},
|
|
287
|
+
...opts.config ? {
|
|
288
|
+
account_configurations: parseAccountConfigurations(
|
|
289
|
+
opts.config
|
|
290
|
+
)
|
|
291
|
+
} : {},
|
|
292
|
+
schedule_at: opts.schedule,
|
|
293
|
+
...opts.draft ? { draft: true } : {}
|
|
294
|
+
});
|
|
295
|
+
if (opts.json) {
|
|
296
|
+
json(result);
|
|
297
|
+
if (result.results.some((r) => !r.success)) process.exit(1);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if ("warnings" in result) printDraftWarnings(result.warnings);
|
|
301
|
+
const failed = printFanOut(
|
|
302
|
+
result.group_id,
|
|
303
|
+
result.results.map((r) => ({
|
|
304
|
+
account_id: r.account_id,
|
|
305
|
+
platform: r.platform,
|
|
306
|
+
success: r.success,
|
|
307
|
+
id: r.post_id,
|
|
308
|
+
status: r.status,
|
|
309
|
+
platform_post_url: r.platform_post_url,
|
|
310
|
+
error: r.error
|
|
311
|
+
}))
|
|
312
|
+
);
|
|
313
|
+
if (failed > 0) process.exit(1);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
204
316
|
const post = await client.createPost({
|
|
205
|
-
account_id:
|
|
317
|
+
account_id: accountIds[0],
|
|
206
318
|
text,
|
|
207
319
|
...media ? { media } : {},
|
|
208
|
-
schedule_at: opts.schedule
|
|
320
|
+
schedule_at: opts.schedule,
|
|
321
|
+
...opts.draft ? { draft: true } : {}
|
|
209
322
|
});
|
|
210
323
|
if (opts.json) {
|
|
211
324
|
json(post);
|
|
325
|
+
} else if (post.status === "draft") {
|
|
326
|
+
success(`Draft saved (${post.id})`);
|
|
327
|
+
if ("warnings" in post) printDraftWarnings(post.warnings);
|
|
212
328
|
} else if (post.status === "scheduled") {
|
|
213
329
|
success(`Post scheduled for ${post.scheduled_at} (${post.id})`);
|
|
214
330
|
} else {
|
|
@@ -222,7 +338,7 @@ var postCommand = new Command3("post").description("Create a post").argument("<t
|
|
|
222
338
|
|
|
223
339
|
// src/commands/thread.ts
|
|
224
340
|
import { Command as Command4 } from "commander";
|
|
225
|
-
import { readFileSync } from "fs";
|
|
341
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
226
342
|
function parseMediaSpec(spec) {
|
|
227
343
|
const m = spec.match(/^(\d+):(.+)$/);
|
|
228
344
|
if (!m) return null;
|
|
@@ -238,7 +354,11 @@ function parseAltSpec(spec) {
|
|
|
238
354
|
alts: m[2].split(",").map((a) => a.trim())
|
|
239
355
|
};
|
|
240
356
|
}
|
|
241
|
-
var threadCommand = new Command4("thread").description("Create a thread").argument("[posts...]", "Thread posts (each argument is one post). Omit when using --file.").option(
|
|
357
|
+
var threadCommand = new Command4("thread").description("Create a thread").argument("[posts...]", "Thread posts (each argument is one post). Omit when using --file.").option(
|
|
358
|
+
"-a, --account <id>",
|
|
359
|
+
"Account ID. Repeat it to publish the same thread to several accounts at once (up to 25)",
|
|
360
|
+
collect
|
|
361
|
+
).option(
|
|
242
362
|
"-m, --media <specs...>",
|
|
243
363
|
'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
364
|
).option(
|
|
@@ -246,39 +366,39 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
246
366
|
'Per-post alt text as "INDEX:first,second", lining up with --media. Carried to X, Bluesky, LinkedIn, Mastodon, Instagram and Facebook.'
|
|
247
367
|
).option(
|
|
248
368
|
"-f, --file <path>",
|
|
249
|
-
"JSON file with a full thread payload: { posts: [{ text, media_urls? }, ...], schedule_at?: string }"
|
|
250
|
-
).option(
|
|
369
|
+
"JSON file with a full thread payload: { posts: [{ text, media_urls? }, ...], schedule_at?: string }. It may also carry account_ids and account_configurations."
|
|
370
|
+
).option(
|
|
371
|
+
"--config <json-or-file>",
|
|
372
|
+
`Per-account overrides as JSON, or a path to a JSON file holding them: { "<account id>": { "posts": [{ "text": "one" }, { "text": "two" }] } }. An account's posts replace the whole thread for that account`
|
|
373
|
+
).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). Threads publish within ~5 minutes of the scheduled time.").option(
|
|
374
|
+
"--draft",
|
|
375
|
+
"Save without publishing. Nothing is sent, nothing counts against your quota, and any problems are listed back"
|
|
376
|
+
).option("--json", "Output raw JSON").action(async (posts, opts) => {
|
|
251
377
|
const client = getClient();
|
|
252
|
-
let accountId = opts.account;
|
|
253
|
-
if (!accountId) {
|
|
254
|
-
const accounts = await client.listAccounts();
|
|
255
|
-
const active = accounts.filter((a) => a.is_active);
|
|
256
|
-
if (active.length === 0) {
|
|
257
|
-
error("No connected accounts. Run: chirpie accounts connect");
|
|
258
|
-
process.exit(1);
|
|
259
|
-
}
|
|
260
|
-
if (active.length > 1) {
|
|
261
|
-
error("Multiple accounts found. Use --account <id> to specify:");
|
|
262
|
-
active.forEach((a) => console.log(` ${a.id} @${a.username}`));
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
accountId = active[0].id;
|
|
266
|
-
}
|
|
267
378
|
let postPayload;
|
|
268
379
|
let scheduleAt = opts.schedule;
|
|
380
|
+
let fileAccountIds;
|
|
381
|
+
let fileConfigurations;
|
|
269
382
|
if (opts.file) {
|
|
270
383
|
try {
|
|
271
|
-
const raw = JSON.parse(
|
|
384
|
+
const raw = JSON.parse(readFileSync2(opts.file, "utf8"));
|
|
272
385
|
if (!Array.isArray(raw.posts)) throw new Error("File must contain { posts: [...] }");
|
|
273
386
|
postPayload = raw.posts;
|
|
274
387
|
scheduleAt = scheduleAt ?? raw.schedule_at;
|
|
388
|
+
if (raw.account_ids !== void 0 && (!Array.isArray(raw.account_ids) || raw.account_ids.some((id) => typeof id !== "string"))) {
|
|
389
|
+
throw new Error("account_ids in the file must be an array of account IDs");
|
|
390
|
+
}
|
|
391
|
+
fileAccountIds = raw.account_ids;
|
|
392
|
+
fileConfigurations = raw.account_configurations;
|
|
275
393
|
} catch (err) {
|
|
276
394
|
error(err instanceof Error ? err.message : "Failed to read thread file");
|
|
277
395
|
process.exit(1);
|
|
278
396
|
}
|
|
279
397
|
} else {
|
|
280
|
-
if (posts.length < 2) {
|
|
281
|
-
error(
|
|
398
|
+
if (posts.length < (opts.draft ? 1 : 2)) {
|
|
399
|
+
error(
|
|
400
|
+
opts.draft ? "Pass at least one post (or use --file)" : "Thread must have at least 2 posts (or use --file)"
|
|
401
|
+
);
|
|
282
402
|
process.exit(1);
|
|
283
403
|
}
|
|
284
404
|
postPayload = posts.map((text) => ({ text }));
|
|
@@ -314,14 +434,55 @@ var threadCommand = new Command4("thread").description("Create a thread").argume
|
|
|
314
434
|
}
|
|
315
435
|
}
|
|
316
436
|
}
|
|
437
|
+
const configurations = parseAccountConfigurations(opts.config) ?? fileConfigurations;
|
|
438
|
+
const named = opts.account ?? fileAccountIds ?? [];
|
|
439
|
+
if (configurations && named.length === 0) {
|
|
440
|
+
error("Per-account posts name accounts, so pass --account <id> for each of them");
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const accountIds = named.length > 0 ? named : [await resolveSingleAccountId(client)];
|
|
444
|
+
const fanOut = named.length > 1 || Boolean(configurations);
|
|
317
445
|
try {
|
|
446
|
+
if (fanOut) {
|
|
447
|
+
const result = await client.createThread({
|
|
448
|
+
account_ids: accountIds,
|
|
449
|
+
posts: postPayload,
|
|
450
|
+
...configurations ? { account_configurations: configurations } : {},
|
|
451
|
+
schedule_at: scheduleAt,
|
|
452
|
+
...opts.draft ? { draft: true } : {}
|
|
453
|
+
});
|
|
454
|
+
if (opts.json) {
|
|
455
|
+
json(result);
|
|
456
|
+
if (result.results.some((r) => !r.success)) process.exit(1);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if ("warnings" in result) printDraftWarnings(result.warnings);
|
|
460
|
+
const failed = printFanOut(
|
|
461
|
+
result.group_id,
|
|
462
|
+
result.results.map((r) => ({
|
|
463
|
+
account_id: r.account_id,
|
|
464
|
+
platform: r.platform,
|
|
465
|
+
success: r.success,
|
|
466
|
+
id: r.thread_id,
|
|
467
|
+
status: r.status,
|
|
468
|
+
platform_post_url: r.platform_post_url,
|
|
469
|
+
error: r.error
|
|
470
|
+
}))
|
|
471
|
+
);
|
|
472
|
+
if (failed > 0) process.exit(1);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
318
475
|
const thread = await client.createThread({
|
|
319
|
-
account_id:
|
|
476
|
+
account_id: accountIds[0],
|
|
320
477
|
posts: postPayload,
|
|
321
|
-
schedule_at: scheduleAt
|
|
478
|
+
schedule_at: scheduleAt,
|
|
479
|
+
...opts.draft ? { draft: true } : {}
|
|
322
480
|
});
|
|
323
481
|
if (opts.json) {
|
|
324
482
|
json(thread);
|
|
483
|
+
} else if (thread.status === "draft") {
|
|
484
|
+
success(`Draft thread saved with ${thread.posts.length} posts (${thread.id})`);
|
|
485
|
+
if ("warnings" in thread) printDraftWarnings(thread.warnings);
|
|
325
486
|
} else {
|
|
326
487
|
success(`Thread created with ${thread.posts.length} posts (${thread.id})`);
|
|
327
488
|
}
|
|
@@ -342,12 +503,19 @@ function wantsJson(opts, cmd) {
|
|
|
342
503
|
}
|
|
343
504
|
|
|
344
505
|
// src/commands/posts.ts
|
|
345
|
-
var postsCommand = new Command5("posts").description("List, get, update, or delete posts").option(
|
|
506
|
+
var postsCommand = new Command5("posts").description("List, get, update, or delete posts").option(
|
|
507
|
+
"--status <status>",
|
|
508
|
+
"Filter by status: draft, scheduled, publishing, published, failed, deleted"
|
|
509
|
+
).option("--account <id>", "Filter by account").option(
|
|
510
|
+
"--group <id>",
|
|
511
|
+
"Only the posts of one fan-out, from the group ID a multi-account post returned"
|
|
512
|
+
).option("--limit <n>", "Limit results", "20").option("--json", "Output raw JSON").action(async (opts, cmd) => {
|
|
346
513
|
const client = getClient();
|
|
347
514
|
try {
|
|
348
515
|
const posts = await client.listPosts({
|
|
349
516
|
status: opts.status,
|
|
350
517
|
account_id: opts.account,
|
|
518
|
+
group_id: opts.group,
|
|
351
519
|
limit: parseInt(opts.limit)
|
|
352
520
|
});
|
|
353
521
|
if (wantsJson(opts, cmd)) {
|
|
@@ -395,20 +563,25 @@ postsCommand.command("get <id>").description("Get a post by ID").option("--json"
|
|
|
395
563
|
}
|
|
396
564
|
});
|
|
397
565
|
postsCommand.command("update <id>").description(
|
|
398
|
-
"Edit a post that has not published yet. Leaving --schedule-at out keeps the time
|
|
566
|
+
"Edit a post that has not published yet, or finish a draft. Leaving --schedule-at out keeps the time a queued post already has"
|
|
399
567
|
).option("-t, --text <text>", "Replacement post text").option(
|
|
400
568
|
"-m, --media <files...>",
|
|
401
569
|
"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
570
|
).option("--alt <text...>", ALT_OPTION_HELP).option("--clear-media", "Remove the media from the post").option(
|
|
403
571
|
"-s, --schedule-at <datetime>",
|
|
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."
|
|
572
|
+
"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. On a draft this queues it, unless you also pass --keep-draft."
|
|
573
|
+
).option(
|
|
574
|
+
"--keep-draft",
|
|
575
|
+
"Keep a draft a draft, so --schedule-at only changes the time it remembers"
|
|
405
576
|
).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
|
406
577
|
if (opts.media && opts.clearMedia) {
|
|
407
578
|
error("Pass either --media or --clear-media, not both");
|
|
408
579
|
process.exit(1);
|
|
409
580
|
}
|
|
410
|
-
if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt) {
|
|
411
|
-
error(
|
|
581
|
+
if (!opts.text && !opts.media && !opts.clearMedia && !opts.scheduleAt && !opts.keepDraft) {
|
|
582
|
+
error(
|
|
583
|
+
"Nothing to change. Pass --text, --media, --clear-media, --schedule-at or --keep-draft"
|
|
584
|
+
);
|
|
412
585
|
process.exit(1);
|
|
413
586
|
}
|
|
414
587
|
const client = getClient();
|
|
@@ -418,24 +591,44 @@ postsCommand.command("update <id>").description(
|
|
|
418
591
|
...opts.text !== void 0 ? { text: opts.text } : {},
|
|
419
592
|
...opts.clearMedia ? { media: [] } : {},
|
|
420
593
|
...media ? { media } : {},
|
|
421
|
-
...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {}
|
|
594
|
+
...opts.scheduleAt ? { schedule_at: opts.scheduleAt } : {},
|
|
595
|
+
...opts.keepDraft ? { draft: true } : {}
|
|
422
596
|
});
|
|
423
597
|
if (wantsJson(opts, cmd)) {
|
|
424
598
|
json(post);
|
|
425
599
|
} else {
|
|
426
600
|
const moved = post.rescheduled_post_ids ?? [];
|
|
427
|
-
|
|
601
|
+
const promoted = post.promoted_from_draft_id ?? post.promoted_from_draft_ids?.[0];
|
|
602
|
+
success(promoted ? `Draft ${promoted} is now ${post.status}` : `Post ${post.id} updated`);
|
|
428
603
|
if (opts.scheduleAt) {
|
|
429
604
|
success(
|
|
430
605
|
moved.length > 1 ? `Thread rescheduled for ${post.scheduled_at} (${moved.length} posts)` : `Scheduled for ${post.scheduled_at}`
|
|
431
606
|
);
|
|
432
607
|
}
|
|
608
|
+
if (post.warnings?.length) printDraftWarnings(post.warnings);
|
|
433
609
|
}
|
|
434
610
|
} catch (err) {
|
|
435
611
|
apiError(err, "Failed to update post");
|
|
436
612
|
process.exit(1);
|
|
437
613
|
}
|
|
438
614
|
});
|
|
615
|
+
postsCommand.command("publish <id>").description("Publish a draft now. Every rule and the monthly quota apply").option("-t, --text <text>", "Final post text, saved before publishing").option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
|
616
|
+
const client = getClient();
|
|
617
|
+
try {
|
|
618
|
+
const post = await client.updatePost(id, {
|
|
619
|
+
publish: true,
|
|
620
|
+
...opts.text !== void 0 ? { text: opts.text } : {}
|
|
621
|
+
});
|
|
622
|
+
if (wantsJson(opts, cmd)) {
|
|
623
|
+
json(post);
|
|
624
|
+
} else {
|
|
625
|
+
success(`Draft ${id} published (${post.id})`);
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
apiError(err, "Failed to publish draft");
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
439
632
|
postsCommand.command("delete <id>").description(
|
|
440
633
|
"Delete a post (deleting any post of a scheduled thread cancels the thread)"
|
|
441
634
|
).option("--json", "Output raw JSON").action(async (id, opts, cmd) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chirpie",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
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.43",
|
|
48
48
|
"commander": "^13"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|