postemi 0.0.0-stage → 1.0.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/SKILL.md ADDED
@@ -0,0 +1,1068 @@
1
+ ---
2
+ name: postemi
3
+ description: Postemi schedules and publishes social posts from the command line to 20 platforms, including Facebook, Instagram, X, LinkedIn, TikTok, YouTube, Threads, Bluesky, Pinterest and Mastodon.
4
+ homepage: https://postemi.com
5
+ metadata: {"openclaw":{"emoji":"🌎","requires":{"bins":[],"env":[]}}}
6
+ ---
7
+
8
+ ## Install
9
+
10
+ The npm package and the command are both **`postemi`**.
11
+
12
+ ```bash
13
+ npm install -g postemi
14
+ postemi auth:login # opens the browser to approve the login
15
+ ```
16
+
17
+ official website: https://postemi.com
18
+ API base: https://api.postemi.com
19
+ MCP server, for chat clients without a terminal: https://mcp.postemi.com/mcp
20
+ (the user signs in to Postemi in the browser; same account, same channels)
21
+
22
+ ---
23
+
24
+ | Property | Value |
25
+ |----------|-------|
26
+ | **name** | postemi |
27
+ | **description** | CLI for scheduling and publishing posts through the Postemi public API |
28
+ | **allowed-tools** | Bash(postemi:*) |
29
+
30
+ ---
31
+
32
+ ## Which channels you can publish to
33
+
34
+ **Only these. Do not offer a user anything else, and do not try to post to it.**
35
+ Postemi's server carries inherited code for many more providers; that code is not
36
+ enabled and not supported.
37
+
38
+ | Channel | `identifier` |
39
+ |---|---|
40
+ | X (Twitter) | `x` |
41
+ | Facebook (Pages) | `facebook` |
42
+ | Instagram | `instagram` |
43
+ | LinkedIn (member feed) | `linkedin` |
44
+ | TikTok | `tiktok` |
45
+ | YouTube | `youtube` |
46
+ | Threads | `threads` |
47
+ | Bluesky | `bluesky` |
48
+ | Telegram | `telegram` |
49
+ | Discord | `discord` |
50
+ | Slack | `slack` |
51
+ | Pinterest (BETA) | `pinterest` |
52
+
53
+ **Pinterest is a BETA channel.** Until Pinterest grants Postemi Standard access, a
54
+ pin is visible only to the account that created it. Tell the user this before
55
+ you schedule a pin. A pin needs a `board` id in `--settings`; get the ids with
56
+ `postemi integrations:trigger <integration-id> boards` (see PROVIDER_SETTINGS.md).
57
+
58
+ **`integrations:list` is the truth.** The table says what Postemi supports; that
59
+ command says what THIS user has actually connected. Check it before you
60
+ schedule anything, and never post to an id that is not in its output.
61
+
62
+ **Never assume another provider exists.** If a user asks for Mastodon, Reddit,
63
+ Google Business Profile, Medium, Dev.to, WordPress, Tumblr, Farcaster or
64
+ anything else, the answer is that Postemi does not offer it. Do not
65
+ build a `--settings` payload for it.
66
+
67
+ ---
68
+
69
+ ## ⚠️ Two Hard Rules (Read First)
70
+
71
+ **Rule 1: Authenticate before anything.** All commands fail without valid credentials.
72
+
73
+ **Rule 2: Every file passed to `-m` (or to `image`/media fields in JSON mode) MUST first go through `postemi upload`.** Raw filesystem paths (`image.jpg`, `video.mp4`) and external URLs (`https://example.com/...`) are **NOT** accepted by the publishing pipeline. TikTok, Instagram, YouTube, and most other providers reject anything that isn't a Postemi-verified URL. Always:
74
+
75
+ ```bash
76
+ RESULT=$(postemi upload <file>)
77
+ URL=$(echo "$RESULT" | jq -r '.path')
78
+ postemi posts:create ... -m "$URL" ...
79
+ ```
80
+
81
+ If you see `-m "something.jpg"` anywhere below, treat it as shorthand for "the `.path` you got back from `postemi upload something.jpg`", never a raw local file.
82
+
83
+ ---
84
+
85
+ ## Output contract
86
+
87
+ Every command prints its **JSON result, and nothing else, to stdout**;
88
+ status lines, warnings and errors go to stderr, and every failure exits 1. So
89
+ `postemi upload x.jpg | jq -r '.path'` is always safe, with no stripping needed.
90
+ Unknown flags are an error, not silently ignored. If a flag is rejected, it
91
+ does not exist; do not retry with variations.
92
+
93
+ ---
94
+
95
+ ## ⚠️ Authentication Required
96
+
97
+ **You MUST authenticate before running any Postemi CLI command.** All commands will fail without valid credentials.
98
+
99
+ Before doing anything else, check auth status:
100
+ ```bash
101
+ postemi auth:status
102
+ ```
103
+
104
+ If not authenticated:
105
+ 1. **Sign in:** `postemi auth:login` opens a browser, and the human approves it.
106
+ You never see or handle a key.
107
+ 2. **No account yet:** `postemi auth:signup` creates one, see below. Only when
108
+ the user asks you to create a Postemi account for them. They can also sign
109
+ up at https://postemi.com/app/signup and then run `postemi auth:login`.
110
+
111
+ ### Creating an account for the user (no free trial)
112
+
113
+ When the user asks you to make them a Postemi account and they have none:
114
+
115
+ ```bash
116
+ postemi auth:signup --email anna@example.com
117
+ # → {codeSent:true, terms, privacy, timezone, language, next} (no terminal: the code is on its way; `next` is the exact second command, every option kept)
118
+ postemi auth:signup --email anna@example.com --code 123456 --accept-terms [--workspace-name "Anna's Cafe"] [--timezone Europe/London]
119
+ # → {created:true, workspaceId, workspaceName, trial:false, credentials, next}
120
+ ```
121
+
122
+ 1. Run it with `--email` only. Postemi e-mails the user a six-digit code
123
+ (valid 10 minutes). Ask the user to read the code to you.
124
+ 2. Show the user the terms (https://postemi.com/terms) and the privacy notice
125
+ (https://postemi.com/privacy) and ask whether they agree. Pass
126
+ `--accept-terms` ONLY after they said yes; never decide it for them.
127
+ 3. Run it again with `--code` and `--accept-terms`. The CLI keeps the
128
+ sign-in the same way `auth:login` does and never prints it.
129
+ 4. **There is no free trial on an account made this way**, and until the
130
+ user pays, only the `billing:*` commands work. Next: `billing:plans`,
131
+ confirm a plan with the user, `billing:subscribe`, give them the link.
132
+ Channels are connected by the user in the web app after paying.
133
+ 5. **You keep working for the user only on a plan with API access**
134
+ (`limits.apiAndMcpAccess: true` in `billing:plans`). Basic has none: if
135
+ the user picks it, tell them BEFORE they pay that you will not be able to
136
+ schedule for them afterwards. `billing:subscribe` answers with
137
+ `apiAndMcpAccess: false` and prints the warning on stderr for such a plan.
138
+
139
+ Exit 1 with "already has a Postemi account" (HTTP 409) means the address is
140
+ taken: the user signs in with `postemi auth:login` instead. A wrong or expired
141
+ code: run step 1 again for a new one.
142
+
143
+ `postemi auth:status` reports a valid key of a workspace that has not paid yet
144
+ as `authenticated: true, apiAccess: false, reason: "no-plan"` (exit 0). It is
145
+ not an invalid key; do not run `auth:login` for it. Only the `billing:*`
146
+ commands work until the workspace is on a plan with API access.
147
+
148
+ **Do NOT proceed with any other commands until authentication is confirmed.**
149
+
150
+ ### What a key can and cannot do
151
+
152
+ Keys are **per person, named, and scoped**. They are stored hashed and shown
153
+ exactly once, at creation, in the web app under **Settings → Developers**. A
154
+ lost key cannot be recovered. It can only be rotated.
155
+
156
+ Two limits apply to every request, and both explain 403s that look like bugs:
157
+
158
+ 1. **Scopes.** A key carries a subset of `posts:read`, `posts:draft`,
159
+ `posts:publish`, `channels:read`, `channels:write`, `media:write`,
160
+ `analytics:read`. A route refuses a key that lacks its scope.
161
+ 2. **The owner's current role, re-checked on every request.** A key can never
162
+ do more than the person who created it can do *right now*. If that person is
163
+ demoted, their live keys narrow on the next request, with nobody revoking
164
+ anything. So a key that worked yesterday can legitimately start returning
165
+ 403 today with no change to the key.
166
+
167
+ A key can also be **granted only some workspaces and only some channels**. A
168
+ request naming a channel outside the grant is refused even when the scope is
169
+ right.
170
+
171
+ **When you hit a 403, read the message.** The CLI names the missing scope; the key is valid, so `auth:login` does not fix it. Do not
172
+ retry, and do not try a different route to get around it. Ask the user to mint
173
+ a key with the scope, or to have someone with the right role do it.
174
+
175
+ ---
176
+
177
+ ## Core Workflow
178
+
179
+ The fundamental pattern for using Postemi CLI:
180
+
181
+ 1. **Authenticate** - Verify or set up authentication (see above)
182
+ 2. **Workspace** - `postemi workspaces:list`; if more than one and none is `current`, ask the user which, then `postemi workspaces:use <id>`. Channel ids belong to a workspace.
183
+ 3. **Discover** - List integrations and get their settings
184
+ 4. **Fetch** - Use integration tools to retrieve dynamic data (Instagram `audioSearch`; Discord and Slack `channels`)
185
+ 5. **Prepare** - Upload media files if needed
186
+ 6. **Post** - Create posts with content, media, and platform-specific settings
187
+ 7. **Analyze** - Track performance with platform and post-level analytics
188
+ 8. **Resolve** - If analytics returns `{"missing": true}`, run `posts:missing` to list provider content, then `posts:connect` to link it
189
+
190
+ ```bash
191
+ # 1. Authenticate
192
+ postemi auth:status
193
+ # If not authenticated: postemi auth:login (no client id/secret, it is a device flow)
194
+
195
+ # 2. Discover
196
+ postemi integrations:list
197
+ postemi integrations:settings <integration-id>
198
+
199
+ # 3. Fetch (if needed)
200
+ postemi integrations:trigger <integration-id> <method> -d '{"key":"value"}'
201
+
202
+ # 4. Prepare
203
+ postemi upload image.jpg
204
+ # or, when the file is on the person's phone / not on this machine:
205
+ postemi upload:link # show the url to the person, they drop the files
206
+ postemi upload:files <id> # what arrived; use each .path in -m
207
+
208
+ # 5. Post
209
+ postemi posts:create -c "Content" -m "image.jpg" -i "<integration-id>"
210
+
211
+ # 6. Analyze
212
+ postemi analytics:platform <integration-id> -d 30
213
+ postemi analytics:post <post-id> -d 7
214
+
215
+ # 7. Resolve (if analytics returns {"missing": true})
216
+ postemi posts:missing <post-id>
217
+ postemi posts:connect <post-id> --release-id "<content-id>"
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Essential Commands
223
+
224
+ ### Workspaces
225
+
226
+ A credential may reach several workspaces (the user ticked "All my
227
+ workspaces" at login, or made a multi-workspace key). The API refuses every
228
+ request from such a key that does not name one, with 403 and the message
229
+ "This API key spans several workspaces", so check first:
230
+
231
+ ```bash
232
+ postemi workspaces:list # [{id, name, role, current}]
233
+ postemi workspaces:use <id> # persists; every later command acts there
234
+ postemi workspaces:current
235
+ postemi integrations:list --workspace <id> # one command elsewhere, without switching
236
+ ```
237
+
238
+ Say the workspace name back to the user when they have more than one, and
239
+ never pick one for them. After a switch, run `integrations:list` again:
240
+ channel ids from the other workspace are invalid here.
241
+
242
+ ### Authentication
243
+
244
+ **Sign in: device login**
245
+ ```bash
246
+ # Opens a browser. No client ID or secret, the human approves the code shown.
247
+ postemi auth:login
248
+
249
+ # Check auth status (verifies credentials are still valid)
250
+ postemi auth:status
251
+
252
+ # Logout (remove stored credentials)
253
+ postemi auth:logout
254
+ ```
255
+
256
+ How it works, so you can explain it if the user asks: the CLI calls
257
+ `POST /device/code`, prints a short user code and opens the approval page in
258
+ the browser, then polls `POST /device/token` until the human (signed in to
259
+ Postemi) approves it and chooses which workspace and which channels the
260
+ resulting key may reach. This runs inside Postemi's own backend; there is no
261
+ separate auth service.
262
+
263
+ The CLI keeps its own sign-in after that; do not read, print or copy it.
264
+
265
+ Off a terminal (when you, an agent, run it) the CLI does not ask for a
266
+ timezone: pass `--timezone` on date-taking commands, or run
267
+ `postemi config:set timezone Europe/London` once. See "Date Handling".
268
+
269
+ The CLI talks to `https://api.postemi.com`. The MCP server, for chat clients,
270
+ is `https://mcp.postemi.com/mcp`. Other hostnames you may have read about do
271
+ not exist.
272
+
273
+ ### Upload link: when the file is not on this machine
274
+
275
+ `postemi upload` needs the file on disk. When the person has it on their phone,
276
+ or you cannot see their filesystem, do NOT ask them to host it somewhere
277
+ public. Mint a link:
278
+
279
+ ```bash
280
+ postemi upload:link
281
+ # → {"id":"kR3mQ7xP2nLa","url":"https://…","expiresAt":"…"}
282
+ ```
283
+
284
+ Show the `url` to the person verbatim and say: open it, drop the image or
285
+ video, come back and tell me it is done. It works signed out and on a phone.
286
+ When they say done:
287
+
288
+ ```bash
289
+ postemi upload:files kR3mQ7xP2nLa
290
+ # → {"status":"ready","count":1,"files":[{"id":"…","name":"clip.mp4","path":"https://…","type":"video"}]}
291
+ ```
292
+
293
+ Pass each `path` to `posts:create -m`. `status: "empty"` is not an error: they
294
+ have not finished, or spoke before the last file landed. Ask them to check the
295
+ page says "uploaded" and run it again. The link lives two hours; mint a new one
296
+ if it expires.
297
+
298
+ ### Subscription (only when the user asks)
299
+
300
+ Four commands, the only place a plan is named. Call them only when the user
301
+ asks about plans, asks to subscribe, or asks to manage their subscription;
302
+ never suggest a plan or an upgrade otherwise.
303
+
304
+ ```bash
305
+ postemi billing:plans # → {current:{tier,plan}, plans:[{plan,slug,monthly,yearly,limits}], trial, trialDays}
306
+ postemi billing:subscribe --tier pro --period yearly # → {status:"requires_payment", checkoutUrl, checkoutId, plan, amount, currency, trial, expiresAt, apiAndMcpAccess}
307
+ postemi billing:status # → {state, plan, period, currentPeriodEnd, payerIsCaller, pendingCheckout, apiAndMcpAccess}
308
+ postemi billing:manage # → {portalUrl}
309
+ ```
310
+
311
+ The flow:
312
+
313
+ 1. Confirm the plan (Basic, Pro, Creator or Team; `--tier basic`, `pro`,
314
+ `creator` or `team`, or the `slug` that `billing:plans` prints) and the
315
+ period (monthly or yearly) with the user. Basic has no API access: if the
316
+ user picks it, tell them BEFORE they pay that you will not be able to
317
+ schedule for them afterwards.
318
+ 2. Run `billing:subscribe` once and give the user `checkoutUrl`. **The user
319
+ opens it and pays with their OWN Link wallet or card.** Do NOT pay with a
320
+ one-time or agent-issued card, a virtual card number or any credential of
321
+ your own: a Postemi subscription renews monthly or yearly, and a one-time
322
+ credential fails at the first renewal, which ends the plan. `--open` opens
323
+ the link in the browser of this machine; use it only when this machine is
324
+ the user's own.
325
+ 3. After the user says they have paid, run `billing:status` and check that
326
+ `state` is `trialing` or `active` before continuing setup. It changes
327
+ within seconds of the payment. If `billing:status` shows a
328
+ `pendingCheckout`, hand that link back instead of running
329
+ `billing:subscribe` again.
330
+ 4. A live subscription is changed, cancelled or invoiced in the Stripe
331
+ portal: `billing:manage` prints the link. Do not describe plans there.
332
+
333
+ These commands work before the workspace has a plan with API access (they
334
+ are how it gets one). `billing:subscribe` and `billing:manage` need a
335
+ full-workspace key or an OAuth token held by the person who pays (or a
336
+ workspace owner when nobody pays yet); a scoped key is refused with a
337
+ message that says so.
338
+
339
+ ### Rate limits
340
+
341
+ Tiered, **per key, per route, per hour**. Each route keeps its own bucket, so a
342
+ polling loop on `posts:list` cannot starve `posts:create`.
343
+
344
+ | Routes | Limit |
345
+ |---|---|
346
+ | Reads: `posts:list`, `integrations:list`, `integrations:groups`, `integrations:settings`, `posts:missing`, `posts:find-slot`, notifications | 600/h (the `API_LIMIT` default; production sets 600) |
347
+ | Publish and delete: `posts:create`, `posts:delete`, `posts:delete-group`, `posts:status`, `integrations:trigger` | 60/h |
348
+ | Analytics and channel refresh: `analytics:platform`, `analytics:post`, `/social/:integration` | 30/h |
349
+ | Uploads: `upload`, `upload-from-url` | 30/h |
350
+ | Upload links: `upload:link` 60/h, `upload:files` 600/h | |
351
+
352
+ The bucket is keyed on the hash of the presented key, so rotating a key starts
353
+ a fresh allowance and the old secret's allowance dies with it. Exceeding a
354
+ limit returns `429`. **Back off; do not retry in a tight loop**: a retry
355
+ storm just burns the next hour's allowance too.
356
+
357
+ ### Integration Discovery
358
+
359
+ ```bash
360
+ # List all connected integrations
361
+ postemi integrations:list
362
+
363
+ # List integrations belonging to a specific group (customer)
364
+ postemi integrations:list --group <group-id>
365
+
366
+ # List all groups (customers) as {id, name}
367
+ postemi integrations:groups
368
+
369
+ # Get settings schema for specific integration
370
+ postemi integrations:settings <integration-id>
371
+
372
+ # Trigger integration tool to fetch dynamic data
373
+ postemi integrations:trigger <integration-id> <method-name>
374
+ postemi integrations:trigger <integration-id> <method-name> -d '{"param":"value"}'
375
+ ```
376
+
377
+ ### Creating Posts
378
+
379
+ ```bash
380
+ # Simple post (date is REQUIRED)
381
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" -i "integration-id"
382
+
383
+ # Draft post
384
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" -t draft -i "integration-id"
385
+
386
+ # Post with media (upload each file FIRST, see Rule 2)
387
+ IMG1=$(postemi upload img1.jpg | jq -r '.path')
388
+ IMG2=$(postemi upload img2.jpg | jq -r '.path')
389
+ postemi posts:create -c "Content" -m "$IMG1,$IMG2" -s "2026-12-31T12:00:00Z" -i "integration-id"
390
+
391
+ # Post with comments (each with own media, every file uploaded first)
392
+ MAIN=$(postemi upload main.jpg | jq -r '.path')
393
+ C1=$(postemi upload comment1.jpg | jq -r '.path')
394
+ C2A=$(postemi upload comment2.jpg | jq -r '.path')
395
+ C2B=$(postemi upload comment3.jpg | jq -r '.path')
396
+ postemi posts:create \
397
+ -c "Main post" -m "$MAIN" \
398
+ -c "First comment" -m "$C1" \
399
+ -c "Second comment" -m "$C2A,$C2B" \
400
+ -s "2026-12-31T12:00:00Z" \
401
+ -i "integration-id"
402
+
403
+ # Multi-platform post
404
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" -i "$X_ID,$FB_ID,$THREADS_ID"
405
+
406
+ # Platform-specific settings
407
+ postemi posts:create \
408
+ -c "Content" \
409
+ -s "2026-12-31T12:00:00Z" \
410
+ --settings '{"who_can_reply_post":"everyone"}' \
411
+ -i "$X_ID"
412
+
413
+ # Complex post from JSON file
414
+ postemi posts:create --json post.json
415
+ ```
416
+
417
+ ### Managing Posts
418
+
419
+ ```bash
420
+ # List posts (defaults to last 30 days to next 30 days)
421
+ postemi posts:list
422
+
423
+ # List posts in date range
424
+ postemi posts:list --startDate "2026-01-01T00:00:00Z" --endDate "2026-12-31T23:59:59Z"
425
+
426
+ # Delete one post (one channel, with its thread or comments); other channels of its group stay
427
+ postemi posts:delete <post-id>
428
+
429
+ # Delete a whole group: the post on every channel it went to (group from posts:list)
430
+ postemi posts:delete-group <group>
431
+
432
+ # Change post status (draft ↔ schedule)
433
+ postemi posts:status <post-id> --status draft # Move back to draft, terminates any running publish workflow
434
+ postemi posts:status <post-id> --status schedule # Promote a draft into the publishing queue (uses the post's stored date)
435
+ ```
436
+
437
+ ### Analytics
438
+
439
+ ```bash
440
+ # Get platform analytics (default: last 7 days)
441
+ postemi analytics:platform <integration-id>
442
+
443
+ # Get platform analytics for last 30 days
444
+ postemi analytics:platform <integration-id> -d 30
445
+
446
+ # Get post analytics (default: last 7 days)
447
+ postemi analytics:post <post-id>
448
+
449
+ # Get post analytics for last 30 days
450
+ postemi analytics:post <post-id> -d 30
451
+ ```
452
+
453
+ Returns an array of metrics (e.g. Followers, Impressions, Likes, Comments) with daily data points and percentage change over the period.
454
+
455
+ **⚠️ IMPORTANT: Missing Release ID Handling**
456
+
457
+ If `analytics:post` returns `{"missing": true}` instead of an analytics array, the post was published but the platform didn't return a usable post ID. You **must** resolve this before analytics will work:
458
+
459
+ ```bash
460
+ # 1. analytics:post returns {"missing": true}
461
+ postemi analytics:post <post-id>
462
+
463
+ # 2. Get available content from the provider
464
+ postemi posts:missing <post-id>
465
+ # Returns: [{"id": "7321456789012345678", "url": "https://...cover.jpg"}, ...]
466
+
467
+ # 3. Connect the correct content to the post
468
+ postemi posts:connect <post-id> --release-id "7321456789012345678"
469
+
470
+ # 4. Now analytics will work
471
+ postemi analytics:post <post-id>
472
+ ```
473
+
474
+ ### Connecting Missing Posts
475
+
476
+ Some platforms (e.g. TikTok) don't return a post ID immediately after publishing. When this happens, the post's `releaseId` is set to `"missing"` and analytics are unavailable until resolved.
477
+
478
+ ```bash
479
+ # List recent content from the provider for a post with missing release ID
480
+ postemi posts:missing <post-id>
481
+
482
+ # Connect a post to its published content
483
+ postemi posts:connect <post-id> --release-id "<content-id>"
484
+ ```
485
+
486
+ Returns an empty array if the provider doesn't support this feature or if the post doesn't have a missing release ID.
487
+
488
+ ### Media Upload
489
+
490
+ **⚠️ IMPORTANT:** Always upload files to Postemi before using them in posts. Many platforms (TikTok, Instagram, YouTube) **require verified URLs** and will reject external links.
491
+
492
+ ```bash
493
+ # Upload file and get URL
494
+ postemi upload image.jpg
495
+
496
+ # EXACTLY EIGHT TYPES ARE ACCEPTED. Anything else returns 400 "Unsupported
497
+ # file type.". The server sniffs the magic bytes, so renaming the file does
498
+ # not help.
499
+ #
500
+ # image/jpeg image/png image/gif image/webp
501
+ # image/avif image/bmp image/tiff (max 10 MB)
502
+ # video/mp4 (max 1 GB)
503
+ #
504
+ # NOT accepted: MOV, MKV, WEBM, AVI, SVG, PDF, DOC/DOCX, and every audio
505
+ # format. Convert first: ffmpeg -i clip.mov -c:v libx264 -c:a aac clip.mp4
506
+ # See SUPPORTED_FILE_TYPES.md.
507
+
508
+ # Workflow: Upload → Extract URL → Use in post
509
+ VIDEO=$(postemi upload video.mp4)
510
+ VIDEO_PATH=$(echo "$VIDEO" | jq -r '.path')
511
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" -m "$VIDEO_PATH" -i "tiktok-id"
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Common Patterns
517
+
518
+ ### Pattern 1: Discover & Use Integration Tools
519
+
520
+ **Most supported channels have no tools at all.** Of the channels in the table
521
+ at the top of this file, four expose a tool: Instagram (`audioSearch`),
522
+ Discord (`channels`), Slack (`channels`) and Pinterest (`boards`). X, Facebook, Threads, Bluesky,
523
+ YouTube, LinkedIn, TikTok and Telegram expose none: `integrations:settings`
524
+ returns an empty `tools` array for them, and `integrations:trigger` on them
525
+ returns `404 Tool not found`.
526
+
527
+ **Never guess a method name.** Read the `tools` array from
528
+ `integrations:settings` first; the `methodName` there is the only string
529
+ `integrations:trigger` will accept.
530
+
531
+ ```bash
532
+ IG_ID=$(postemi integrations:list | jq -r '.[] | select(.identifier=="instagram") | .id')
533
+
534
+ # What can this channel actually do?
535
+ postemi integrations:settings "$IG_ID" | jq '.output.tools'
536
+ # [ { "methodName": "audioSearch", "description": "Search audio …", "dataSchema": [ … ] } ]
537
+ ```
538
+
539
+ **Instagram, find audio for a Reel:**
540
+ ```bash
541
+ # Empty query returns trending audio. type is "music" (default) or "original_sound".
542
+ AUDIO=$(postemi integrations:trigger "$IG_ID" audioSearch -d '{"q":"lofi","type":"music"}')
543
+ AUDIO_ID=$(echo "$AUDIO" | jq -r '.output[0].id')
544
+
545
+ VIDEO=$(postemi upload reel.mp4 | jq -r '.path')
546
+ postemi posts:create \
547
+ -c "Reel caption" \
548
+ -s "2026-12-31T12:00:00Z" \
549
+ --settings "{\"post_type\":\"post\",\"audio\":{\"id\":\"$AUDIO_ID\"}}" \
550
+ -m "$VIDEO" \
551
+ -i "$IG_ID"
552
+ ```
553
+
554
+ ### Pattern 2: Upload Media Before Posting
555
+
556
+ ```bash
557
+ # Upload multiple files
558
+ VIDEO_RESULT=$(postemi upload video.mp4)
559
+ VIDEO_PATH=$(echo "$VIDEO_RESULT" | jq -r '.path')
560
+
561
+ THUMB_RESULT=$(postemi upload thumbnail.jpg)
562
+ THUMB_PATH=$(echo "$THUMB_RESULT" | jq -r '.path')
563
+
564
+ # Use in post
565
+ postemi posts:create \
566
+ -c "Check out my video!" \
567
+ -s "2026-12-31T12:00:00Z" \
568
+ -m "$VIDEO_PATH" \
569
+ -i "tiktok-id"
570
+ ```
571
+
572
+ ### Pattern 3: Twitter Thread
573
+
574
+ ```bash
575
+ # Upload every image first (Rule 2)
576
+ INTRO=$(postemi upload intro.jpg | jq -r '.path')
577
+ P1=$(postemi upload point1.jpg | jq -r '.path')
578
+ P2=$(postemi upload point2.jpg | jq -r '.path')
579
+ OUTRO=$(postemi upload outro.jpg | jq -r '.path')
580
+
581
+ postemi posts:create \
582
+ -c "🧵 Thread starter (1/4)" -m "$INTRO" \
583
+ -c "Point one (2/4)" -m "$P1" \
584
+ -c "Point two (3/4)" -m "$P2" \
585
+ -c "Conclusion (4/4)" -m "$OUTRO" \
586
+ -s "2026-12-31T12:00:00Z" \
587
+ -d 5 \
588
+ -i "$X_ID"
589
+ # -d is the delay between comments in MINUTES (here: 5 minutes)
590
+ ```
591
+
592
+ ### Pattern 4: Multi-Platform Campaign (different content per channel)
593
+
594
+ JSON mode takes the API's real request body: `type`, `date`, `shortLink`,
595
+ `tags`, and a `posts` array where each element names an `integration`, its
596
+ `value` thread and its `settings`. There is no `"integrations"` key and no
597
+ `"provider"` key; a full working file is
598
+ [examples/multi-platform-post.json](./examples/multi-platform-post.json).
599
+
600
+ ```bash
601
+ IMG=$(postemi upload sale.jpg | jq -r '.path')
602
+
603
+ cat > campaign.json << EOF
604
+ {
605
+ "type": "schedule",
606
+ "date": "2026-12-01T09:00:00Z",
607
+ "shortLink": false,
608
+ "tags": [],
609
+ "posts": [
610
+ {
611
+ "integration": { "id": "<x-id>" },
612
+ "value": [
613
+ { "content": "Holiday sale! 🎄 20% off every plan. #sale", "image": [ { "id": "a", "path": "$IMG" } ] }
614
+ ],
615
+ "settings": { "who_can_reply_post": "everyone" }
616
+ },
617
+ {
618
+ "integration": { "id": "<facebook-id>" },
619
+ "value": [
620
+ { "content": "Our holiday sale is live, 20% off every plan until the end of December. Details on the website!", "image": [ { "id": "b", "path": "$IMG" } ] }
621
+ ],
622
+ "settings": { "post_type": "post" }
623
+ }
624
+ ]
625
+ }
626
+ EOF
627
+
628
+ postemi posts:create --json campaign.json
629
+ ```
630
+
631
+ ### Pattern 5: Validate Settings Before Posting
632
+
633
+ ```bash
634
+ #!/bin/bash
635
+
636
+ INTEGRATION_ID="$X_ID"
637
+ CONTENT="Your post content here"
638
+
639
+ # Get integration settings and extract max length
640
+ SETTINGS_JSON=$(postemi integrations:settings "$INTEGRATION_ID")
641
+ MAX_LENGTH=$(echo "$SETTINGS_JSON" | jq '.output.maxLength')
642
+
643
+ # Check character limit and truncate if needed
644
+ if [ ${#CONTENT} -gt "$MAX_LENGTH" ]; then
645
+ echo "Content exceeds $MAX_LENGTH chars, truncating..."
646
+ CONTENT="${CONTENT:0:$((MAX_LENGTH - 3))}..."
647
+ fi
648
+
649
+ # Create post with settings
650
+ postemi posts:create \
651
+ -c "$CONTENT" \
652
+ -s "2026-12-31T12:00:00Z" \
653
+ --settings '{"key": "value"}' \
654
+ -i "$INTEGRATION_ID"
655
+ ```
656
+
657
+ ### Pattern 6: Batch Scheduling
658
+
659
+ ```bash
660
+ #!/bin/bash
661
+
662
+ # Schedule posts for the week
663
+ DATES=(
664
+ "2026-02-14T09:00:00Z"
665
+ "2026-02-15T09:00:00Z"
666
+ "2026-02-16T09:00:00Z"
667
+ )
668
+
669
+ CONTENT=(
670
+ "Monday motivation 💪"
671
+ "Tuesday tips 💡"
672
+ "Wednesday wisdom 🧠"
673
+ )
674
+
675
+ for i in "${!DATES[@]}"; do
676
+ # Rule 2: upload each file before passing to -m
677
+ IMG=$(postemi upload "post-${i}.jpg" | jq -r '.path')
678
+ postemi posts:create \
679
+ -c "${CONTENT[$i]}" \
680
+ -s "${DATES[$i]}" \
681
+ -i "$X_ID" \
682
+ -m "$IMG"
683
+ echo "Scheduled: ${CONTENT[$i]} for ${DATES[$i]}"
684
+ done
685
+ ```
686
+
687
+ ### Pattern 7: Error Handling & Retry
688
+
689
+ `posts:create` already retries for you: every run sends an `Idempotency-Key`
690
+ (a new UUID per run) and, after a network failure or a 5xx, retries ONCE with
691
+ the same key, so the server hands back the post it already made instead of
692
+ making a second one. Running the command again is a NEW key and a NEW post.
693
+ So never wrap `posts:create` in a retry loop; after a failure, look first:
694
+
695
+ ```bash
696
+ #!/bin/bash
697
+
698
+ CONTENT="Your post content"
699
+ INTEGRATION_ID="$X_ID"
700
+ DATE="2026-12-31T12:00:00Z"
701
+
702
+ if postemi posts:create -c "$CONTENT" -s "$DATE" -i "$INTEGRATION_ID"; then
703
+ echo "Post created successfully"
704
+ else
705
+ # Did it get through anyway? Check before running it again.
706
+ postemi posts:list --startDate "$DATE" --endDate "$DATE"
707
+ fi
708
+ ```
709
+
710
+ A 403 that names a missing permission is not retryable: the key is valid
711
+ and lacks the scope. A 401 saying the workspace has no plan with API access
712
+ is not retryable either: see `billing:plans`.
713
+
714
+ ---
715
+
716
+ ## Technical Concepts
717
+
718
+ ### Integration Tools Workflow
719
+
720
+ Some integrations expose a tool for data that cannot be hard-coded. Among supported channels Instagram, Discord and Slack do. The workflow:
721
+
722
+ 1. **Check available tools** - `integrations:settings` returns a `tools` array
723
+ 2. **Review tool schema** - Each tool has `methodName`, `description`, and `dataSchema`
724
+ 3. **Trigger tool** - Call `integrations:trigger` with required parameters
725
+ 4. **Use output** - Tool returns data to use in post settings
726
+
727
+ **Tools on supported channels, exhaustively:**
728
+
729
+ | Channel | Tools |
730
+ |---|---|
731
+ | Instagram (`instagram`) | `audioSearch` |
732
+ | Discord (`discord`) | `channels` (the server's channels; the `id` goes into `settings.channel`) |
733
+ | Slack (`slack`) | `channels` (the workspace's channels; the `id` goes into `settings.channel`) |
734
+ | X, Facebook, Threads, Bluesky, YouTube, LinkedIn, TikTok, Telegram | *(none)* |
735
+
736
+ That is the whole list. There is no `getPlaylists`, no `getCompanies`, no
737
+ `getBoards`, no `getFlairs`. Earlier versions of this file invented all four,
738
+ and `integrations:trigger` answers `404 Tool not found` for every one of them.
739
+
740
+ ### Provider Settings Structure
741
+
742
+ Platform-specific settings use a discriminator pattern with `__type` field:
743
+
744
+ ```json
745
+ {
746
+ "posts": [
747
+ {
748
+ "provider": "youtube",
749
+ "post": [{ "content": "Video description", "image": [...] }],
750
+ "settings": {
751
+ "__type": "youtube",
752
+ "title": "Video title",
753
+ "type": "public",
754
+ "selfDeclaredMadeForKids": "no",
755
+ "tags": [{ "value": "tech", "label": "Tech" }]
756
+ }
757
+ }
758
+ ]
759
+ }
760
+ ```
761
+
762
+ Pass settings directly:
763
+ ```bash
764
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" --settings '{"title":"…","type":"public"}' -i "youtube-id"
765
+ # Backend automatically adds "__type" based on integration ID
766
+ ```
767
+
768
+ Channels with **no settings of their own** (Threads and Bluesky) take an
769
+ empty object. Do not invent fields for them.
770
+
771
+ **You never have to write `__type` yourself.** The server stamps it from the
772
+ integration's provider before validating, overwriting whatever you sent. An
773
+ example file carrying `"__type": "EmptySettings"` still works for that reason:
774
+ it is being replaced, not accepted.
775
+
776
+ ### Two behaviours that surprise people
777
+
778
+ **1. A key without `posts:publish` silently produces a draft.** `posts:create`
779
+ requires only `posts:draft`. If the key (or its owner's current role, a
780
+ Contributor/DRAFTER) lacks `posts:publish`, the server coerces `type` to
781
+ `draft` and creates a draft instead of refusing. It is not silent in the
782
+ response: **read back the `type` you got, do not assume the one you sent.**
783
+ If the user wanted it published, tell them the key cannot, rather than
784
+ reporting success.
785
+
786
+ **2. One X channel per post, always.** A single post may not target two X
787
+ channels, whatever the content. X's Developer Policy forbids substantially
788
+ similar content from multiple accounts through one developer app, and the
789
+ account at risk is Postemi's, shared by every customer. Split it into two posts.
790
+ X alongside Facebook, Instagram, Threads, Bluesky, YouTube and the rest is
791
+ unaffected. That combination is the entire point.
792
+
793
+ ### Comments and Threading
794
+
795
+ Posts can have comments (threads on Twitter/X, replies elsewhere). Each comment can have its own media:
796
+
797
+ ```bash
798
+ # Upload every file first (Rule 2)
799
+ I1=$(postemi upload image1.jpg | jq -r '.path')
800
+ I2=$(postemi upload image2.jpg | jq -r '.path')
801
+ CI=$(postemi upload comment-img.jpg | jq -r '.path')
802
+ A1=$(postemi upload another.jpg | jq -r '.path')
803
+ A2=$(postemi upload more.jpg | jq -r '.path')
804
+
805
+ postemi posts:create \
806
+ -c "Main post" -m "$I1,$I2" \
807
+ -c "Comment 1" -m "$CI" \
808
+ -c "Comment 2" -m "$A1,$A2" \
809
+ -s "2026-12-31T12:00:00Z" \
810
+ -d 5 \ # Delay between comments in minutes
811
+ -i "integration-id"
812
+ ```
813
+
814
+ Internally creates (note: every URL is a Postemi-uploaded `.path`, not a raw filename):
815
+ ```json
816
+ {
817
+ "posts": [{
818
+ "value": [
819
+ { "content": "Main post", "image": ["<uploaded image1>", "<uploaded image2>"] },
820
+ { "content": "Comment 1", "image": ["<uploaded comment-img>"], "delay": 5 },
821
+ { "content": "Comment 2", "image": ["<uploaded another>", "<uploaded more>"], "delay": 5 }
822
+ ]
823
+ }]
824
+ }
825
+ ```
826
+
827
+ ### Date Handling
828
+
829
+ **The contract: ALWAYS pass an explicit offset, or pass `--timezone` with an
830
+ IANA name.** The CLI never guesses a timezone. The server reads a naive
831
+ datetime as UTC, which silently shifts a local "12:00" by the user's UTC
832
+ offset. The CLI refuses ambiguity instead.
833
+
834
+ The two safe forms:
835
+
836
+ ```bash
837
+ -s "2026-12-31T12:00:00Z" # explicit UTC
838
+ -s "2026-12-31 12:00" --timezone Europe/London # wall-clock + IANA zone
839
+ ```
840
+
841
+ How a date is resolved, in priority order:
842
+
843
+ 1. **Explicit offset in the string** (`Z`, `+01:00`), used as written.
844
+ 2. **`--timezone <IANA name>`** on `posts:create` / `posts:list`.
845
+ 3. **The saved config**: `postemi config:set timezone Europe/London`
846
+ (also offered interactively during `postemi auth:login` on a TTY).
847
+ 4. **None of the above and the date is naive → hard error, exit 1.** The
848
+ error names the fixes. Do not retry the same naive date; add a
849
+ timezone.
850
+
851
+ Rules and behaviors:
852
+
853
+ - Timezone values must be IANA names (`Europe/London`, `UTC`). **Numeric
854
+ offsets (`+02:00`, `UTC+2`, `Etc/GMT+2`) are rejected as timezone values**.
855
+ Never hand-compute DST; put an offset inside the date string only when
856
+ you are certain of it, otherwise use the IANA name and let the CLI convert
857
+ (DST-aware).
858
+ - Whenever a timezone is applied (steps 2 to 4), the CLI echoes the resolved
859
+ UTC instant on stderr:
860
+ `ℹ️ Interpreted "2026-12-24 18:00" as Europe/London (via --timezone) → 2026-12-24T18:00:00.000Z`
861
+ - A bare date (`"2026-12-31"`) means midnight in the resolved timezone.
862
+ - `posts:list --startDate/--endDate` accept the same shapes and the same
863
+ `--timezone` flag; the defaults (30 days back / 30 days forward) never
864
+ need a timezone.
865
+ - `postemi config:get` / `postemi config:get timezone` shows what is saved;
866
+ settings survive `auth:logout`.
867
+ - `postemi posts:find-slot <integration-id>` returns `{"date": "..."}`, the
868
+ next free slot on that channel's schedule; pass it straight back to
869
+ `posts:create -s`.
870
+ - `-t now` publishes immediately and needs no `--date`.
871
+
872
+ ### Media Upload Response
873
+
874
+ Upload returns JSON with path and metadata:
875
+ ```json
876
+ {
877
+ "path": "https://…/uploads/2026/08/abc123.jpg",
878
+ "size": 123456,
879
+ "type": "image/jpeg"
880
+ }
881
+ ```
882
+
883
+ Extract path for use in posts:
884
+ ```bash
885
+ RESULT=$(postemi upload image.jpg)
886
+ PATH=$(echo "$RESULT" | jq -r '.path')
887
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" -m "$PATH" -i "integration-id"
888
+ ```
889
+
890
+ ### JSON Mode vs CLI Flags
891
+
892
+ **CLI flags** - Quick posts:
893
+ ```bash
894
+ postemi posts:create -c "Content" -m "$(postemi upload img.jpg | jq -r '.path')" -s "2026-12-31T12:00:00Z" -i "$X_ID"
895
+ ```
896
+
897
+ **JSON mode** - Complex posts with multiple platforms and settings:
898
+ ```bash
899
+ postemi posts:create --json post.json
900
+ ```
901
+
902
+ JSON mode supports:
903
+ - Multiple platforms with different content per platform
904
+ - Complex provider-specific settings
905
+ - Scheduled posts
906
+ - Posts with many comments
907
+ - Custom delay between comments
908
+
909
+ ---
910
+
911
+ ## Platform-Specific Examples
912
+
913
+ Every field below is taken from the server's validation DTOs. A field that is
914
+ not listed does not exist, and sending it is a `400`.
915
+
916
+ ### X (Twitter): `x`
917
+ ```bash
918
+ postemi posts:create \
919
+ -c "Tweet content" \
920
+ -s "2026-12-31T12:00:00Z" \
921
+ --settings '{"who_can_reply_post":"everyone"}' \
922
+ -i "$X_ID"
923
+ ```
924
+ `who_can_reply_post` is **required**: `everyone` | `following` |
925
+ `mentionedUsers` | `subscribers` | `verified`. Optional: `community` (must
926
+ match `https://x.com/i/communities/<digits>`), `made_with_ai` (bool),
927
+ `paid_partnership` (bool).
928
+
929
+ ### Facebook: `facebook`
930
+ ```bash
931
+ IMG=$(postemi upload photo.jpg | jq -r '.path')
932
+ postemi posts:create \
933
+ -c "Post text" \
934
+ -s "2026-12-31T12:00:00Z" \
935
+ --settings '{"post_type":"post"}' \
936
+ -m "$IMG" \
937
+ -i "$FB_ID"
938
+ ```
939
+ All optional: `post_type` (`post` | `story`), `url`, `text_format_preset_id`
940
+ (a background for a **text-only** post; Pages only, ~130 characters max).
941
+
942
+ ### Instagram: `instagram`
943
+ ```bash
944
+ IMG=$(postemi upload image.jpg | jq -r '.path')
945
+
946
+ # Feed post
947
+ postemi posts:create -c "Caption #hashtag" -s "2026-12-31T12:00:00Z" \
948
+ --settings '{"post_type":"post"}' -m "$IMG" -i "$IG_ID"
949
+
950
+ # Story
951
+ STORY=$(postemi upload story.jpg | jq -r '.path')
952
+ postemi posts:create -c "" -s "2026-12-31T12:00:00Z" \
953
+ --settings '{"post_type":"story"}' -m "$STORY" -i "$IG_ID"
954
+ ```
955
+ `post_type` is **required**: `post` | `story`. Optional: `collaborators` (array
956
+ of `{label}`), `audio` (`{id, title?, artist?, image?, audio_volume?,
957
+ video_volume?}`; get the `id` from the `audioSearch` tool), `is_trial_reel`,
958
+ `graduation_strategy` (`MANUAL` | `SS_PERFORMANCE`).
959
+
960
+ ### Threads (`threads`) and Bluesky (`bluesky`)
961
+ ```bash
962
+ postemi posts:create -c "Post text" -s "2026-12-31T12:00:00Z" -i "$THREADS_ID"
963
+ ```
964
+ No provider settings. Omit `--settings` entirely.
965
+
966
+ ## What Postemi does NOT do
967
+
968
+ State these plainly rather than attempting a workaround.
969
+
970
+ - **No AI video generation.** The `/generate-video` and `/video/function`
971
+ routes were removed from the API entirely (2026-08-06). Do not tell a user
972
+ Postemi can generate video, and do not call those routes: they 404.
973
+ - **No image or video generation of any kind.** Media comes from files the
974
+ user already has, via `postemi upload`.
975
+ - **No channel deletion.** `DELETE /integrations/:id` was deliberately removed
976
+ from the public API. Disconnecting a channel is a signed-in, admin action in
977
+ the web app.
978
+ - **Nothing outside the channel table at the top of this file.**
979
+
980
+ ---
981
+
982
+ ## Supporting Resources
983
+
984
+ **This file is the command reference.** Everything the CLI accepts is
985
+ documented above; there is no second syntax guide to consult.
986
+
987
+ **Deep-dive documentation** (shipped in the npm package):
988
+ - [PROVIDER_SETTINGS.md](./PROVIDER_SETTINGS.md) - Settings schema for every supported channel
989
+ - [SUPPORTED_FILE_TYPES.md](./SUPPORTED_FILE_TYPES.md) - The eight accepted media types
990
+
991
+ **Ready-to-use `--json` payloads** (all verified against the current CLI):
992
+ - [examples/post-with-comments.json](./examples/post-with-comments.json) - Threading example
993
+ - [examples/thread-post.json](./examples/thread-post.json) - Multi-part thread
994
+ - [examples/multi-platform-post.json](./examples/multi-platform-post.json) - One post, several channels
995
+ - [examples/multi-platform-with-settings.json](./examples/multi-platform-with-settings.json) - Campaign example
996
+ - [examples/youtube-video.json](./examples/youtube-video.json) - YouTube with tags
997
+ - [examples/tiktok-video.json](./examples/tiktok-video.json) - TikTok with privacy
998
+
999
+ ---
1000
+
1001
+ ## Common Gotchas
1002
+
1003
+ 1. **Not authenticated** - Ask the user to run `postemi auth:login` and approve it in the browser
1004
+ 2. **Invalid integration ID** - Run `integrations:list` to get current IDs
1005
+ 3. **Settings schema mismatch** - Check `integrations:settings` for required fields
1006
+ 4. **Media MUST be uploaded to Postemi first** - ⚠️ **CRITICAL (Rule 2):** Every value passed to `-m` or to an `image`/media field in JSON mode must be a `.path` returned by `postemi upload`. Raw local filenames (`image.jpg`) and external URLs (`https://...`) will be rejected. TikTok, Instagram, YouTube and most other providers only accept Postemi-verified URLs. No exceptions: even a "quick test post" needs the upload step.
1007
+ 5. **JSON escaping in shell** - Use single quotes for JSON: `--settings '{...}'`
1008
+ 6. **Date format** - ISO 8601 with an explicit offset (`"2026-12-31T12:00:00Z"`), or a naive datetime plus `--timezone <IANA name>`. A naive datetime with no timezone configured is a hard error. Required except with `-t now` or `--json`.
1009
+ 7. **Tool not found** - Check available tools in `integrations:settings` output
1010
+ 8. **Character limits** - Each platform has different limits, check `maxLength` in settings
1011
+ 9. **Required settings** - YouTube requires `title` and `type`; X requires `who_can_reply_post`; Instagram requires `post_type`; TikTok requires `privacy_level` and `content_posting_method`; Discord and Slack require `channel` (an id from their `channels` tool). Threads, Bluesky, LinkedIn and Telegram require nothing.
1012
+ 10. **Media MIME types** - CLI auto-detects from file extension, ensure correct extension
1013
+ 11. **Analytics returns `{"missing": true}`** - The post was published but the platform didn't return a post ID. Run `posts:missing <post-id>` to get available content, then `posts:connect <post-id> --release-id "<id>"` to link it. Analytics will work after connecting.
1014
+
1015
+ ---
1016
+
1017
+ ## Quick Reference
1018
+
1019
+ ```bash
1020
+ # ⚠️ AUTHENTICATE FIRST - required before any other command
1021
+ postemi auth:status # Check if authenticated
1022
+ postemi auth:login # OAuth2 device flow login
1023
+ postemi auth:signup --email <address> # New account by e-mailed code; no free trial
1024
+ postemi auth:logout # Remove credentials
1025
+
1026
+ # Settings (survive logout)
1027
+ postemi config:set timezone Europe/London # Timezone for dates without an offset
1028
+ postemi config:get # Show saved settings as JSON
1029
+
1030
+ # Discovery (only after auth is confirmed)
1031
+ postemi integrations:list # Get integration IDs
1032
+ postemi integrations:list --group <group-id> # Get integration IDs in a group
1033
+ postemi integrations:groups # List groups (customers)
1034
+ postemi integrations:settings <id> # Get settings schema
1035
+ postemi integrations:trigger <id> <method> -d '{}' # Fetch dynamic data
1036
+
1037
+ # Posting (date required unless -t now or --json; explicit offset or --timezone, never naive)
1038
+ postemi posts:create -c "text" -s "2026-12-31T12:00:00Z" -i "id" # Simple (UTC)
1039
+ postemi posts:create -c "Good morning!" -s "2026-12-31 08:00" --timezone Europe/London -i "id" # Local wall-clock
1040
+ postemi posts:create -c "text" -t now -i "id" # Publish immediately
1041
+ postemi posts:create -c "text" -s "2026-12-31T12:00:00Z" -t draft -i "id" # Draft
1042
+ postemi posts:create -c "text" -m "$(postemi upload img.jpg | jq -r '.path')" -s "2026-12-31T12:00:00Z" -i "id" # With media (upload first, Rule 2)
1043
+ postemi posts:create -c "main" -c "comment" -s "2026-12-31T12:00:00Z" -i "id" # With comment
1044
+ postemi posts:create -c "text" -s "2026-12-31T12:00:00Z" --settings '{}' -i "id" # Platform-specific
1045
+ postemi posts:create --json file.json # Complex
1046
+
1047
+ # Management
1048
+ postemi posts:list # List posts
1049
+ postemi posts:find-slot <integration-id> # Next free slot: {"date": "..."}
1050
+ postemi posts:delete <id> # Delete one post (one channel)
1051
+ postemi posts:delete-group <group> # Delete every channel of a group
1052
+ postemi posts:status <id> --status draft # Move to draft (stops workflow)
1053
+ postemi posts:status <id> --status schedule # Queue draft for publishing
1054
+ postemi upload <file> # Upload media
1055
+
1056
+ # Analytics
1057
+ postemi analytics:platform <id> # Platform analytics (7 days)
1058
+ postemi analytics:platform <id> -d 30 # Platform analytics (30 days)
1059
+ postemi analytics:post <id> # Post analytics (7 days)
1060
+ postemi analytics:post <id> -d 30 # Post analytics (30 days)
1061
+ # If analytics:post returns {"missing": true}, resolve it:
1062
+ postemi posts:missing <id> # List provider content
1063
+ postemi posts:connect <id> --release-id "<rid>" # Connect content to post
1064
+
1065
+ # Help
1066
+ postemi --help # Show help
1067
+ postemi posts:create --help # Command help
1068
+ ```