typefully 0.1.1 → 0.2.0

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/skills/spec.md ADDED
@@ -0,0 +1,898 @@
1
+ # Typefully CLI — Specification
2
+
3
+ > **This document is the authoritative contract for the Typefully CLI.**
4
+ > All commands, options, arguments, output shapes, and behaviors described here must be satisfied by any conforming implementation.
5
+
6
+ Last updated: 2026-02-18
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Invocation](#invocation)
13
+ 2. [Global Flags](#global-flags)
14
+ 3. [Output Modes](#output-modes)
15
+ 4. [Configuration & Authentication](#configuration--authentication)
16
+ 5. [Exit Codes](#exit-codes)
17
+ 6. [Commands](#commands)
18
+ - [setup](#setup)
19
+ - [me](#me)
20
+ - [social-sets list](#social-sets-list)
21
+ - [social-sets get](#social-sets-get)
22
+ - [drafts list](#drafts-list)
23
+ - [drafts get](#drafts-get)
24
+ - [drafts create](#drafts-create)
25
+ - [drafts update](#drafts-update)
26
+ - [drafts delete](#drafts-delete)
27
+ - [drafts schedule](#drafts-schedule)
28
+ - [drafts publish](#drafts-publish)
29
+ - [create-draft](#create-draft-alias)
30
+ - [update-draft](#update-draft-alias)
31
+ - [tags list](#tags-list)
32
+ - [tags create](#tags-create)
33
+ - [media upload](#media-upload)
34
+ - [media status](#media-status)
35
+ - [config show](#config-show)
36
+ - [config set-default](#config-set-default)
37
+ - [config set-platforms](#config-set-platforms)
38
+ - [rm (alias)](#rm-alias)
39
+ - [Default command (tfly)](#default-command-tfly)
40
+ 7. [Thread Syntax](#thread-syntax)
41
+ 8. [Error Shape](#error-shape)
42
+ 9. [Environment Variables](#environment-variables)
43
+
44
+ ---
45
+
46
+ ## Invocation
47
+
48
+ ```
49
+ typefully [global-flags] [text] [command] [subcommand] [arguments] [options]
50
+ tfly [global-flags] [text] [command] [subcommand] [arguments] [options]
51
+ ```
52
+
53
+ The binary names are `typefully` and `tfly` (short alias). All commands and subcommands are lowercase, hyphen-separated.
54
+
55
+ ---
56
+
57
+ ## Global Flags
58
+
59
+ These flags apply to every command and must be parsed before the subcommand action runs.
60
+
61
+ | Flag | Short | Description |
62
+ |------|-------|-------------|
63
+ | `--version` | `-v` | Print version and exit. Banner is suppressed. |
64
+ | `--json` | `-j` | Force JSON output mode. Suppresses banner and spinners. stdout receives only the JSON payload. |
65
+
66
+ ---
67
+
68
+ ## Output Modes
69
+
70
+ ### Human-readable (default)
71
+
72
+ When stdout is a TTY and `--json` is not set, the CLI prints colored, formatted output to stdout and uses `ora` spinners for in-progress state (written to stderr).
73
+
74
+ ### JSON mode
75
+
76
+ Activated by `--json` / `-j` **or** when `stdout` is not a TTY (i.e., piped or redirected).
77
+
78
+ - **stdout**: a single pretty-printed JSON object (`JSON.stringify(data, null, 2)`)
79
+ - **stderr**: nothing (spinners and banners are suppressed)
80
+ - The banner (ASCII art / intro text) is never shown in JSON mode
81
+
82
+ ### Banner
83
+
84
+ The banner is shown once per invocation before the first command action, **unless**:
85
+ - `--version` / `-v` is passed
86
+ - `--json` / `-j` is passed
87
+ - stdout is not a TTY
88
+
89
+ ---
90
+
91
+ ## Configuration & Authentication
92
+
93
+ ### Config file format
94
+
95
+ ```json
96
+ {
97
+ "apiKey": "typ_xxxx",
98
+ "defaultSocialSetId": 12345,
99
+ "defaultPlatforms": ["x", "linkedin"]
100
+ }
101
+ ```
102
+
103
+ Files are written with `0600` permissions. Invalid or missing files are silently ignored.
104
+
105
+ ### Priority order (highest wins)
106
+
107
+ 1. `TYPEFULLY_API_KEY` environment variable
108
+ 2. `./.typefully/config.json` (project-local, relative to cwd)
109
+ 3. `~/.config/typefully/config.json` (user-global)
110
+
111
+ ### Missing API key behavior
112
+
113
+ - **TTY stderr**: interactive clack prompt guides the user to obtain a key, choose a storage location, and saves it. Returns immediately once saved.
114
+ - **Non-TTY stderr**: prints JSON error to stderr and exits with code 1.
115
+
116
+ ```json
117
+ { "error": "API key not found", "hint": "Run: typefully setup", "api_key_url": "https://typefully.com/?settings=api" }
118
+ ```
119
+
120
+ ### Missing social_set_id behavior
121
+
122
+ - Checks for a configured default (local then global config).
123
+ - If no default: prints JSON error to stderr, exits 1.
124
+
125
+ ```json
126
+ { "error": "social_set_id is required", "hint": "Run: typefully config set-default to set a default, or provide it as an argument" }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Exit Codes
132
+
133
+ | Code | Meaning |
134
+ |------|---------|
135
+ | `0` | Success |
136
+ | `1` | Error (authentication failure, missing required arg, API error, file not found, etc.) |
137
+
138
+ ---
139
+
140
+ ## Commands
141
+
142
+ ---
143
+
144
+ ### `setup`
145
+
146
+ Interactive or non-interactive first-time setup. Saves the API key and optionally a default social set.
147
+
148
+ ```
149
+ typefully setup [options]
150
+ ```
151
+
152
+ **Options**
153
+
154
+ | Flag | Description |
155
+ |------|-------------|
156
+ | `--key <api_key>` | API key (non-interactive mode when provided) |
157
+ | `--location <global\|local>` | Where to store the config file |
158
+ | `--scope <global\|local>` | Alias for `--location` |
159
+ | `--default-social-set <id>` | Set default social set ID non-interactively |
160
+ | `--no-default` | Skip default social set selection entirely |
161
+
162
+ **Behavior**
163
+
164
+ - If `--key` is omitted: interactive clack flow (prompt for key, location, gitignore, default social set).
165
+ - If `--key` is provided: non-interactive. `--location` defaults to `global` if omitted.
166
+ - If only one social set exists and `--no-default` is not set: auto-selects it as the default.
167
+ - For local configs: offers to add `.typefully/` to `.gitignore`.
168
+
169
+ **JSON output shape**
170
+
171
+ ```json
172
+ {
173
+ "success": true,
174
+ "message": "Setup complete",
175
+ "config_path": "/Users/you/.config/typefully/config.json",
176
+ "scope": "global",
177
+ "default_social_set_id": 12345
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ### `me`
184
+
185
+ Returns the authenticated user's profile.
186
+
187
+ ```
188
+ typefully me
189
+ ```
190
+
191
+ **No options.**
192
+
193
+ **JSON output shape** — mirrors the Typefully API `/me` response (fields vary by plan):
194
+
195
+ ```json
196
+ {
197
+ "id": 1,
198
+ "name": "Ahmad Awais",
199
+ "email": "hi@example.com",
200
+ "username": "ahmadawais",
201
+ "plan": "pro",
202
+ "timezone": "America/New_York",
203
+ "locale": "en",
204
+ "avatar_url": "https://..."
205
+ }
206
+ ```
207
+
208
+ ---
209
+
210
+ ### `social-sets list`
211
+
212
+ Lists all social sets accessible to the authenticated user.
213
+
214
+ ```
215
+ typefully social-sets list
216
+ ```
217
+
218
+ **No options.**
219
+
220
+ **JSON output shape**
221
+
222
+ ```json
223
+ {
224
+ "results": [
225
+ {
226
+ "id": 123,
227
+ "name": "Ahmad Awais",
228
+ "username": "ahmadawais",
229
+ "team": null,
230
+ "platforms": {
231
+ "x": { "connected": true, "username": "ahmadawais" },
232
+ "linkedin": { "connected": false }
233
+ }
234
+ }
235
+ ],
236
+ "total": 1
237
+ }
238
+ ```
239
+
240
+ ---
241
+
242
+ ### `social-sets get`
243
+
244
+ Returns details for a specific social set.
245
+
246
+ ```
247
+ typefully social-sets get [social_set_id]
248
+ ```
249
+
250
+ **Arguments**
251
+
252
+ | Argument | Description |
253
+ |----------|-------------|
254
+ | `[social_set_id]` | Social set ID. Uses configured default if omitted. |
255
+
256
+ **JSON output shape** — single social set object (same shape as one item from `social-sets list`).
257
+
258
+ ---
259
+
260
+ ### `drafts list`
261
+
262
+ Lists drafts for a social set.
263
+
264
+ ```
265
+ typefully drafts list [social_set_id] [options]
266
+ ```
267
+
268
+ **Arguments**
269
+
270
+ | Argument | Description |
271
+ |----------|-------------|
272
+ | `[social_set_id]` | Uses configured default if omitted. |
273
+
274
+ **Options**
275
+
276
+ | Flag | Description |
277
+ |------|-------------|
278
+ | `--status <status>` | Filter: `draft`, `scheduled`, `published`, `error` |
279
+ | `--tag <tag>` | Filter by tag slug |
280
+ | `--sort <order>` | Sort field (e.g., `created_at`, `-created_at`, `scheduled_date`) |
281
+ | `--limit <n>` | Max results. Default: `10` |
282
+
283
+ **JSON output shape**
284
+
285
+ ```json
286
+ {
287
+ "results": [ /* draft objects */ ],
288
+ "total": 42
289
+ }
290
+ ```
291
+
292
+ ---
293
+
294
+ ### `drafts get`
295
+
296
+ Returns a specific draft.
297
+
298
+ ```
299
+ typefully drafts get [first_arg] [second_arg] [options]
300
+ ```
301
+
302
+ **Arguments**
303
+
304
+ Argument resolution (same pattern used by `delete`, `schedule`, `publish`):
305
+
306
+ | Args provided | Interpretation |
307
+ |---------------|----------------|
308
+ | `<social_set_id> <draft_id>` | Explicit — no ambiguity |
309
+ | `<draft_id>` (single arg) + default configured + `--use-default` | Uses default social set |
310
+ | `<draft_id>` (single arg) + default configured, no `--use-default` | **Error**: ambiguous — add `--use-default` |
311
+ | `<draft_id>` (single arg) + no default configured | **Error**: provide both IDs |
312
+ | No args | **Error**: `draft_id is required` |
313
+
314
+ **Options**
315
+
316
+ | Flag | Description |
317
+ |------|-------------|
318
+ | `--use-default` | Confirm intent to use the configured default social set when only `draft_id` is provided |
319
+
320
+ **JSON output shape** — single draft object.
321
+
322
+ ---
323
+
324
+ ### `drafts create`
325
+
326
+ Creates a new draft.
327
+
328
+ ```
329
+ typefully drafts create [social_set_id] [options]
330
+ ```
331
+
332
+ **Arguments**
333
+
334
+ | Argument | Description |
335
+ |----------|-------------|
336
+ | `[social_set_id]` | Uses configured default if omitted. |
337
+
338
+ **Options**
339
+
340
+ | Flag | Short | Description |
341
+ |------|-------|-------------|
342
+ | `--text <text>` | | Post content. Use `---` on its own line to create thread posts. |
343
+ | `--file <path>` | `-f` | Read content from file (overrides `--text`). |
344
+ | `--platform <platforms>` | | Comma-separated platform names. Uses `defaultPlatforms` config if set, otherwise first connected. |
345
+ | `--all` | | Post to all connected platforms. Mutually exclusive with `--platform`. |
346
+ | `--media <media_ids>` | | Comma-separated media IDs to attach to the first post. |
347
+ | `--title <title>` | | Internal draft title (never published). |
348
+ | `--schedule <time>` | | `"now"`, `"next-free-slot"`, or ISO 8601 datetime. |
349
+ | `--tags <tags>` | | Comma-separated tag slugs. |
350
+ | `--reply-to <url>` | | URL of X post to reply to (X platform only). |
351
+ | `--community <id>` | | X community ID to post into (X platform only). |
352
+ | `--share` | | Generate a public share URL for the draft. |
353
+ | `--scratchpad <text>` | | Internal notes attached to the draft. Never published. |
354
+ | `--notes <text>` | | Alias for `--scratchpad`. |
355
+
356
+ **Constraints**
357
+
358
+ - `--text` or `--file` is required.
359
+ - `--all` and `--platform` are mutually exclusive.
360
+
361
+ **JSON output shape** — single draft object.
362
+
363
+ ---
364
+
365
+ ### `drafts update`
366
+
367
+ Updates an existing draft. At least one change option is required.
368
+
369
+ ```
370
+ typefully drafts update [first_arg] [second_arg] [options]
371
+ ```
372
+
373
+ **Arguments** — same resolution rules as `drafts get`.
374
+
375
+ **Options**
376
+
377
+ | Flag | Short | Description |
378
+ |------|-------|-------------|
379
+ | `--text <text>` | | Replace post content. |
380
+ | `--file <path>` | `-f` | Read new content from file. |
381
+ | `--platform <platforms>` | | Override which platforms are enabled. Preserves existing if omitted. |
382
+ | `--media <media_ids>` | | Comma-separated media IDs. |
383
+ | `--append` | `-a` | Append a new post to the existing thread instead of replacing. |
384
+ | `--title <title>` | | Update the internal title. |
385
+ | `--schedule <time>` | | Reschedule: `"now"`, `"next-free-slot"`, or ISO 8601 datetime. |
386
+ | `--tags <tags>` | | Replace tag slugs. |
387
+ | `--share` | | Generate a public share URL. |
388
+ | `--scratchpad <text>` | | Update internal notes. |
389
+ | `--notes <text>` | | Alias for `--scratchpad`. |
390
+ | `--use-default` | | Confirm use of default social set for single-arg form. |
391
+
392
+ **Constraints**
393
+
394
+ - At least one of `--text`, `--file`, `--title`, `--schedule`, `--share`, `--scratchpad`/`--notes`, or `--tags` is required.
395
+
396
+ **JSON output shape** — single draft object.
397
+
398
+ ---
399
+
400
+ ### `drafts delete`
401
+
402
+ Deletes a draft permanently.
403
+
404
+ ```
405
+ typefully drafts delete [first_arg] [second_arg] [options]
406
+ ```
407
+
408
+ **Arguments** — same resolution rules as `drafts get`.
409
+
410
+ **Options**
411
+
412
+ | Flag | Description |
413
+ |------|-------------|
414
+ | `--use-default` | Confirm use of default social set for single-arg form. |
415
+
416
+ **JSON output shape**
417
+
418
+ ```json
419
+ { "success": true, "message": "Draft deleted" }
420
+ ```
421
+
422
+ ---
423
+
424
+ ### `drafts schedule`
425
+
426
+ Schedules an existing draft.
427
+
428
+ ```
429
+ typefully drafts schedule [first_arg] [second_arg] [options]
430
+ ```
431
+
432
+ **Arguments** — same resolution rules as `drafts get`.
433
+
434
+ **Options**
435
+
436
+ | Flag | Description |
437
+ |------|-------------|
438
+ | `--time <time>` | **Required.** `"next-free-slot"` or ISO 8601 datetime. |
439
+ | `--use-default` | Confirm use of default social set for single-arg form. |
440
+
441
+ **JSON output shape** — single draft object.
442
+
443
+ ---
444
+
445
+ ### `drafts publish`
446
+
447
+ Publishes a draft immediately.
448
+
449
+ ```
450
+ typefully drafts publish [first_arg] [second_arg] [options]
451
+ ```
452
+
453
+ **Arguments** — same resolution rules as `drafts get`.
454
+
455
+ **Options**
456
+
457
+ | Flag | Description |
458
+ |------|-------------|
459
+ | `--use-default` | Confirm use of default social set for single-arg form. |
460
+
461
+ **JSON output shape** — single draft object.
462
+
463
+ ---
464
+
465
+ ### `create-draft` (alias)
466
+
467
+ Top-level alias for `drafts create` with positional text. Designed for agents and scripts where positional arguments are more natural.
468
+
469
+ ```
470
+ typefully create-draft [text] [options]
471
+ ```
472
+
473
+ **Arguments**
474
+
475
+ | Argument | Description |
476
+ |----------|-------------|
477
+ | `[text]` | Draft content. Overridden by `--text` or `--file` if provided. |
478
+
479
+ **Options**
480
+
481
+ | Flag | Short | Description |
482
+ |------|-------|-------------|
483
+ | `--social-set-id <id>` | | Social set ID. Uses configured default if omitted. |
484
+ | `--text <text>` | | Explicit content (overrides positional `[text]`). |
485
+ | `--file <path>` | `-f` | Read content from file (overrides positional `[text]` and `--text`). |
486
+ | `--platform <platforms>` | | Comma-separated platform names. |
487
+ | `--all` | | Post to all connected platforms. |
488
+ | `--media <media_ids>` | | Comma-separated media IDs. |
489
+ | `--title <title>` | | Internal draft title. |
490
+ | `--schedule <time>` | | `"now"`, `"next-free-slot"`, or ISO 8601 datetime. |
491
+ | `--tags <tags>` | | Comma-separated tag slugs. |
492
+ | `--reply-to <url>` | | URL of X post to reply to. |
493
+ | `--community <id>` | | X community ID. |
494
+ | `--share` | | Generate a public share URL. |
495
+ | `--scratchpad <text>` | | Internal notes. |
496
+ | `--notes <text>` | | Alias for `--scratchpad`. |
497
+
498
+ **Text resolution priority**: `--file` > `--text` > positional `[text]`. At least one must be provided.
499
+
500
+ **JSON output shape** — single draft object (same as `drafts create`).
501
+
502
+ ---
503
+
504
+ ### `update-draft` (alias)
505
+
506
+ Top-level alias for `drafts update` with a positional draft ID. Designed for agents and scripts.
507
+
508
+ ```
509
+ typefully update-draft <draft_id> [text] [options]
510
+ ```
511
+
512
+ **Arguments**
513
+
514
+ | Argument | Required | Description |
515
+ |----------|----------|-------------|
516
+ | `<draft_id>` | Yes | ID of the draft to update. |
517
+ | `[text]` | No | New content. Overridden by `--text` or `--file` if provided. |
518
+
519
+ **Options**
520
+
521
+ | Flag | Short | Description |
522
+ |------|-------|-------------|
523
+ | `--social-set-id <id>` | | Social set ID. Uses configured default if omitted. |
524
+ | `--text <text>` | | New content (overrides positional `[text]`). |
525
+ | `--file <path>` | `-f` | Read content from file. |
526
+ | `--platform <platforms>` | | Override enabled platforms. |
527
+ | `--media <media_ids>` | | Comma-separated media IDs. |
528
+ | `--append` | `-a` | Append new post to existing thread. |
529
+ | `--title <title>` | | Update internal title. |
530
+ | `--schedule <time>` | | Reschedule. |
531
+ | `--tags <tags>` | | Replace tag slugs. |
532
+ | `--share` | | Generate a public share URL. |
533
+ | `--scratchpad <text>` | | Update internal notes. |
534
+ | `--notes <text>` | | Alias for `--scratchpad`. |
535
+
536
+ **Text resolution priority**: `--file` > `--text` > positional `[text]`.
537
+
538
+ **Constraints** — same as `drafts update`: at least one change option required.
539
+
540
+ **JSON output shape** — single draft object (same as `drafts update`).
541
+
542
+ ---
543
+
544
+ ### `tags list`
545
+
546
+ Lists all tags for a social set.
547
+
548
+ ```
549
+ typefully tags list [social_set_id]
550
+ ```
551
+
552
+ **Arguments**
553
+
554
+ | Argument | Description |
555
+ |----------|-------------|
556
+ | `[social_set_id]` | Uses configured default if omitted. |
557
+
558
+ **JSON output shape**
559
+
560
+ ```json
561
+ {
562
+ "results": [
563
+ { "id": 1, "name": "Product", "slug": "product" }
564
+ ]
565
+ }
566
+ ```
567
+
568
+ ---
569
+
570
+ ### `tags create`
571
+
572
+ Creates a new tag.
573
+
574
+ ```
575
+ typefully tags create [social_set_id] --name <name>
576
+ ```
577
+
578
+ **Arguments**
579
+
580
+ | Argument | Description |
581
+ |----------|-------------|
582
+ | `[social_set_id]` | Uses configured default if omitted. |
583
+
584
+ **Options**
585
+
586
+ | Flag | Description |
587
+ |------|-------------|
588
+ | `--name <name>` | **Required.** Tag display name. |
589
+
590
+ **JSON output shape**
591
+
592
+ ```json
593
+ { "id": 1, "name": "Product", "slug": "product" }
594
+ ```
595
+
596
+ ---
597
+
598
+ ### `media upload`
599
+
600
+ Uploads a media file and waits for processing (unless `--no-wait`).
601
+
602
+ ```
603
+ typefully media upload <file_path> [social_set_id] [options]
604
+ ```
605
+
606
+ **Arguments**
607
+
608
+ | Argument | Required | Description |
609
+ |----------|----------|-------------|
610
+ | `<file_path>` | Yes | Path to the local file to upload. |
611
+ | `[social_set_id]` | No | Uses configured default if omitted. |
612
+
613
+ **Options**
614
+
615
+ | Flag | Description |
616
+ |------|-------------|
617
+ | `--social-set-id <id>` | Social set ID via flag (overrides positional). |
618
+ | `--no-wait` | Return immediately after S3 upload without waiting for processing. |
619
+ | `--timeout <seconds>` | Max seconds to wait for processing. Default: `60`. |
620
+
621
+ **Behavior**
622
+
623
+ 1. Requests a presigned S3 URL from the API.
624
+ 2. Uploads the file to S3 via HTTP PUT.
625
+ 3. Polls `media status` until `status === "ready"` or `status === "error"/"failed"`, or timeout.
626
+ 4. With `--no-wait`: returns after step 2 with `status: "processing"`.
627
+
628
+ Poll interval can be overridden with `TYPEFULLY_MEDIA_POLL_INTERVAL_MS` env var (default: 2000ms).
629
+
630
+ **JSON output shape (ready)**
631
+
632
+ ```json
633
+ { "media_id": "abc-123", "status": "ready", "message": "Media uploaded and ready" }
634
+ ```
635
+
636
+ **JSON output shape (no-wait)**
637
+
638
+ ```json
639
+ { "media_id": "abc-123", "message": "Upload complete. Use media status to check processing." }
640
+ ```
641
+
642
+ **JSON output shape (timeout)**
643
+
644
+ ```json
645
+ {
646
+ "media_id": "abc-123",
647
+ "status": "processing",
648
+ "message": "Upload complete but still processing. Use media status to check.",
649
+ "hint": "Increase timeout with --timeout <seconds>"
650
+ }
651
+ ```
652
+
653
+ ---
654
+
655
+ ### `media status`
656
+
657
+ Checks the processing status of an uploaded media file.
658
+
659
+ ```
660
+ typefully media status <media_id> [social_set_id] [options]
661
+ ```
662
+
663
+ **Arguments**
664
+
665
+ | Argument | Required | Description |
666
+ |----------|----------|-------------|
667
+ | `<media_id>` | Yes | Media ID returned by `media upload`. |
668
+ | `[social_set_id]` | No | Uses configured default if omitted. |
669
+
670
+ **Options**
671
+
672
+ | Flag | Description |
673
+ |------|-------------|
674
+ | `--social-set-id <id>` | Social set ID via flag (overrides positional). |
675
+
676
+ **JSON output shape** — media status object from the API (includes `status` field: `"ready"`, `"processing"`, `"error"`, `"failed"`).
677
+
678
+ ---
679
+
680
+ ### `config show`
681
+
682
+ Shows the current configuration state.
683
+
684
+ ```
685
+ typefully config show
686
+ ```
687
+
688
+ **No options.**
689
+
690
+ **JSON output shape (configured)**
691
+
692
+ ```json
693
+ {
694
+ "configured": true,
695
+ "active_source": "/Users/you/.config/typefully/config.json",
696
+ "api_key_preview": "typ_xxxx...",
697
+ "default_social_set": { "id": 123, "source": "/Users/you/.config/typefully/config.json" },
698
+ "default_platforms": ["x", "linkedin"],
699
+ "config_files": {
700
+ "local": { "path": ".typefully/config.json", "has_key": false, "has_default_social_set": false },
701
+ "global": { "path": "/Users/you/.config/typefully/config.json", "has_key": true, "has_default_social_set": true }
702
+ }
703
+ }
704
+ ```
705
+
706
+ **JSON output shape (not configured)**
707
+
708
+ ```json
709
+ { "configured": false, "hint": "Run: typefully setup", "api_key_url": "https://typefully.com/?settings=api" }
710
+ ```
711
+
712
+ ---
713
+
714
+ ### `config set-default`
715
+
716
+ Sets the default social set ID.
717
+
718
+ ```
719
+ typefully config set-default [social_set_id] [options]
720
+ ```
721
+
722
+ **Arguments**
723
+
724
+ | Argument | Description |
725
+ |----------|-------------|
726
+ | `[social_set_id]` | Social set ID to set as default. Prompts interactively if omitted. |
727
+
728
+ **Options**
729
+
730
+ | Flag | Description |
731
+ |------|-------------|
732
+ | `--location <global\|local>` | Where to save: `global` (~/.config/typefully/) or `local` (./.typefully/). Interactive if omitted. |
733
+ | `--scope <global\|local>` | Alias for `--location`. |
734
+
735
+ **Behavior**
736
+
737
+ - Verifies the social set exists via API before saving.
738
+ - If `social_set_id` is omitted: fetches social sets and shows a numbered list to choose from.
739
+ - If only one social set exists: auto-selects it.
740
+
741
+ **JSON output shape**
742
+
743
+ ```json
744
+ {
745
+ "success": true,
746
+ "message": "Default social set configured",
747
+ "default_social_set_id": 123,
748
+ "config_path": "/Users/you/.config/typefully/config.json",
749
+ "scope": "global"
750
+ }
751
+ ```
752
+
753
+ ---
754
+
755
+ ### `config set-platforms`
756
+
757
+ Sets the default platforms used when creating drafts without `--platform` or `--all`.
758
+
759
+ ```
760
+ typefully config set-platforms [options]
761
+ tfly config set-platforms [options]
762
+ ```
763
+
764
+ **Options**
765
+
766
+ | Flag | Description |
767
+ |------|-------------|
768
+ | `--platforms <platforms>` | Comma-separated platform list (skips interactive). |
769
+ | `--location <global\|local>` | Where to save: `global` or `local`. Interactive if omitted. |
770
+ | `--scope <global\|local>` | Alias for `--location`. |
771
+
772
+ **Behavior**
773
+
774
+ - If `--platforms` is omitted: shows a clack multiselect of all 5 supported platforms, pre-ticking `x`.
775
+ - Saved preference is used by `drafts create`, `create-draft`, and the default `tfly` command when no `--platform` flag is given.
776
+ - If saved platforms don't match connected platforms on the social set, falls back to first connected.
777
+
778
+ **JSON output shape**
779
+
780
+ ```json
781
+ {
782
+ "success": true,
783
+ "default_platforms": ["x", "linkedin", "threads"],
784
+ "config_path": "/Users/you/.config/typefully/config.json"
785
+ }
786
+ ```
787
+
788
+ ---
789
+
790
+ ### `rm` (alias)
791
+
792
+ Deletes a draft. Provide a draft ID for direct deletion, or omit it for an interactive clack multiselect picker.
793
+
794
+ ```
795
+ tfly rm [draft_id] [options]
796
+ typefully rm [draft_id] [options]
797
+ ```
798
+
799
+ **Arguments**
800
+
801
+ | Argument | Description |
802
+ |----------|-------------|
803
+ | `[draft_id]` | Draft ID to delete. Omit to pick interactively. |
804
+
805
+ **Options**
806
+
807
+ | Flag | Description |
808
+ |------|-------------|
809
+ | `--social-set-id <id>` | Social set ID. Uses configured default if omitted. |
810
+ | `--status <status>` | Filter drafts shown in picker. Default: `draft`. |
811
+ | `--limit <n>` | Max drafts loaded in picker. Default: `20`. |
812
+
813
+ **Behavior**
814
+
815
+ - **With `draft_id`**: deletes immediately, no prompts.
816
+ - **Without `draft_id`**: fetches the draft list, then fetches each draft in parallel to load text content, shows a multiselect, asks for confirmation, then deletes selected drafts.
817
+
818
+ **JSON output shape**
819
+
820
+ ```json
821
+ { "success": true, "message": "Draft deleted" }
822
+ ```
823
+
824
+ ---
825
+
826
+ ### Default command (`tfly` / `typefully`)
827
+
828
+ Running `tfly` (or `typefully`) with no subcommand creates a draft directly or launches an interactive flow.
829
+
830
+ ```
831
+ tfly [text]
832
+ typefully [text]
833
+ ```
834
+
835
+ **Arguments**
836
+
837
+ | Argument | Description |
838
+ |----------|-------------|
839
+ | `[text]` | Post text. If provided: creates a draft directly using default platforms and social set. If omitted: launches interactive clack flow. |
840
+
841
+ **Behavior — with text**
842
+
843
+ - Uses configured `defaultPlatforms` (or first connected platform as fallback).
844
+ - Creates draft immediately with no prompts (same as `create-draft "<text>"`).
845
+
846
+ **Behavior — without text (interactive)**
847
+
848
+ 1. clack `text` prompt for post content.
849
+ 2. clack `multiselect` for platforms — pre-ticks `defaultPlatforms` if configured.
850
+ 3. clack `select` for timing: `Save as draft` / `Schedule: next free slot` / `Publish now`.
851
+ 4. Creates draft, shows result.
852
+
853
+ Cancelling at any prompt (Ctrl+C) exits cleanly with code 0.
854
+
855
+ **JSON output shape** — single draft object (same as `drafts create`).
856
+
857
+ ---
858
+
859
+ ## Thread Syntax
860
+
861
+ Split a single draft into a thread by using `---` on its own line as a separator:
862
+
863
+ ```
864
+ First post in the thread.
865
+
866
+ ---
867
+
868
+ Second post. Can include line breaks
869
+ within the same post.
870
+
871
+ ---
872
+
873
+ Third post.
874
+ ```
875
+
876
+ The separator is matched by: `/\r?\n[ \t]*---[ \t]*\r?\n/`
877
+
878
+ ---
879
+
880
+ ## Error Shape
881
+
882
+ All errors are written to **stdout** as JSON (so they are parseable in JSON mode):
883
+
884
+ ```json
885
+ { "error": "<message>", "hint": "<optional hint>", ...extraFields }
886
+ ```
887
+
888
+ Process exits with code `1`.
889
+
890
+ ---
891
+
892
+ ## Environment Variables
893
+
894
+ | Variable | Description |
895
+ |----------|-------------|
896
+ | `TYPEFULLY_API_KEY` | API key. Highest priority, overrides all config files. |
897
+ | `TYPEFULLY_API_BASE` | Override API base URL. Default: `https://api.typefully.com/v2`. |
898
+ | `TYPEFULLY_MEDIA_POLL_INTERVAL_MS` | Polling interval for `media upload`. Default: `2000`. |