@postaz/cli 0.1.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +21 -0
- package/README.md +81 -0
- package/SKILL.md +207 -0
- package/dist/index.js +569 -0
- package/package.json +52 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postaz-agent",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Postaz",
|
|
5
|
+
"url": "https://postaz.app"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "Postaz TikTok scheduling skill — publish and schedule TikTok posts from an AI agent",
|
|
9
|
+
"version": "0.1.0"
|
|
10
|
+
},
|
|
11
|
+
"plugins": [
|
|
12
|
+
{
|
|
13
|
+
"name": "postaz",
|
|
14
|
+
"description": "TikTok-first social scheduling CLI for AI agents — schedule and publish TikTok posts (video or photo slideshows), generate captions, and manage media.",
|
|
15
|
+
"source": "./",
|
|
16
|
+
"strict": false,
|
|
17
|
+
"skills": ["./"]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postaz",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TikTok-first social scheduling CLI for AI agents — schedule and publish TikTok posts (video or photo slideshows), generate captions, and manage media. JSON in, JSON out.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Postaz",
|
|
7
|
+
"url": "https://postaz.app"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://postaz.app",
|
|
10
|
+
"repository": "https://github.com/tonymanh-dev/postaz-agent",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"skills": ["./"],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"postaz",
|
|
15
|
+
"tiktok",
|
|
16
|
+
"social-media",
|
|
17
|
+
"scheduling",
|
|
18
|
+
"automation",
|
|
19
|
+
"ai-agent"
|
|
20
|
+
]
|
|
21
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Postaz CLI
|
|
2
|
+
|
|
3
|
+
> The cheapest way to grow your app on TikTok — from your terminal or an AI agent.
|
|
4
|
+
|
|
5
|
+
`postaz` is a thin, JSON-in/JSON-out command-line wrapper over the [Postaz](https://postaz.app)
|
|
6
|
+
public API. It lets you (or an AI agent) schedule and publish TikTok posts, upload media, and
|
|
7
|
+
generate captions without touching the web app.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @postaz/cli
|
|
13
|
+
# or
|
|
14
|
+
pnpm install -g @postaz/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js ≥ 20.
|
|
18
|
+
|
|
19
|
+
## Authenticate
|
|
20
|
+
|
|
21
|
+
Create an API key in your Postaz workspace settings (**API Keys → Create key**), then:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
postaz auth:login --api-key postaz_sk_xxx
|
|
25
|
+
# or
|
|
26
|
+
export POSTAZ_API_KEY=postaz_sk_xxx
|
|
27
|
+
|
|
28
|
+
postaz auth:status
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Credentials are stored at `~/.postaz/credentials.json` (mode `600`). The `POSTAZ_API_KEY`
|
|
32
|
+
environment variable takes priority. Point at a self-hosted backend with `POSTAZ_API_URL`.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Find your TikTok platform id
|
|
38
|
+
postaz platforms:list
|
|
39
|
+
|
|
40
|
+
# Upload media (required before posting — returns a READY object with an id)
|
|
41
|
+
MEDIA_ID=$(postaz upload launch.mp4 | jq -r '.id')
|
|
42
|
+
|
|
43
|
+
# Publish now
|
|
44
|
+
postaz posts:create -c "We just shipped 🚀 #buildinpublic" \
|
|
45
|
+
-m "$MEDIA_ID" -i "<platform-id>" --mode now
|
|
46
|
+
|
|
47
|
+
# Or schedule
|
|
48
|
+
postaz posts:create -c "Coming soon" -m "$MEDIA_ID" -i "<platform-id>" \
|
|
49
|
+
-s "2026-07-01T12:00:00Z"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
| Command | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `auth:login --api-key <key>` | Validate and store an API key |
|
|
57
|
+
| `auth:status` | Check the current key |
|
|
58
|
+
| `auth:logout` | Remove stored credentials |
|
|
59
|
+
| `platforms:list` | List connected accounts and their IDs |
|
|
60
|
+
| `platforms:schema <id>` | Posting limits + settings for a platform |
|
|
61
|
+
| `upload <file>` | Upload an image/video, returns a READY media object |
|
|
62
|
+
| `media:list` | List uploaded media |
|
|
63
|
+
| `media:folders:list` / `media:folders:create <name>` | Manage folders |
|
|
64
|
+
| `posts:create` | Create a post (`-c` content, `-i` platform ids, `-m` media ids, `--mode`, `-s`) |
|
|
65
|
+
| `posts:list` | List posts in a date range |
|
|
66
|
+
| `posts:get <id>` | Fetch one post group |
|
|
67
|
+
| `posts:reschedule <id> -s <iso>` | Change scheduled time |
|
|
68
|
+
| `posts:delete <id>` | Delete a post |
|
|
69
|
+
| `slides:content` | Generate AI captions for a slideshow (uses credits) |
|
|
70
|
+
| `slides:render` | Bake captions onto images (uses credits) |
|
|
71
|
+
|
|
72
|
+
Both `posts:create` and `posts create` syntaxes work. Run `postaz <command> --help` for details.
|
|
73
|
+
|
|
74
|
+
## Use as an AI agent skill
|
|
75
|
+
|
|
76
|
+
This repo ships a [`SKILL.md`](./SKILL.md) and a [`.claude-plugin`](./.claude-plugin) manifest so
|
|
77
|
+
agents can install and drive the CLI directly. See `SKILL.md` for the full agent playbook.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postaz
|
|
3
|
+
description: Postaz is the cheapest way to grow your app on TikTok. Schedule and publish TikTok posts (one video, or a 1–35 image photo slideshow), generate captions with AI, and manage media — all from the command line or an AI agent. JSON in, JSON out.
|
|
4
|
+
homepage: https://postaz.app
|
|
5
|
+
metadata: {"openclaw":{"emoji":"📱","requires":{"bins":[],"env":["POSTAZ_API_KEY"]}}}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Install Postaz if it doesn't exist
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @postaz/cli
|
|
12
|
+
# or
|
|
13
|
+
pnpm install -g @postaz/cli
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Property | Value |
|
|
17
|
+
|----------|-------|
|
|
18
|
+
| **name** | postaz |
|
|
19
|
+
| **description** | TikTok-first social scheduling CLI |
|
|
20
|
+
| **allowed-tools** | Bash(postaz:*) |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ⚠️ Two Hard Rules (Read First)
|
|
25
|
+
|
|
26
|
+
**Rule 1 — Authenticate before anything.** Every command needs a valid API key. Check with `postaz auth:status`.
|
|
27
|
+
|
|
28
|
+
**Rule 2 — Every file you post MUST be uploaded first.** TikTok only accepts Postaz-verified HTTPS URLs, never raw local paths or external links. The workflow is always: `postaz upload <file>` → take the returned `id` → pass it to `posts:create -m`. The upload returns a `READY` object in one call (no separate finalize step).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
MEDIA_ID=$(postaz upload video.mp4 | jq -r '.id')
|
|
32
|
+
postaz posts:create -c "caption" -m "$MEDIA_ID" -i "<platform-id>" --mode now
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Authentication
|
|
38
|
+
|
|
39
|
+
Get an API key from your Postaz workspace settings (API Keys → Create key). Keys look like `postaz_sk_…` and are shown once.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Option 1 — store it (saved to ~/.postaz/credentials.json, chmod 600)
|
|
43
|
+
postaz auth:login --api-key postaz_sk_xxx
|
|
44
|
+
|
|
45
|
+
# Option 2 — environment variable (takes priority over the stored file)
|
|
46
|
+
export POSTAZ_API_KEY=postaz_sk_xxx
|
|
47
|
+
|
|
48
|
+
# Verify
|
|
49
|
+
postaz auth:status
|
|
50
|
+
|
|
51
|
+
# Custom host (self-hosted backend)
|
|
52
|
+
export POSTAZ_API_URL=https://api.your-domain.com
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
API access is available on **every plan, including Free**. Per-plan limits
|
|
56
|
+
(posts/month, storage, connected accounts, AI credits) do the gating — a blocked
|
|
57
|
+
action returns a `409` with a specific code (e.g. `post_limit_reached`).
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Core Workflow
|
|
62
|
+
|
|
63
|
+
1. **Authenticate** — `postaz auth:status`
|
|
64
|
+
2. **Discover** — `postaz platforms:list` to get platform IDs; `platforms:schema <id>` for limits/settings
|
|
65
|
+
3. **Prepare media** — `postaz upload <file>` (required before posting)
|
|
66
|
+
4. **(Optional) Generate** — `postaz slides:content` for AI captions; `slides:render` to bake captions onto images
|
|
67
|
+
5. **Post** — `postaz posts:create …`
|
|
68
|
+
6. **Manage** — `postaz posts:list`, `posts:get`, `posts:reschedule`, `posts:delete`
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 1. Auth
|
|
72
|
+
postaz auth:status
|
|
73
|
+
|
|
74
|
+
# 2. Discover
|
|
75
|
+
postaz platforms:list
|
|
76
|
+
PLATFORM_ID=$(postaz platforms:list | jq -r '.platforms[] | select(.provider=="tiktok") | .id' | head -n1)
|
|
77
|
+
postaz platforms:schema "$PLATFORM_ID"
|
|
78
|
+
|
|
79
|
+
# 3. Prepare
|
|
80
|
+
MEDIA_ID=$(postaz upload promo.mp4 | jq -r '.id')
|
|
81
|
+
|
|
82
|
+
# 4. Post now
|
|
83
|
+
postaz posts:create -c "Try our app! #fyp" -m "$MEDIA_ID" -i "$PLATFORM_ID" --mode now
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
Both `group:sub` (e.g. `posts:create`) and `group sub` (e.g. `posts create`) syntax work. Every command prints JSON to stdout; errors print `{ "error": { "code", "message" } }` to stderr and exit non-zero.
|
|
91
|
+
|
|
92
|
+
### Authentication
|
|
93
|
+
```bash
|
|
94
|
+
postaz auth:login --api-key <key> [--api-url <url>] # validate + store
|
|
95
|
+
postaz auth:status # check current key
|
|
96
|
+
postaz auth:logout # remove stored key
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Platforms
|
|
100
|
+
```bash
|
|
101
|
+
postaz platforms:list # connected accounts + their IDs
|
|
102
|
+
postaz platforms:schema <platformId> # content/media limits + settings fields
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Media
|
|
106
|
+
```bash
|
|
107
|
+
postaz upload <file> [--mime-type <t>] [--media-type image|video] [--folder-id <id>]
|
|
108
|
+
postaz media:list [--folder-id <id|null>] [--media-type image|video]
|
|
109
|
+
postaz media:folders:list
|
|
110
|
+
postaz media:folders:create <name>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Posts
|
|
114
|
+
```bash
|
|
115
|
+
# Create (date REQUIRED when --mode schedule)
|
|
116
|
+
postaz posts:create -c "<content>" -i "<platformId[,platformId2]>" \
|
|
117
|
+
[-m "<mediaId[,mediaId2]>"] [--mode draft|now|schedule] \
|
|
118
|
+
[-s "2026-07-01T12:00:00Z"] [--settings '{"privacy_level":"PUBLIC_TO_EVERYONE"}']
|
|
119
|
+
|
|
120
|
+
postaz posts:list [--start-date <iso>] [--end-date <iso>]
|
|
121
|
+
postaz posts:get <postId>
|
|
122
|
+
postaz posts:reschedule <postId> -s "2026-07-02T09:00:00Z"
|
|
123
|
+
postaz posts:delete <postId>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`--mode` defaults to `now`, or `schedule` when `-s` is supplied. Use `draft` to
|
|
127
|
+
save without queuing.
|
|
128
|
+
|
|
129
|
+
### Slides (AI — consumes credits)
|
|
130
|
+
```bash
|
|
131
|
+
# Generate a caption + per-slide captions
|
|
132
|
+
postaz slides:content -r "5 tips for launching an indie app" -n 5 \
|
|
133
|
+
[--language en] [--slide-length short|medium|long]
|
|
134
|
+
|
|
135
|
+
# Bake captions onto already-uploaded images
|
|
136
|
+
postaz slides:render --slides '[{"index":0,"mediaId":"<id>","caption":"Tip 1"}]' \
|
|
137
|
+
[--caption-size small|medium|large]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## TikTok Posting Notes
|
|
143
|
+
|
|
144
|
+
- **Media:** exactly **one video**, or **1–35 images** for a photo slideshow. Never mix video and images in one post.
|
|
145
|
+
- **Settings** (pass via `--settings '{…}'`, see `platforms:schema` for the live list):
|
|
146
|
+
- `content_posting_method`: `DIRECT_POST` (publishes directly) or `UPLOAD` (sends to the TikTok inbox to finish manually).
|
|
147
|
+
- `privacy_level`: must be one the account allows — read the allowed values from `platforms:schema`.
|
|
148
|
+
- `title`, `description`, `duet`, `stitch`, `comment`, `brand_content_toggle`, `brand_organic_toggle`, `video_made_with_ai`.
|
|
149
|
+
- TikTok may not return a public post ID immediately; a freshly published post can show `releaseId` as pending until processing completes.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Common Patterns
|
|
154
|
+
|
|
155
|
+
### Publish a TikTok video now
|
|
156
|
+
```bash
|
|
157
|
+
PLATFORM_ID=$(postaz platforms:list | jq -r '.platforms[] | select(.provider=="tiktok") | .id' | head -n1)
|
|
158
|
+
MEDIA_ID=$(postaz upload launch.mp4 | jq -r '.id')
|
|
159
|
+
postaz posts:create -c "We just shipped 🚀 #buildinpublic" -m "$MEDIA_ID" -i "$PLATFORM_ID" \
|
|
160
|
+
--mode now --settings '{"privacy_level":"PUBLIC_TO_EVERYONE"}'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Schedule a photo slideshow
|
|
164
|
+
```bash
|
|
165
|
+
PLATFORM_ID=$(postaz platforms:list | jq -r '.platforms[] | select(.provider=="tiktok") | .id' | head -n1)
|
|
166
|
+
M1=$(postaz upload slide1.jpg | jq -r '.id')
|
|
167
|
+
M2=$(postaz upload slide2.jpg | jq -r '.id')
|
|
168
|
+
M3=$(postaz upload slide3.jpg | jq -r '.id')
|
|
169
|
+
postaz posts:create -c "3 reasons to try our app" -m "$M1,$M2,$M3" -i "$PLATFORM_ID" \
|
|
170
|
+
-s "2026-07-01T15:00:00Z"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### AI captions → render → post
|
|
174
|
+
```bash
|
|
175
|
+
# 1. Generate copy
|
|
176
|
+
CONTENT=$(postaz slides:content -r "Why indie devs love our app" -n 4)
|
|
177
|
+
# 2. Upload base images, then render captions onto them (returns composited media ids)
|
|
178
|
+
# Build the --slides array from your uploaded ids + the generated captions.
|
|
179
|
+
# 3. posts:create with the rendered media ids.
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Common Gotchas
|
|
185
|
+
|
|
186
|
+
1. **Not authenticated** — run `postaz auth:login` or set `POSTAZ_API_KEY`.
|
|
187
|
+
2. **Posting a raw file path** — you must `postaz upload` first and pass the returned `id` to `-m` (Rule 2).
|
|
188
|
+
3. **`post_limit_reached` (409)** — the workspace hit its monthly post cap; upgrade the plan.
|
|
189
|
+
4. **`media_not_ready`** — the media ID is wrong or not owned by this workspace. Re-upload and use the new `id`.
|
|
190
|
+
5. **Wrong `privacy_level`** — use a value from `platforms:schema`; accounts differ.
|
|
191
|
+
6. **Scheduling without a date** — `-s` is required when `--mode schedule`.
|
|
192
|
+
7. **Mixed media** — TikTok rejects video+image in one post.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Quick Reference
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
postaz auth:status # check auth
|
|
200
|
+
postaz platforms:list # get platform IDs
|
|
201
|
+
postaz upload <file> # → { id, publicUrl, status:"READY", … }
|
|
202
|
+
postaz posts:create -c "text" -m "<id>" -i "<pid>" --mode now
|
|
203
|
+
postaz posts:create -c "text" -m "<id>" -i "<pid>" -s "2026-07-01T12:00:00Z"
|
|
204
|
+
postaz posts:list # recent posts
|
|
205
|
+
postaz posts:delete <postId>
|
|
206
|
+
postaz slides:content -r "topic" -n 5 # AI captions
|
|
207
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/api.ts
|
|
7
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
8
|
+
import { basename } from "path";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
14
|
+
var DEFAULT_API_URL = "https://api.postaz.app";
|
|
15
|
+
var CONFIG_DIR = join(homedir(), ".postaz");
|
|
16
|
+
var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
|
|
17
|
+
async function resolveCredentials() {
|
|
18
|
+
const envKey = process.env.POSTAZ_API_KEY?.trim();
|
|
19
|
+
if (envKey) {
|
|
20
|
+
return {
|
|
21
|
+
apiKey: envKey,
|
|
22
|
+
apiUrl: process.env.POSTAZ_API_URL?.trim() || void 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const stored = await readStoredCredentials();
|
|
26
|
+
if (stored?.apiKey) {
|
|
27
|
+
return {
|
|
28
|
+
apiKey: stored.apiKey,
|
|
29
|
+
apiUrl: process.env.POSTAZ_API_URL?.trim() || stored.apiUrl
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function resolveBaseUrl(apiUrl) {
|
|
35
|
+
const host = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
36
|
+
return host.endsWith("/public/v1") ? host : `${host}/public/v1`;
|
|
37
|
+
}
|
|
38
|
+
async function readStoredCredentials() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readFile(CREDENTIALS_PATH, "utf8");
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (typeof parsed.apiKey === "string" && parsed.apiKey) {
|
|
43
|
+
return {
|
|
44
|
+
apiKey: parsed.apiKey,
|
|
45
|
+
apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function writeStoredCredentials(credentials) {
|
|
54
|
+
await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
55
|
+
await writeFile(
|
|
56
|
+
CREDENTIALS_PATH,
|
|
57
|
+
`${JSON.stringify(credentials, null, 2)}
|
|
58
|
+
`,
|
|
59
|
+
{ mode: 384 }
|
|
60
|
+
);
|
|
61
|
+
return CREDENTIALS_PATH;
|
|
62
|
+
}
|
|
63
|
+
async function clearStoredCredentials() {
|
|
64
|
+
try {
|
|
65
|
+
await rm(CREDENTIALS_PATH);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/output.ts
|
|
73
|
+
function printJson(value) {
|
|
74
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
var CliError = class extends Error {
|
|
78
|
+
constructor(message, code = "cli_error", details) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.code = code;
|
|
81
|
+
this.details = details;
|
|
82
|
+
this.name = "CliError";
|
|
83
|
+
}
|
|
84
|
+
code;
|
|
85
|
+
details;
|
|
86
|
+
};
|
|
87
|
+
function failAndExit(error) {
|
|
88
|
+
if (error instanceof CliError) {
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
`${JSON.stringify(
|
|
91
|
+
{ error: { code: error.code, message: error.message, details: error.details } },
|
|
92
|
+
null,
|
|
93
|
+
2
|
|
94
|
+
)}
|
|
95
|
+
`
|
|
96
|
+
);
|
|
97
|
+
} else if (error instanceof Error) {
|
|
98
|
+
process.stderr.write(
|
|
99
|
+
`${JSON.stringify(
|
|
100
|
+
{ error: { code: "unexpected_error", message: error.message } },
|
|
101
|
+
null,
|
|
102
|
+
2
|
|
103
|
+
)}
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`${JSON.stringify(
|
|
109
|
+
{ error: { code: "unexpected_error", message: String(error) } },
|
|
110
|
+
null,
|
|
111
|
+
2
|
|
112
|
+
)}
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/api.ts
|
|
120
|
+
var PostazApiClient = class _PostazApiClient {
|
|
121
|
+
constructor(baseUrl, apiKey) {
|
|
122
|
+
this.baseUrl = baseUrl;
|
|
123
|
+
this.apiKey = apiKey;
|
|
124
|
+
}
|
|
125
|
+
baseUrl;
|
|
126
|
+
apiKey;
|
|
127
|
+
/**
|
|
128
|
+
* Builds a client from an explicit key (used by `auth:login` before anything
|
|
129
|
+
* is persisted) or from resolved credentials (env → stored file).
|
|
130
|
+
*/
|
|
131
|
+
static async create(options) {
|
|
132
|
+
if (options?.apiKey) {
|
|
133
|
+
return new _PostazApiClient(resolveBaseUrl(options.apiUrl), options.apiKey);
|
|
134
|
+
}
|
|
135
|
+
const credentials = await resolveCredentials();
|
|
136
|
+
if (!credentials) {
|
|
137
|
+
throw new CliError(
|
|
138
|
+
"No API key found. Run `postaz auth:login --api-key <key>` or set POSTAZ_API_KEY.",
|
|
139
|
+
"not_authenticated"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return new _PostazApiClient(
|
|
143
|
+
resolveBaseUrl(options?.apiUrl ?? credentials.apiUrl),
|
|
144
|
+
credentials.apiKey
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
async get(path, query) {
|
|
148
|
+
return this.request("GET", path, { query });
|
|
149
|
+
}
|
|
150
|
+
async postJson(path, body) {
|
|
151
|
+
return this.request("POST", path, { jsonBody: body });
|
|
152
|
+
}
|
|
153
|
+
async patchJson(path, body) {
|
|
154
|
+
return this.request("PATCH", path, { jsonBody: body });
|
|
155
|
+
}
|
|
156
|
+
async delete(path) {
|
|
157
|
+
await this.request("DELETE", path, {});
|
|
158
|
+
}
|
|
159
|
+
/** Uploads a local file via multipart/form-data to `POST /media`. */
|
|
160
|
+
async uploadFile(filePath, options) {
|
|
161
|
+
let bytes;
|
|
162
|
+
try {
|
|
163
|
+
bytes = await readFile2(filePath);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new CliError(`File not found or unreadable: ${filePath}`, "file_unreadable");
|
|
166
|
+
}
|
|
167
|
+
const form = new FormData();
|
|
168
|
+
const fileName = basename(filePath);
|
|
169
|
+
const mimeType = options?.mimeType ?? inferMimeType(fileName);
|
|
170
|
+
form.set("file", new Blob([bytes], { type: mimeType }), fileName);
|
|
171
|
+
form.set("fileName", fileName);
|
|
172
|
+
form.set("mimeType", mimeType);
|
|
173
|
+
if (options?.mediaType) {
|
|
174
|
+
form.set("mediaType", options.mediaType);
|
|
175
|
+
}
|
|
176
|
+
if (options?.folderId !== void 0) {
|
|
177
|
+
form.set("folderId", options.folderId === null ? "null" : options.folderId);
|
|
178
|
+
}
|
|
179
|
+
return this.request("POST", "/media", { formBody: form });
|
|
180
|
+
}
|
|
181
|
+
async request(method, path, options) {
|
|
182
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
183
|
+
if (options.query) {
|
|
184
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
185
|
+
if (value !== void 0) {
|
|
186
|
+
url.searchParams.set(key, value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const headers = {
|
|
191
|
+
authorization: `Bearer ${this.apiKey}`
|
|
192
|
+
};
|
|
193
|
+
let body;
|
|
194
|
+
if (options.jsonBody !== void 0) {
|
|
195
|
+
headers["content-type"] = "application/json";
|
|
196
|
+
body = JSON.stringify(options.jsonBody);
|
|
197
|
+
} else if (options.formBody) {
|
|
198
|
+
body = options.formBody;
|
|
199
|
+
}
|
|
200
|
+
let response;
|
|
201
|
+
try {
|
|
202
|
+
response = await fetch(url, { method, headers, body });
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new CliError(
|
|
205
|
+
`Could not reach the Postaz API at ${url.host}. ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
"network_error"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (response.status === 204) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
const text = await response.text();
|
|
213
|
+
const parsed = text ? safeJsonParse(text) : void 0;
|
|
214
|
+
if (!response.ok) {
|
|
215
|
+
const errorBody = parsed ?? {};
|
|
216
|
+
throw new CliError(
|
|
217
|
+
errorBody.message ?? `Request failed with status ${response.status}.`,
|
|
218
|
+
errorBody.code ?? `http_${response.status}`,
|
|
219
|
+
errorBody.details
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
function safeJsonParse(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(value);
|
|
228
|
+
} catch {
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function inferMimeType(fileName) {
|
|
233
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
234
|
+
switch (ext) {
|
|
235
|
+
case "jpg":
|
|
236
|
+
case "jpeg":
|
|
237
|
+
return "image/jpeg";
|
|
238
|
+
case "webp":
|
|
239
|
+
return "image/webp";
|
|
240
|
+
case "mp4":
|
|
241
|
+
return "video/mp4";
|
|
242
|
+
case "webm":
|
|
243
|
+
return "video/webm";
|
|
244
|
+
case "mov":
|
|
245
|
+
return "video/quicktime";
|
|
246
|
+
default:
|
|
247
|
+
return "application/octet-stream";
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/commands/auth.ts
|
|
252
|
+
function registerAuthCommands(program2) {
|
|
253
|
+
const auth = program2.command("auth").description("Authentication commands");
|
|
254
|
+
auth.command("login").description("Store a Postaz API key after validating it").requiredOption("--api-key <key>", "A postaz_sk_\u2026 API key from your workspace settings").option("--api-url <url>", "Override the API host (defaults to https://api.postaz.app)").action(async (options) => {
|
|
255
|
+
try {
|
|
256
|
+
const client = await PostazApiClient.create({
|
|
257
|
+
apiKey: options.apiKey,
|
|
258
|
+
apiUrl: options.apiUrl
|
|
259
|
+
});
|
|
260
|
+
await client.get("/platforms");
|
|
261
|
+
const path = await writeStoredCredentials({
|
|
262
|
+
apiKey: options.apiKey,
|
|
263
|
+
apiUrl: options.apiUrl
|
|
264
|
+
});
|
|
265
|
+
printJson({ status: "authenticated", credentialsPath: path });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
failAndExit(error);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
auth.command("status").description("Check whether the stored API key is valid").action(async () => {
|
|
271
|
+
try {
|
|
272
|
+
const credentials = await resolveCredentials();
|
|
273
|
+
if (!credentials) {
|
|
274
|
+
throw new CliError(
|
|
275
|
+
"Not authenticated. Run `postaz auth:login --api-key <key>` or set POSTAZ_API_KEY.",
|
|
276
|
+
"not_authenticated"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
const client = await PostazApiClient.create();
|
|
280
|
+
const platforms = await client.get("/platforms");
|
|
281
|
+
printJson({
|
|
282
|
+
status: "authenticated",
|
|
283
|
+
source: process.env.POSTAZ_API_KEY ? "env" : "file",
|
|
284
|
+
connectedPlatforms: platforms.platforms.length
|
|
285
|
+
});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
failAndExit(error);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
auth.command("logout").description("Remove stored credentials").action(async () => {
|
|
291
|
+
try {
|
|
292
|
+
const removed = await clearStoredCredentials();
|
|
293
|
+
printJson({
|
|
294
|
+
status: removed ? "logged_out" : "no_stored_credentials",
|
|
295
|
+
credentialsPath: CREDENTIALS_PATH
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
failAndExit(error);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/commands/platforms.ts
|
|
304
|
+
function registerPlatformCommands(program2) {
|
|
305
|
+
const platforms = program2.command("platforms").description("Discover connected social accounts (TikTok, etc.)");
|
|
306
|
+
platforms.command("list").description("List the workspace\u2019s connected platforms and their IDs").action(async () => {
|
|
307
|
+
try {
|
|
308
|
+
const client = await PostazApiClient.create();
|
|
309
|
+
printJson(await client.get("/platforms"));
|
|
310
|
+
} catch (error) {
|
|
311
|
+
failAndExit(error);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
platforms.command("schema <platformId>").description("Get the posting schema (limits, settings fields) for a platform").action(async (platformId) => {
|
|
315
|
+
try {
|
|
316
|
+
const client = await PostazApiClient.create();
|
|
317
|
+
printJson(await client.get(`/platforms/${encodeURIComponent(platformId)}/schema`));
|
|
318
|
+
} catch (error) {
|
|
319
|
+
failAndExit(error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/commands/media.ts
|
|
325
|
+
function registerMediaCommands(program2) {
|
|
326
|
+
program2.command("upload <file>").description("Upload a local image or video and get a READY media object (with id)").option("--mime-type <type>", "Override the detected MIME type").option("--media-type <type>", "image or video (inferred from MIME type by default)").option("--folder-id <id>", "Place the upload in a media folder").action(
|
|
327
|
+
async (file, options) => {
|
|
328
|
+
try {
|
|
329
|
+
const client = await PostazApiClient.create();
|
|
330
|
+
printJson(
|
|
331
|
+
await client.uploadFile(file, {
|
|
332
|
+
mimeType: options.mimeType,
|
|
333
|
+
mediaType: options.mediaType,
|
|
334
|
+
folderId: options.folderId
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
failAndExit(error);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
const media = program2.command("media").description("Media library commands");
|
|
343
|
+
media.command("list").description("List uploaded media objects").option("--folder-id <id>", 'Filter by folder id, or "null" for the root folder').option("--media-type <type>", "Filter by image or video").action(async (options) => {
|
|
344
|
+
try {
|
|
345
|
+
const client = await PostazApiClient.create();
|
|
346
|
+
printJson(
|
|
347
|
+
await client.get("/media", {
|
|
348
|
+
folderId: options.folderId,
|
|
349
|
+
mediaType: options.mediaType
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
failAndExit(error);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const folders = media.command("folders").description("Media folder commands");
|
|
357
|
+
folders.command("list").description("List media folders").action(async () => {
|
|
358
|
+
try {
|
|
359
|
+
const client = await PostazApiClient.create();
|
|
360
|
+
printJson(await client.get("/media/folders"));
|
|
361
|
+
} catch (error) {
|
|
362
|
+
failAndExit(error);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
folders.command("create <name>").description("Create a media folder").action(async (name) => {
|
|
366
|
+
try {
|
|
367
|
+
const client = await PostazApiClient.create();
|
|
368
|
+
printJson(await client.postJson("/media/folders", { name }));
|
|
369
|
+
} catch (error) {
|
|
370
|
+
failAndExit(error);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/commands/posts.ts
|
|
376
|
+
var POST_MODES = ["draft", "now", "schedule"];
|
|
377
|
+
function splitIds(value) {
|
|
378
|
+
if (!value) {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
return value.split(",").map((id) => id.trim()).filter(Boolean);
|
|
382
|
+
}
|
|
383
|
+
function registerPostCommands(program2) {
|
|
384
|
+
const posts = program2.command("posts").description("Create and manage posts");
|
|
385
|
+
posts.command("create").description("Create a post for one or more platforms").requiredOption("-c, --content <text>", "Post caption / content").requiredOption(
|
|
386
|
+
"-i, --platform <ids>",
|
|
387
|
+
"Comma-separated platform IDs (from `postaz platforms:list`)"
|
|
388
|
+
).option(
|
|
389
|
+
"-m, --media <ids>",
|
|
390
|
+
"Comma-separated media IDs (from `postaz upload`). Required for TikTok."
|
|
391
|
+
).option("--mode <mode>", "draft | now | schedule (default: now, or schedule if -s is set)").option("-s, --scheduled-for <iso>", "ISO 8601 time to publish, e.g. 2026-07-01T12:00:00Z").option("--settings <json>", "Provider settings as a JSON string").action(
|
|
392
|
+
async (options) => {
|
|
393
|
+
try {
|
|
394
|
+
const platformIds = splitIds(options.platform);
|
|
395
|
+
if (platformIds.length === 0) {
|
|
396
|
+
throw new CliError("At least one platform ID is required.", "platform_required");
|
|
397
|
+
}
|
|
398
|
+
const mode = resolveMode(options.mode, options.scheduledFor);
|
|
399
|
+
if (mode === "schedule" && !options.scheduledFor) {
|
|
400
|
+
throw new CliError(
|
|
401
|
+
"scheduledFor (-s) is required when --mode is schedule.",
|
|
402
|
+
"scheduled_for_required"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const settings = parseSettings(options.settings);
|
|
406
|
+
const mediaIds = splitIds(options.media);
|
|
407
|
+
const client = await PostazApiClient.create();
|
|
408
|
+
printJson(
|
|
409
|
+
await client.postJson("/posts", {
|
|
410
|
+
platformIds,
|
|
411
|
+
content: options.content,
|
|
412
|
+
mode,
|
|
413
|
+
...options.scheduledFor ? { scheduledFor: options.scheduledFor } : {},
|
|
414
|
+
...mediaIds.length > 0 ? { mediaIds } : {},
|
|
415
|
+
...settings ? { settings } : {}
|
|
416
|
+
})
|
|
417
|
+
);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
failAndExit(error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
posts.command("list").description("List posts in a date range (defaults to the API\u2019s default window)").option("--start-date <iso>", "ISO 8601 start of range").option("--end-date <iso>", "ISO 8601 end of range").action(async (options) => {
|
|
424
|
+
try {
|
|
425
|
+
const client = await PostazApiClient.create();
|
|
426
|
+
printJson(
|
|
427
|
+
await client.get("/posts", {
|
|
428
|
+
startDate: options.startDate,
|
|
429
|
+
endDate: options.endDate
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
failAndExit(error);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
posts.command("get <postId>").description("Get a single post group (root + comments)").action(async (postId) => {
|
|
437
|
+
try {
|
|
438
|
+
const client = await PostazApiClient.create();
|
|
439
|
+
printJson(await client.get(`/posts/${encodeURIComponent(postId)}`));
|
|
440
|
+
} catch (error) {
|
|
441
|
+
failAndExit(error);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
posts.command("reschedule <postId>").description("Change the scheduled time of a post").requiredOption("-s, --scheduled-for <iso>", "New ISO 8601 publish time").action(async (postId, options) => {
|
|
445
|
+
try {
|
|
446
|
+
const client = await PostazApiClient.create();
|
|
447
|
+
printJson(
|
|
448
|
+
await client.patchJson(`/posts/${encodeURIComponent(postId)}/schedule`, {
|
|
449
|
+
scheduledFor: options.scheduledFor
|
|
450
|
+
})
|
|
451
|
+
);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
failAndExit(error);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
posts.command("delete <postId>").description("Delete a post (and its group)").action(async (postId) => {
|
|
457
|
+
try {
|
|
458
|
+
const client = await PostazApiClient.create();
|
|
459
|
+
await client.delete(`/posts/${encodeURIComponent(postId)}`);
|
|
460
|
+
printJson({ status: "deleted", postId });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
failAndExit(error);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function resolveMode(rawMode, scheduledFor) {
|
|
467
|
+
if (rawMode) {
|
|
468
|
+
if (!POST_MODES.includes(rawMode)) {
|
|
469
|
+
throw new CliError(
|
|
470
|
+
`Invalid --mode "${rawMode}". Use one of: ${POST_MODES.join(", ")}.`,
|
|
471
|
+
"invalid_mode"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return rawMode;
|
|
475
|
+
}
|
|
476
|
+
return scheduledFor ? "schedule" : "now";
|
|
477
|
+
}
|
|
478
|
+
function parseSettings(raw) {
|
|
479
|
+
if (!raw) {
|
|
480
|
+
return void 0;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(raw);
|
|
484
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
485
|
+
return parsed;
|
|
486
|
+
}
|
|
487
|
+
throw new Error("not an object");
|
|
488
|
+
} catch {
|
|
489
|
+
throw new CliError("--settings must be a JSON object string.", "invalid_settings");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/commands/slides.ts
|
|
494
|
+
function registerSlidesCommands(program2) {
|
|
495
|
+
const slides = program2.command("slides").description("AI caption generation and slideshow rendering (metered against AI credits)");
|
|
496
|
+
slides.command("content").description("Generate a caption + per-slide captions for a TikTok photo slideshow").requiredOption("-r, --request <text>", "What the slideshow should be about (max 500 chars)").requiredOption("-n, --slide-count <n>", "Number of slides (3\u201310)", (v) => Number.parseInt(v, 10)).option("-l, --language <lang>", "Language for the generated text").option(
|
|
497
|
+
"--slide-length <length>",
|
|
498
|
+
"short | medium | long \u2014 content density per slide (default: medium)"
|
|
499
|
+
).action(
|
|
500
|
+
async (options) => {
|
|
501
|
+
try {
|
|
502
|
+
if (!Number.isInteger(options.slideCount)) {
|
|
503
|
+
throw new CliError("--slide-count must be an integer between 3 and 10.", "invalid_slide_count");
|
|
504
|
+
}
|
|
505
|
+
const client = await PostazApiClient.create();
|
|
506
|
+
printJson(
|
|
507
|
+
await client.postJson("/slides/content", {
|
|
508
|
+
userRequest: options.request,
|
|
509
|
+
slideCount: options.slideCount,
|
|
510
|
+
...options.language ? { language: options.language } : {},
|
|
511
|
+
...options.slideLength ? { slideLength: options.slideLength } : {}
|
|
512
|
+
})
|
|
513
|
+
);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
failAndExit(error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
slides.command("render").description("Bake captions onto uploaded images, producing composited slide media").requiredOption(
|
|
520
|
+
"--slides <json>",
|
|
521
|
+
"JSON array of { index, mediaId, caption } objects"
|
|
522
|
+
).option("--caption-size <size>", "small | medium | large (default: medium)").action(async (options) => {
|
|
523
|
+
try {
|
|
524
|
+
const parsed = parseSlides(options.slides);
|
|
525
|
+
const client = await PostazApiClient.create();
|
|
526
|
+
printJson(
|
|
527
|
+
await client.postJson("/slides/render", {
|
|
528
|
+
slides: parsed,
|
|
529
|
+
...options.captionSize ? { captionSize: options.captionSize } : {}
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
failAndExit(error);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function parseSlides(raw) {
|
|
538
|
+
let parsed;
|
|
539
|
+
try {
|
|
540
|
+
parsed = JSON.parse(raw);
|
|
541
|
+
} catch {
|
|
542
|
+
throw new CliError("--slides must be a valid JSON array.", "invalid_slides");
|
|
543
|
+
}
|
|
544
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
545
|
+
throw new CliError("--slides must be a non-empty JSON array.", "invalid_slides");
|
|
546
|
+
}
|
|
547
|
+
return parsed;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/index.ts
|
|
551
|
+
var program = new Command();
|
|
552
|
+
program.name("postaz").description("Postaz CLI \u2014 schedule and publish TikTok posts from the terminal or an AI agent.").version("0.1.0").enablePositionalOptions();
|
|
553
|
+
registerAuthCommands(program);
|
|
554
|
+
registerPlatformCommands(program);
|
|
555
|
+
registerMediaCommands(program);
|
|
556
|
+
registerPostCommands(program);
|
|
557
|
+
registerSlidesCommands(program);
|
|
558
|
+
async function main() {
|
|
559
|
+
await program.parseAsync(normalizeColonArgs(process.argv));
|
|
560
|
+
}
|
|
561
|
+
function normalizeColonArgs(argv) {
|
|
562
|
+
const [node, script, first, ...rest] = argv;
|
|
563
|
+
if (first && /^[a-z]+:[a-z-]+$/.test(first)) {
|
|
564
|
+
const [group, sub] = first.split(":");
|
|
565
|
+
return [node, script, group, sub, ...rest];
|
|
566
|
+
}
|
|
567
|
+
return argv;
|
|
568
|
+
}
|
|
569
|
+
main().catch(failAndExit);
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@postaz/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Postaz CLI — the cheapest way to grow your app on TikTok. Schedule and publish TikTok posts (video + photo slideshows), generate captions, and manage media from the command line or an AI agent.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"postaz",
|
|
7
|
+
"tiktok",
|
|
8
|
+
"social-media",
|
|
9
|
+
"scheduling",
|
|
10
|
+
"automation",
|
|
11
|
+
"ai-agent",
|
|
12
|
+
"cli"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://postaz.app",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/tonymanh-dev/postaz-agent.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/tonymanh-dev/postaz-agent/issues"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"postaz": "dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"SKILL.md",
|
|
33
|
+
".claude-plugin"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"prepublishOnly": "pnpm run build"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"commander": "^12.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.0",
|
|
49
|
+
"tsup": "^8.3.5",
|
|
50
|
+
"typescript": "^5.7.2"
|
|
51
|
+
}
|
|
52
|
+
}
|