postemi 0.0.0-stage → 1.0.1

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.
@@ -0,0 +1,374 @@
1
+ # Provider-specific settings
2
+
3
+ Every channel Postemi supports, and every field its `--settings` payload accepts.
4
+
5
+ **The channels below are the whole list.** Postemi's server carries inherited
6
+ code for many other providers — Reddit, Mastodon, Medium, Dev.to, Hashnode,
7
+ WordPress, Lemmy, Nostr, VK, Tumblr, Dribbble, Farcaster and others.
8
+ None of them are enabled or supported. Do not build a settings payload for one.
9
+
10
+ | Channel | `__type` | Has settings? | Status |
11
+ |---|---|---|---|
12
+ | X (Twitter) | `x` | yes | Publishing verified |
13
+ | Facebook | `facebook` | yes, all optional | Publishing verified |
14
+ | Instagram | `instagram` | yes | Publishing verified |
15
+ | Threads | `threads` | **no** | Publishing verified |
16
+ | Bluesky | `bluesky` | **no** | Publishing verified |
17
+ | LinkedIn | `linkedin` | yes, all optional | Publishing verified |
18
+ | Telegram | `telegram` | **no** | Publishing verified |
19
+ | Discord | `discord` | yes, `channel` required | Publishing verified |
20
+ | Slack | `slack` | yes, `channel` required | Publishing verified |
21
+ | YouTube | `youtube` | yes | Publishing verified |
22
+ | TikTok | `tiktok` | yes | Publishing verified |
23
+ | Pinterest | `pinterest` | yes, `board` required | BETA: pins visible only to their creator until Pinterest grants Standard access |
24
+
25
+ Source of truth for everything on this page:
26
+ `libraries/nestjs-libraries/src/dtos/posts/providers-settings/*.ts` in the Postemi
27
+ server. If this file and a DTO disagree, the DTO is right — and the
28
+ `integrations:settings` route serves those DTOs as live JSON schema, so you can
29
+ always ask instead of trusting a document:
30
+
31
+ ```bash
32
+ postemi integrations:settings <integration-id> | jq '.output.settings'
33
+ ```
34
+
35
+ ---
36
+
37
+ ## How to pass settings
38
+
39
+ ### Command line
40
+
41
+ ```bash
42
+ postemi posts:create \
43
+ -c "Your content" \
44
+ -s "2026-12-31T12:00:00Z" \
45
+ --settings '<json>' \
46
+ -i "<integration-id>"
47
+ ```
48
+
49
+ The backend fills in `__type` from the integration's provider. You do not send
50
+ it on the command line.
51
+
52
+ ### JSON file
53
+
54
+ ```bash
55
+ postemi posts:create --json post.json
56
+ ```
57
+
58
+ ```json
59
+ {
60
+ "type": "schedule",
61
+ "date": "2026-12-31T12:00:00Z",
62
+ "shortLink": false,
63
+ "tags": [],
64
+ "posts": [{
65
+ "integration": { "id": "<youtube-integration-id>" },
66
+ "value": [{ "content": "Video description", "image": [{ "id": "…", "path": "https://…" }] }],
67
+ "settings": {
68
+ "__type": "youtube",
69
+ "title": "Video title",
70
+ "type": "public",
71
+ "tags": [{ "value": "tech", "label": "Tech" }]
72
+ }
73
+ }]
74
+ }
75
+ ```
76
+
77
+ You do not have to write `__type` even here: the server stamps it from the
78
+ integration's provider before validating, overwriting whatever you sent. Write
79
+ it for readability if you like — a stale or wrong value cannot break the
80
+ request, because it is replaced, not checked.
81
+
82
+ **Two behaviours worth knowing before you publish:**
83
+
84
+ - **A key without the `posts:publish` scope produces a draft, not a refusal.**
85
+ `posts:create` needs only `posts:draft`; without `posts:publish` the server
86
+ coerces `type` to `draft` and creates one. Read back the `type` in the
87
+ response rather than assuming the one you sent.
88
+ - **One X channel per post.** A single post may not target two X channels,
89
+ whatever the content — X's Developer Policy forbids it and the developer
90
+ account at risk is Postemi's, shared by every customer. Any other combination
91
+ of channels is fine.
92
+
93
+ ---
94
+
95
+ ## X (Twitter) — `x`
96
+
97
+ | Field | Required | Values |
98
+ |---|---|---|
99
+ | `who_can_reply_post` | **yes** | `everyone`, `following`, `mentionedUsers`, `subscribers`, `verified` |
100
+ | `community` | no | A community URL matching `https://x.com/i/communities/<digits>`, or empty string |
101
+ | `made_with_ai` | no | boolean |
102
+ | `paid_partnership` | no | boolean |
103
+
104
+ ```bash
105
+ postemi posts:create \
106
+ -c "Announcement" \
107
+ -s "2026-12-31T12:00:00Z" \
108
+ --settings '{"who_can_reply_post":"everyone"}' \
109
+ -i "$X_ID"
110
+ ```
111
+
112
+ A malformed `community` fails validation with the expected format in the
113
+ message. Anything other than the five reply values is a `400`.
114
+
115
+ ---
116
+
117
+ ## Facebook — `facebook`
118
+
119
+ All fields optional.
120
+
121
+ | Field | Values |
122
+ |---|---|
123
+ | `post_type` | `post`, `story` |
124
+ | `url` | A URL to attach |
125
+ | `text_format_preset_id` | A Facebook background preset id |
126
+
127
+ `text_format_preset_id` applies to **text-only posts on Pages** and caps at
128
+ about 130 characters. Facebook exposes no API to enumerate the presets, so the
129
+ server carries a hardcoded catalogue; the ids are opaque strings.
130
+
131
+ ```bash
132
+ IMG=$(postemi upload photo.jpg | jq -r '.path')
133
+ postemi posts:create -c "Post text" -s "2026-12-31T12:00:00Z" \
134
+ --settings '{"post_type":"post"}' -m "$IMG" -i "$FB_ID"
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Instagram — `instagram`
140
+
141
+ | Field | Required | Values |
142
+ |---|---|---|
143
+ | `post_type` | **yes** | `post`, `story` |
144
+ | `collaborators` | no | array of `{ "label": "<username>" }` |
145
+ | `audio` | no | `{ id, title?, artist?, image?, audio_volume?, video_volume? }` — volumes 0–100 |
146
+ | `is_trial_reel` | no | boolean |
147
+ | `graduation_strategy` | no | `MANUAL`, `SS_PERFORMANCE` |
148
+
149
+ `audio.id` comes from the `audioSearch` tool:
150
+
151
+ ```bash
152
+ postemi integrations:trigger "$IG_ID" audioSearch -d '{"q":"lofi","type":"music"}'
153
+ # type is "music" (default) or "original_sound"; an empty q returns trending audio
154
+ ```
155
+
156
+ ```bash
157
+ STORY=$(postemi upload story.jpg | jq -r '.path')
158
+ postemi posts:create -c "" -s "2026-12-31T12:00:00Z" \
159
+ --settings '{"post_type":"story"}' -m "$STORY" -i "$IG_ID"
160
+ ```
161
+
162
+ ---
163
+
164
+ ## Threads — `threads`
165
+
166
+ **No settings.** Omit `--settings`. There are no fields to send and inventing
167
+ one is a `400`.
168
+
169
+ ```bash
170
+ postemi posts:create -c "Post text" -s "2026-12-31T12:00:00Z" -i "$THREADS_ID"
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Bluesky — `bluesky`
176
+
177
+ **No settings.** Same as Threads.
178
+
179
+ ---
180
+
181
+ ## YouTube — `youtube`
182
+
183
+ | Field | Required | Values |
184
+ |---|---|---|
185
+ | `title` | **yes** | 2–100 characters |
186
+ | `type` | **yes** | `public`, `private`, `unlisted` |
187
+ | `selfDeclaredMadeForKids` | no | `yes`, `no` |
188
+ | `thumbnail` | no | A media object from `postemi upload` |
189
+ | `tags` | no | array of `{ value, label }` |
190
+
191
+ The post's `-c` content becomes the **video description**. `title` is separate
192
+ and is the video's title.
193
+
194
+ **The tag budget is 500 characters in total across every tag**, not per tag.
195
+ A tag containing whitespace costs two extra characters against that budget,
196
+ because YouTube wraps it in quotes. Over the budget is a `400` naming the
197
+ limit.
198
+
199
+ There is **no `playlistId` field** and no way to add a video to a playlist
200
+ through the API. Earlier documentation claimed otherwise.
201
+
202
+ ```bash
203
+ VIDEO=$(postemi upload video.mp4 | jq -r '.path')
204
+ postemi posts:create \
205
+ -c "Full video description…" \
206
+ -s "2026-12-31T12:00:00Z" \
207
+ --settings '{"title":"How to build a CLI","type":"public","selfDeclaredMadeForKids":"no","tags":[{"value":"tech","label":"Tech"},{"value":"tutorial","label":"Tutorial"}]}' \
208
+ -m "$VIDEO" \
209
+ -i "$YT_ID"
210
+ ```
211
+
212
+ Only MP4 uploads. See [SUPPORTED_FILE_TYPES.md](./SUPPORTED_FILE_TYPES.md).
213
+
214
+ ---
215
+
216
+ ## LinkedIn — `linkedin`
217
+
218
+ Both fields optional.
219
+
220
+ | Field | Values |
221
+ |---|---|
222
+ | `post_as_images_carousel` | boolean |
223
+ | `carousel_name` | string |
224
+
225
+ There is **no `companyId`**. Posting as a company page is a separate connected
226
+ channel with identifier `linkedin-page`, chosen at connect time — not a setting
227
+ on a personal post.
228
+
229
+ ```bash
230
+ postemi posts:create -c "Content" -s "2026-12-31T12:00:00Z" \
231
+ --settings '{"post_as_images_carousel":true,"carousel_name":"Product launch"}' \
232
+ -m "$A,$B,$C" -i "$LI_ID"
233
+ ```
234
+
235
+ ---
236
+
237
+ ## TikTok — `tiktok`
238
+
239
+ | Field | Required | Values |
240
+ |---|---|---|
241
+ | `title` | no | ≤ 90 characters |
242
+ | `privacy_level` | **yes for `DIRECT_POST`** | `PUBLIC_TO_EVERYONE`, `MUTUAL_FOLLOW_FRIENDS`, `FOLLOWER_OF_CREATOR`, `SELF_ONLY` |
243
+ | `content_posting_method` | **yes** | `DIRECT_POST`, `UPLOAD` |
244
+ | `duet` | yes | boolean — video only, `DIRECT_POST` only |
245
+ | `stitch` | yes | boolean — video only, `DIRECT_POST` only |
246
+ | `comment` | yes | boolean — `DIRECT_POST` only |
247
+ | `autoAddMusic` | yes | `yes`, `no` — photo posts only, `DIRECT_POST` only |
248
+ | `brand_content_toggle` | yes | boolean — `DIRECT_POST` only |
249
+ | `brand_organic_toggle` | yes | boolean — `DIRECT_POST` only |
250
+ | `video_made_with_ai` | no | boolean — video only, `DIRECT_POST` only |
251
+
252
+ The field is `privacy_level`. Earlier documentation called it `privacy`; that
253
+ name is silently ignored, which is worse than an error.
254
+
255
+ **`UPLOAD` does not publish.** It sends the media to the user's TikTok app
256
+ inbox as a draft, which they must finish and publish by hand within 24 hours or
257
+ it is discarded — and TikTok ignores every setting above except `title`. Only
258
+ use it when the user explicitly asks to finish the post inside the TikTok app.
259
+
260
+ **`privacy_level` deliberately has no default.** TikTok's Content Sharing
261
+ Guidelines require the person posting to choose it. Ask the user; do not pick
262
+ one on their behalf.
263
+
264
+ ```bash
265
+ VIDEO=$(postemi upload video.mp4 | jq -r '.path')
266
+ postemi posts:create \
267
+ -c "Caption #fyp" \
268
+ -s "2026-12-31T12:00:00Z" \
269
+ --settings '{"title":"Caption","privacy_level":"PUBLIC_TO_EVERYONE","duet":true,"stitch":true,"comment":true,"autoAddMusic":"no","brand_content_toggle":false,"brand_organic_toggle":false,"content_posting_method":"DIRECT_POST"}' \
270
+ -m "$VIDEO" \
271
+ -i "$TT_ID"
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Telegram — `telegram`
277
+
278
+ **No settings.** Same as Threads. The channel is fixed when the user connects
279
+ it in Postemi (one shared bot posts into the channel or group they chose).
280
+
281
+ ---
282
+
283
+ ## Discord — `discord`
284
+
285
+ | Field | Values |
286
+ |---|---|
287
+ | `channel` | **required** — a channel id from the `channels` tool |
288
+
289
+ Get the id first; a channel name is a `400`:
290
+
291
+ ```bash
292
+ postemi integrations:trigger "$DISCORD_ID" channels -d '{}' | jq '.output'
293
+ postemi posts:create -c "Release notes are out" -s "2026-12-31T12:00:00Z" \
294
+ --settings '{"channel":"123456789012345678"}' -i "$DISCORD_ID"
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Slack — `slack`
300
+
301
+ | Field | Values |
302
+ |---|---|
303
+ | `channel` | **required** — a channel id from the `channels` tool |
304
+
305
+ Same shape as Discord:
306
+
307
+ ```bash
308
+ postemi integrations:trigger "$SLACK_ID" channels -d '{}' | jq '.output'
309
+ postemi posts:create -c "Release notes are out" -s "2026-12-31T12:00:00Z" \
310
+ --settings '{"channel":"C0123456789"}' -i "$SLACK_ID"
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Pinterest — `pinterest`
316
+
317
+ **BETA.** Until Pinterest grants Postemi Standard access, a pin is visible only
318
+ to the account that created it. Tell the user before you schedule one.
319
+
320
+ | Field | Values |
321
+ |---|---|
322
+ | `board` | **required** — a board id from the `boards` tool |
323
+ | `title` | optional, up to 100 characters |
324
+ | `link` | optional, a URL the pin opens |
325
+
326
+ A pin needs media: one image (up to 5 images in one pin), or a video with a
327
+ cover picture as the second attachment. Get the board id first; a board name is
328
+ refused:
329
+
330
+ ```bash
331
+ postemi integrations:trigger "$PINTEREST_ID" boards | jq '.output'
332
+ IMG=$(postemi upload ./pin.jpg | jq -r '.path')
333
+ postemi posts:create -c "Autumn recipes" -m "$IMG" -s "2026-12-31T12:00:00Z" \
334
+ --settings '{"board":"1234567890123456789","title":"Autumn recipes","link":"https://example.com/recipes"}' \
335
+ -i "$PINTEREST_ID"
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Finding your integration ids
341
+
342
+ ```bash
343
+ postemi integrations:list
344
+ # [{ "id": "…", "name": "…", "identifier": "instagram", … }]
345
+
346
+ X_ID=$(postemi integrations:list | jq -r '.[] | select(.identifier=="x") | .id')
347
+ IG_ID=$(postemi integrations:list | jq -r '.[] | select(.identifier=="instagram")| .id')
348
+ YT_ID=$(postemi integrations:list | jq -r '.[] | select(.identifier=="youtube") | .id')
349
+ ```
350
+
351
+ `identifier` is the provider; `__type` in JSON mode is the same string.
352
+
353
+ If a channel is not in `integrations:list`, the user cannot post to it. For
354
+ Google Business Profile and LinkedIn company pages that is the expected state
355
+ until those platforms approve the app (personal LinkedIn profiles work).
356
+ Pinterest is live as a BETA channel. Say so rather than scheduling a post that will fail.
357
+
358
+ ---
359
+
360
+ ## Validating before you publish
361
+
362
+ Post as a draft first. A draft goes through the same validation as a scheduled
363
+ post, so a bad settings payload fails immediately instead of at publish time:
364
+
365
+ ```bash
366
+ postemi posts:create -c "…" -s "2026-12-31T12:00:00Z" -t draft --settings '…' -i "$ID"
367
+ ```
368
+
369
+ Then promote it, or delete it:
370
+
371
+ ```bash
372
+ postemi posts:status <post-id> --status schedule
373
+ postemi posts:delete <post-id>
374
+ ```
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # Postemi CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Social media scheduling from the command line, or from your AI agent.**
4
+
5
+ Postemi (https://postemi.com) is a social media scheduler, and this is its
6
+ command-line tool. It uses the same public API as the web app: what you
7
+ schedule here shows up in your Postemi calendar, and what you schedule there
8
+ you can see here too.
9
+
10
+ Channels: 20 platforms, including Facebook, Instagram, X, LinkedIn, TikTok, YouTube, Threads, Bluesky, Pinterest and Mastodon.
11
+
12
+ [`SKILL.md`](./SKILL.md) is the full command reference, written for AI agents.
13
+ Show it to Claude, ChatGPT, Codex, OpenClaw or Hermes and they can use the
14
+ whole CLI with no further explanation.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install -g postemi
20
+ postemi --help
21
+ ```
22
+
23
+ Node.js 18 or newer.
24
+
25
+ ## Sign in
26
+
27
+ ```bash
28
+ postemi auth:login # device flow: opens postemi.com/app/device in the browser
29
+ postemi auth:status # checks the credential
30
+ postemi auth:logout # deletes the stored credential
31
+ ```
32
+
33
+ You approve the login in the browser while signed in to Postemi, and choose
34
+ there which workspace and which channels the key may reach. The CLI stores it
35
+ in `~/.postemi/credentials.json` (mode `0600`).
36
+
37
+ No account yet? Create one from the terminal:
38
+
39
+ ```bash
40
+ postemi auth:signup --email anna@example.com # e-mails a code, asks for it and for the terms
41
+ postemi auth:signup --email anna@example.com --code 123456 --accept-terms --workspace-name "Anna's Cafe"
42
+ ```
43
+
44
+ The person must agree to https://postemi.com/terms and
45
+ https://postemi.com/privacy first. There is no free trial: next come
46
+ `postemi billing:plans` and `postemi billing:subscribe`. You can also sign up
47
+ at https://postemi.com/app/signup and then run `postemi auth:login`.
48
+
49
+ For a server, CI or an agent without a browser, use an API key from
50
+ Settings, Developers in the web app:
51
+
52
+ ```bash
53
+ export POSTEMI_API_KEY=your_api_key
54
+ ```
55
+
56
+ | Variable | Default | Purpose |
57
+ |---|---|---|
58
+ | `POSTEMI_API_KEY` | none | API key, when you do not use `auth:login` |
59
+ | `POSTEMI_API_URL` | `https://api.postemi.com` | API base |
60
+ | `POSTEMI_WORKSPACE` | none | Workspace id for every command (same as `--workspace`) |
61
+ | `POSTEMI_TIMEZONE` | none | IANA zone for dates written without an offset |
62
+ | `POSTEMI_AUTH_SERVER` | `https://api.postemi.com` | Where `auth:login` runs the device flow |
63
+ | `POSTEMI_CLIENT_NAME` | `Postemi CLI` | Name shown on the approval page |
64
+
65
+ Stored credentials from `auth:login` take precedence over `POSTEMI_API_KEY`.
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ postemi auth:status
71
+ postemi integrations:list # your channels and their ids
72
+ postemi config:set timezone Europe/London # once: how to read dates without an offset
73
+
74
+ IMG=$(postemi upload ./photo.jpg | jq -r '.path')
75
+ postemi posts:create -c "Hello from the terminal" -m "$IMG" \
76
+ -s "2026-12-31 09:00" -i "<integration-id>"
77
+ ```
78
+
79
+ Results are JSON on stdout; status lines and errors go to stderr, and every
80
+ failure exits 1. Run `postemi <command> --help` for each command.
81
+
82
+ ## More
83
+
84
+ - [`SKILL.md`](./SKILL.md): every command, flag and rule
85
+ - [`PROVIDER_SETTINGS.md`](./PROVIDER_SETTINGS.md): per-channel settings
86
+ - [`SUPPORTED_FILE_TYPES.md`](./SUPPORTED_FILE_TYPES.md): the eight accepted media types
87
+ - [`examples/`](./examples): ready `--json` payloads
88
+ - MCP server for chat clients: `https://mcp.postemi.com/mcp`
89
+ - Help: https://postemi.com/help, support: norbert@postemi.com
90
+ - Privacy: https://postemi.com/privacy, terms: https://postemi.com/terms
91
+
92
+ ## Plugins for AI agents
93
+
94
+ This repository also holds the Postemi plugins, in [`plugins/`](./plugins):
95
+
96
+ | Folder | For | What it is |
97
+ |---|---|---|
98
+ | [`plugins/postemi-mcp`](./plugins/postemi-mcp) | Claude (chat, desktop, Code) | The hosted MCP server `https://mcp.postemi.com/mcp` plus a skill |
99
+ | [`plugins/postemi-cli`](./plugins/postemi-cli) | Claude Code | This CLI's skill; the agent runs `postemi` commands |
100
+ | [`plugins/postemi-chatgpt`](./plugins/postemi-chatgpt) | ChatGPT | The ChatGPT app package |
101
+ | [`plugins/postemi-cursor`](./plugins/postemi-cursor) | Cursor | MCP server, skill and commands |
102
+ | [`plugins/postemi-grok`](./plugins/postemi-grok) | Grok Build | MCP server, skill and commands |
103
+
104
+ In Claude Code:
105
+
106
+ ```bash
107
+ claude plugin marketplace add norbertlevente/postemi-agent
108
+ claude plugin install postemi@postemi-agent # MCP server
109
+ claude plugin install postemi-cli@postemi-agent # CLI skill
110
+ ```
111
+
112
+ The skill on its own (for OpenClaw and other agents) is
113
+ [`skills/postemi/SKILL.md`](./skills/postemi/SKILL.md).
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ pnpm install
119
+ pnpm build # dist/index.js
120
+ pnpm test # type check, build, and scripts/check.mjs (manifests, skill copies, no other-brand text)
121
+ node dist/index.js --help
122
+ ```
123
+
124
+ Release steps: [`PUBLISHING.md`](./PUBLISHING.md). Changes: [`CHANGELOG.md`](./CHANGELOG.md).
125
+
126
+ License: AGPL-3.0, see [`LICENSE`](./LICENSE).
127
+ Source: https://github.com/norbertlevente/postemi-agent